mirror of
https://github.com/microtherion/VocalEasel.git
synced 2024-12-22 11:14:00 +00:00
Factor our chord naming/parsing, unit test works
This commit is contained in:
parent
11d4a65076
commit
f47c7d725d
|
@ -11,8 +11,11 @@
|
||||||
#include "VLPitchName.h"
|
#include "VLPitchName.h"
|
||||||
#include "VLModel.h"
|
#include "VLModel.h"
|
||||||
|
|
||||||
const char * kVLSharpStr = "\xE2\x99\xAF";
|
#define SHARP "\xE2\x99\xAF"
|
||||||
const char * kVLFlatStr = "\xE2\x99\xAD";
|
#define FLAT "\xE2\x99\xAD"
|
||||||
|
|
||||||
|
const char * kVLSharpStr = SHARP;
|
||||||
|
const char * kVLFlatStr = FLAT;
|
||||||
const char * kVL2SharpStr = "\xF0\x9D\x84\xAA";
|
const char * kVL2SharpStr = "\xF0\x9D\x84\xAA";
|
||||||
const char * kVL2FlatStr = "\xF0\x9D\x84\xAB";
|
const char * kVL2FlatStr = "\xF0\x9D\x84\xAB";
|
||||||
const char * kVLNaturalStr = "\xE2\x99\xAE";
|
const char * kVLNaturalStr = "\xE2\x99\xAE";
|
||||||
|
@ -120,3 +123,169 @@ int8_t VLParsePitch(std::string & str, size_t at, uint16_t * accidental)
|
||||||
|
|
||||||
return pitch;
|
return pitch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const char * kStepNames[] = {
|
||||||
|
"", "", "sus2", "", "", "sus", FLAT "5", "", SHARP "5", "6",
|
||||||
|
"7", SHARP "7", "", FLAT "9", "9", SHARP "9", "",
|
||||||
|
"11", SHARP "11", "", FLAT "13", "13"
|
||||||
|
};
|
||||||
|
|
||||||
|
#define _ VLChord::
|
||||||
|
|
||||||
|
void VLChordName(int8_t pitch, uint16_t accidental, uint32_t steps,
|
||||||
|
int8_t rootPitch, uint16_t rootAccidental,
|
||||||
|
std::string & baseName, std::string & extName, std::string & rootName)
|
||||||
|
{
|
||||||
|
baseName = VLPitchName(pitch, accidental);
|
||||||
|
if (rootPitch == VLNote::kNoPitch)
|
||||||
|
rootName.clear();
|
||||||
|
else
|
||||||
|
rootName = VLPitchName(rootPitch, rootAccidental);
|
||||||
|
//
|
||||||
|
// m / dim
|
||||||
|
//
|
||||||
|
extName.erase();
|
||||||
|
if (steps & _ kmMin3rd)
|
||||||
|
if (steps & _ kmDim5th
|
||||||
|
&& !(steps & (_ km5th|_ kmMin7th|_ kmMaj7th|_ kmMin9th|_ kmMaj9th|
|
||||||
|
_ km11th|_ kmAug11th|_ kmMin13th|_ kmMaj13th))
|
||||||
|
) {
|
||||||
|
extName += "dim";
|
||||||
|
steps|= (steps & _ kmDim7th) << 1;
|
||||||
|
steps&= ~(_ kmMin3rd|_ kmDim5th|_ kmDim7th);
|
||||||
|
} else {
|
||||||
|
baseName += "m";
|
||||||
|
steps&= ~_ kmMin3rd;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// +
|
||||||
|
//
|
||||||
|
steps &= ~(_ kmUnison | _ kmMaj3rd | _ km5th);
|
||||||
|
if (steps == _ kmAug5th) {
|
||||||
|
extName += "+";
|
||||||
|
steps= 0;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Maj
|
||||||
|
//
|
||||||
|
if (steps & _ kmMaj7th) {
|
||||||
|
extName += "Maj";
|
||||||
|
steps &= ~_ kmMaj7th;
|
||||||
|
steps |= +_ kmMin7th; // Write out the 7 for clarification
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// 6/9
|
||||||
|
//
|
||||||
|
if ((steps & (_ kmDim7th|_ kmMaj9th)) == (_ kmDim7th|_ kmMaj9th)) {
|
||||||
|
extName += "69";
|
||||||
|
steps &= ~(_ kmDim7th|_ kmMaj9th);
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Other extensions. Only the highest unaltered extension is listed.
|
||||||
|
//
|
||||||
|
bool has7th = steps & (_ kmMin7th|_ kmMaj7th);
|
||||||
|
bool has9th = steps & (_ kmMin9th|_ kmMaj9th|_ kmAug9th);
|
||||||
|
if ((steps & _ kmMaj13th) && has7th && has9th ) {
|
||||||
|
extName += kStepNames[_ kMaj13th];
|
||||||
|
steps &= ~(_ kmMin7th |_ kmMaj9th |_ km11th |_ kmMaj13th);
|
||||||
|
} else if ((steps & _ km11th) && has7th && has9th) {
|
||||||
|
extName += kStepNames[_ k11th];
|
||||||
|
steps &= ~(_ kmMin7th | _ kmMaj9th | _ km11th);
|
||||||
|
} else if ((steps & _ kmMaj9th) && has7th) {
|
||||||
|
extName += kStepNames[_ kMaj9th];
|
||||||
|
steps &= ~(_ kmMin7th | _ kmMaj9th);
|
||||||
|
} else if (steps & _ kmMin7th) {
|
||||||
|
extName += kStepNames[_ kMin7th];
|
||||||
|
steps &= ~(_ kmMin7th);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int step = _ kMin2nd; steps; ++step)
|
||||||
|
if (steps & (1 << step)) {
|
||||||
|
if ((1 << step) & (_ kmMaj9th|_ km11th|_ kmMaj13th))
|
||||||
|
extName += "add";
|
||||||
|
extName += kStepNames[step];
|
||||||
|
steps &= ~(1 << step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const VLChordModifier kModifiers[] = {
|
||||||
|
{"b13", _ kmMin13th, 0},
|
||||||
|
{FLAT "13", _ kmMin13th, 0},
|
||||||
|
{"add13", _ kmMaj13th, 0},
|
||||||
|
{"13", _ kmMin7th | _ kmMaj9th | _ km11th | _ kmMaj13th, 0},
|
||||||
|
{"#11", _ kmAug11th, _ km11th},
|
||||||
|
{SHARP "11", _ kmAug11th, _ km11th},
|
||||||
|
{"+11", _ kmAug11th, _ km11th},
|
||||||
|
{"add11", _ km11th, 0},
|
||||||
|
{"11", _ kmMin7th | _ kmMaj9th | _ km11th, 0},
|
||||||
|
{"#9", _ kmAug9th, _ kmMaj9th},
|
||||||
|
{SHARP "9", _ kmAug9th, _ kmMaj9th},
|
||||||
|
{"+9", _ kmAug9th, _ kmMaj9th},
|
||||||
|
{"b9", _ kmMin9th, _ kmMaj9th},
|
||||||
|
{FLAT "9", _ kmMin9th, _ kmMaj9th},
|
||||||
|
{"-9", _ kmMin9th, _ kmMaj9th},
|
||||||
|
{"69", _ kmDim7th | _ kmMaj9th, 0},
|
||||||
|
{"add9", _ kmMaj9th, 0},
|
||||||
|
{"9", _ kmMin7th | _ kmMaj9th, 0},
|
||||||
|
{"7", _ kmMin7th, 0},
|
||||||
|
{"maj", _ kmMaj7th, _ kmMin7th},
|
||||||
|
{"6", _ kmDim7th, 0},
|
||||||
|
{"#5", _ kmAug5th, _ km5th},
|
||||||
|
{SHARP "5", _ kmAug5th, _ km5th},
|
||||||
|
{"+5", _ kmAug5th, _ km5th},
|
||||||
|
{"aug", _ kmAug5th, _ km5th},
|
||||||
|
{"+", _ kmAug5th, _ km5th},
|
||||||
|
{"b5", _ kmDim5th, _ km5th},
|
||||||
|
{FLAT "5", _ kmDim5th, _ km5th},
|
||||||
|
{"-5", _ kmDim5th, _ km5th},
|
||||||
|
{"sus4", _ km4th, _ kmMaj3rd},
|
||||||
|
{"sus2", _ kmMaj2nd, _ kmMaj3rd},
|
||||||
|
{"sus", _ km4th, _ kmMaj3rd},
|
||||||
|
{"4", _ km4th, _ kmMaj3rd},
|
||||||
|
{"add3", _ kmMaj3rd, 0},
|
||||||
|
{"2", _ kmMaj2nd, _ kmMaj3rd},
|
||||||
|
{NULL, 0, 0}
|
||||||
|
};
|
||||||
|
|
||||||
|
int8_t VLParseChord(std::string & str, uint16_t * accidental, uint32_t * steps,
|
||||||
|
int8_t * rootPitch, uint16_t * rootAccidental)
|
||||||
|
{
|
||||||
|
int8_t pitch = VLParsePitch(str, 0, accidental);
|
||||||
|
if (pitch < 0)
|
||||||
|
return pitch;
|
||||||
|
size_t root = str.find('/');
|
||||||
|
if (root != std::string::npos) {
|
||||||
|
*rootPitch = VLParsePitch(str, root+1, rootAccidental);
|
||||||
|
if (*rootPitch < 0)
|
||||||
|
return kPitchError;
|
||||||
|
str.erase(root, 1);
|
||||||
|
} else {
|
||||||
|
*rootPitch = VLNote::kNoPitch;
|
||||||
|
*rootAccidental = 0;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Apply modifiers
|
||||||
|
//
|
||||||
|
*steps = _ kmUnison | _ kmMaj3rd | _ km5th;
|
||||||
|
|
||||||
|
for (const VLChordModifier * mod = kModifiers; mod->fName && str.size()
|
||||||
|
&& str != "dim" && str != "m" && str != "-"; ++mod
|
||||||
|
) {
|
||||||
|
size_t pos = str.find(mod->fName);
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
str.erase(pos, strlen(mod->fName));
|
||||||
|
*steps &= ~mod->fDelSteps;
|
||||||
|
*steps |= mod->fAddSteps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (str == "m" || str == "-") {
|
||||||
|
*steps = (*steps & ~_ kmMaj3rd) | _ kmMin3rd;
|
||||||
|
str.erase(0, 1);
|
||||||
|
} else if (str == "dim") {
|
||||||
|
uint32_t st = *steps & (_ kmMaj3rd |_ km5th |_ kmMin7th);
|
||||||
|
*steps = (*steps ^ st) | (st >> 1); // Diminish 3rd, 5th, and 7th, if present
|
||||||
|
str.erase(0, 3);
|
||||||
|
}
|
||||||
|
return str.empty() ? pitch : kPitchError;
|
||||||
|
}
|
||||||
|
|
|
@ -29,3 +29,16 @@ std::string VLPitchName(int8_t pitch, uint16_t accidental);
|
||||||
//
|
//
|
||||||
enum { kPitchError = -1 };
|
enum { kPitchError = -1 };
|
||||||
int8_t VLParsePitch(std::string & str, size_t at, uint16_t * accidental);
|
int8_t VLParsePitch(std::string & str, size_t at, uint16_t * accidental);
|
||||||
|
|
||||||
|
//
|
||||||
|
// UTF-8 representation of chord
|
||||||
|
//
|
||||||
|
void VLChordName(int8_t pitch, uint16_t accidental, uint32_t steps,
|
||||||
|
int8_t rootPitch, uint16_t rootAccidental,
|
||||||
|
std::string & baseName, std::string & extName, std::string & rootName);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Parse chord name, erase from string
|
||||||
|
//
|
||||||
|
int8_t VLParseChord(std::string & str, uint16_t * accidental, uint32_t * steps,
|
||||||
|
int8_t * rootPitch, uint16_t * rootAccidental);
|
|
@ -99,7 +99,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
#define TestParsePitchAtOffset(pitch, accidental, str, at) \
|
#define TestParsePitchAtOffset(pitch, accidental, str, at) \
|
||||||
{ std::string cppStr = [str UTF8String]; uint16_t acc; \
|
do { std::string cppStr = [str UTF8String]; uint16_t acc; \
|
||||||
STAssertEquals(pitch, (int)VLParsePitch(cppStr, at, &acc),@"VLParsePitch(%@, %lu)", str, at); \
|
STAssertEquals(pitch, (int)VLParsePitch(cppStr, at, &acc),@"VLParsePitch(%@, %lu)", str, at); \
|
||||||
STAssertEquals((uint16_t)accidental, acc, @"VLParsePitch(%@, %lu) [accidental]", str, at); \
|
STAssertEquals((uint16_t)accidental, acc, @"VLParsePitch(%@, %lu) [accidental]", str, at); \
|
||||||
STAssertEquals((size_t)at, cppStr.size(), @"VLParsePitch(%@, %lu) [cleanup]", str, at); \
|
STAssertEquals((size_t)at, cppStr.size(), @"VLParsePitch(%@, %lu) [cleanup]", str, at); \
|
||||||
|
@ -136,4 +136,91 @@
|
||||||
TestParsePitchAtOffset(kB, 0, @"C/B", 2);
|
TestParsePitchAtOffset(kB, 0, @"C/B", 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define TestChord(base, ext, root, stp, str) \
|
||||||
|
do { int8_t pitch; uint16_t accidental; uint32_t steps; int8_t rootPitch; uint16_t rootAccidental; \
|
||||||
|
std::string baseName, extName, rootName, cppStr = [str UTF8String]; \
|
||||||
|
pitch = VLParseChord(cppStr, &accidental, &steps, &rootPitch, &rootAccidental); \
|
||||||
|
STAssertEquals((size_t)0, cppStr.size(), @"VLParseChord(%@)", str); \
|
||||||
|
STAssertEquals((uint32_t)stp, steps, @"VLParseChord(%@)", str); \
|
||||||
|
VLChordName(pitch, accidental, steps, rootPitch, rootAccidental, baseName, extName, rootName); \
|
||||||
|
STAssertEqualObjects(base, [NSString stringWithUTF8String:baseName.c_str()], \
|
||||||
|
@"VLChordName(%@ <%d,%d,%08x,%d,%d>) [base]", str, \
|
||||||
|
pitch, accidental, steps, rootPitch, rootAccidental); \
|
||||||
|
STAssertEqualObjects(ext, [NSString stringWithUTF8String:extName.c_str()], \
|
||||||
|
@"VLChordName(%@ <%d,%d,%08x,%d,%d>) [ext]", str, \
|
||||||
|
pitch, accidental, steps, rootPitch, rootAccidental); \
|
||||||
|
STAssertEqualObjects(root, [NSString stringWithUTF8String:rootName.c_str()], \
|
||||||
|
@"VLChordName(%@ <%d,%d,%08x,%d,%d>) [root]", str, \
|
||||||
|
pitch, accidental, steps, rootPitch, rootAccidental); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
- (void)testChords
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Chords appearing in The New Real Book
|
||||||
|
//
|
||||||
|
TestChord(@"C", @"", @"", 0x00000091, @"c");
|
||||||
|
TestChord(@"C", @"6", [@"B" sharp], 0x00000291, @"c6/b#");
|
||||||
|
TestChord(@"C", @"69", [@"G" flat], 0x00004291, @"c69/gb");
|
||||||
|
TestChord(@"C", @"add9", @"", 0x00004091, @"cadd9");
|
||||||
|
TestChord([@"C" sharp], @"Maj7", @"", 0x00000891, @"c#maj");
|
||||||
|
TestChord([@"C" flat], @"Maj7add13", @"", 0x00200891, @"cbmajadd13");
|
||||||
|
TestChord(@"C", @"Maj9", @"", 0x00004891, @"cmaj9");
|
||||||
|
TestChord(@"C", @"Maj13", @"", 0x00224891, @"cmaj13");
|
||||||
|
TestChord(@"C", @"7", @"", 0x00000491, @"c7");
|
||||||
|
TestChord(@"C", @"9", @"", 0x00004491, @"C9");
|
||||||
|
TestChord(@"C", @"13", @"", 0x00224491, @"c13");
|
||||||
|
TestChord(@"Dm", @"", @"", 0x00000089, @"dm");
|
||||||
|
TestChord(@"Em", @"6", @"", 0x00000289, @"e-6");
|
||||||
|
TestChord(@"Fm", @"69", @"", 0x00004289, @"fm69");
|
||||||
|
TestChord(@"Gm", @"add9", @"", 0x00004089, @"gmadd9");
|
||||||
|
TestChord(@"Am", @"7", @"", 0x00000489, @"am7");
|
||||||
|
TestChord(@"Bm", @"7add11", @"", 0x00020489, @"bm7add11");
|
||||||
|
TestChord(@"Cm", @"9", @"", 0x00004489, @"cm9");
|
||||||
|
TestChord(@"Cm", @"11", @"", 0x00024489, @"cm11");
|
||||||
|
TestChord(@"Cm", @"13", @"", 0x00224489, @"cm13");
|
||||||
|
TestChord(@"Cm", @"Maj7", @"", 0x00000889, @"cmmaj");
|
||||||
|
TestChord(@"Cm", @"Maj9", @"", 0x00004889, @"cm9maj7");
|
||||||
|
TestChord(@"Cm", [@"7" flat5], @"", 0x00000449, @"cm7b5");
|
||||||
|
TestChord(@"Cm", [@"9" flat5], @"", 0x00004449, @"cm9b5");
|
||||||
|
TestChord(@"Cm", [@"11" flat5], @"", 0x00024449, @"cm11b5");
|
||||||
|
TestChord(@"C", @"dim", @"", 0x00000049, @"cdim");
|
||||||
|
TestChord(@"C", @"dim7", @"", 0x00000249, @"cdim7");
|
||||||
|
// TestChord(@"C", @"dim7Maj7",@"", 0x00000A49, @"cdim7maj");
|
||||||
|
TestChord(@"C", @"+", @"", 0x00000111, @"c+");
|
||||||
|
TestChord(@"C", @"sus", @"", 0x000000A1, @"csus");
|
||||||
|
TestChord(@"C", @"7sus", @"", 0x000004A1, @"csus7");
|
||||||
|
TestChord(@"C", @"9sus", @"", 0x000044A1, @"csus9");
|
||||||
|
TestChord(@"C", @"13sus", @"", 0x002244A1, @"csus13");
|
||||||
|
// TestChord(@"C", @"???", @"", ???, @"c7sus4-3");
|
||||||
|
TestChord(@"C", [@"Maj7" flat5], @"", 0x00000851, @"cmaj7b5");
|
||||||
|
TestChord(@"C", [@"Maj7" sharp5], @"", 0x00000911, @"cmaj7#5");
|
||||||
|
TestChord(@"C", [@"Maj7" sharp11], @"", 0x00040891, @"cmaj7#11");
|
||||||
|
TestChord(@"C", [@"Maj9" sharp11], @"", 0x00044891, @"cmaj9#11");
|
||||||
|
TestChord(@"C", [@"Maj13" sharp11], @"", 0x00244891, @"cmaj13#11");
|
||||||
|
TestChord(@"C", [@"7" flat5], @"", 0x00000451, @"c7b5");
|
||||||
|
TestChord(@"C", [@"9" flat5], @"", 0x00004451, @"c9-5");
|
||||||
|
TestChord(@"C", [@"7" sharp5], @"", 0x00000511, @"c7+5");
|
||||||
|
TestChord(@"C", [@"9" sharp5], @"", 0x00004511, @"c9#5");
|
||||||
|
TestChord(@"C", [@"7" flat9], @"", 0x00002491, @"c7b9");
|
||||||
|
TestChord(@"C", [@"7" sharp9], @"", 0x00008491, @"c7+9");
|
||||||
|
TestChord(@"C", [[@"7" flat5] flat9], @"", 0x00002451, @"c7b9b5");
|
||||||
|
TestChord(@"C", [[@"7" sharp5] sharp9], @"", 0x00008511, @"c7+9+5");
|
||||||
|
TestChord(@"C", [[@"7" sharp5] flat9], @"", 0x00002511, @"c7b9#5");
|
||||||
|
TestChord(@"C", [@"7" sharp11], @"", 0x00040491, @"c7#11");
|
||||||
|
TestChord(@"C", [@"9" sharp11], @"", 0x00044491, @"c9#11");
|
||||||
|
TestChord(@"C", [[@"7" flat9] sharp11], @"", 0x00042491, @"c7#11b9");
|
||||||
|
TestChord(@"C", [[@"7" sharp9] sharp11], @"", 0x00048491, @"c7+11+9");
|
||||||
|
TestChord(@"C", [@"13" flat5], @"", 0x00224451, @"c13-5");
|
||||||
|
TestChord(@"C", [@"13" flat9], @"", 0x00222491, @"c13-9");
|
||||||
|
TestChord(@"C", [@"13" sharp11], @"", 0x00244491, @"c13+11");
|
||||||
|
TestChord(@"C", [@"7sus" flat9], @"", 0x000024A1, @"csus7b9");
|
||||||
|
TestChord(@"C", [@"13sus" flat9], @"", 0x002224A1, @"csus13b9");
|
||||||
|
TestChord(@"C", [@"Maj7sus" flat5], @"", 0x00000861, @"cmaj7sus-5");
|
||||||
|
// TestChord(@"C", @"7susadd3", @"", 0x000004B1, @"csus7add3");
|
||||||
|
TestChord(@"C", [@"add9" flat13], @"", 0x00104091, @"cadd9b13");
|
||||||
|
TestChord(@"C", [[[@"" sharp5] flat9] sharp9], @"", 0x0000A111, @"c+b9+9");
|
||||||
|
TestChord(@"C", @"Maj7sus", @"", 0x000008A1, @"cmaj7sus");
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -15,5 +15,11 @@
|
||||||
- (NSString *)natural;
|
- (NSString *)natural;
|
||||||
- (NSString *)doubleSharp;
|
- (NSString *)doubleSharp;
|
||||||
- (NSString *)doubleFlat;
|
- (NSString *)doubleFlat;
|
||||||
|
- (NSString *)flat5;
|
||||||
|
- (NSString *)sharp5;
|
||||||
|
- (NSString *)flat9;
|
||||||
|
- (NSString *)sharp9;
|
||||||
|
- (NSString *)sharp11;
|
||||||
|
- (NSString *)flat13;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -37,4 +37,34 @@
|
||||||
return [self stringByAppendingString:[NSString stringWithUTF8String:kVL2FlatStr]];
|
return [self stringByAppendingString:[NSString stringWithUTF8String:kVL2FlatStr]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSString *)flat5
|
||||||
|
{
|
||||||
|
return [[self flat] stringByAppendingString:@"5"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)sharp5
|
||||||
|
{
|
||||||
|
return [[self sharp] stringByAppendingString:@"5"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)flat9
|
||||||
|
{
|
||||||
|
return [[self flat] stringByAppendingString:@"9"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)sharp9
|
||||||
|
{
|
||||||
|
return [[self sharp] stringByAppendingString:@"9"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)sharp11
|
||||||
|
{
|
||||||
|
return [[self sharp] stringByAppendingString:@"11"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)flat13
|
||||||
|
{
|
||||||
|
return [[self flat] stringByAppendingString:@"13"];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user