From f47c7d725d3350346c641639034bcb45573acdae Mon Sep 17 00:00:00 2001 From: Matthias Neeracher Date: Sat, 27 Aug 2011 22:50:29 +0200 Subject: [PATCH] Factor our chord naming/parsing, unit test works --- Sources/VLPitchName.cpp | 173 ++++++++++++++++++++++++++- Sources/VLPitchName.h | 13 ++ Tests/TVLPitchNames/TVLPitchNames.mm | 89 +++++++++++++- Tests/TVLStringAccidentals.h | 6 + Tests/TVLStringAccidentals.mm | 30 +++++ 5 files changed, 308 insertions(+), 3 deletions(-) diff --git a/Sources/VLPitchName.cpp b/Sources/VLPitchName.cpp index b6c391d..1c06089 100644 --- a/Sources/VLPitchName.cpp +++ b/Sources/VLPitchName.cpp @@ -11,8 +11,11 @@ #include "VLPitchName.h" #include "VLModel.h" -const char * kVLSharpStr = "\xE2\x99\xAF"; -const char * kVLFlatStr = "\xE2\x99\xAD"; +#define SHARP "\xE2\x99\xAF" +#define FLAT "\xE2\x99\xAD" + +const char * kVLSharpStr = SHARP; +const char * kVLFlatStr = FLAT; const char * kVL2SharpStr = "\xF0\x9D\x84\xAA"; const char * kVL2FlatStr = "\xF0\x9D\x84\xAB"; const char * kVLNaturalStr = "\xE2\x99\xAE"; @@ -120,3 +123,169 @@ int8_t VLParsePitch(std::string & str, size_t at, uint16_t * accidental) 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; +} diff --git a/Sources/VLPitchName.h b/Sources/VLPitchName.h index 3557009..565534a 100644 --- a/Sources/VLPitchName.h +++ b/Sources/VLPitchName.h @@ -29,3 +29,16 @@ std::string VLPitchName(int8_t pitch, uint16_t accidental); // enum { kPitchError = -1 }; 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); \ No newline at end of file diff --git a/Tests/TVLPitchNames/TVLPitchNames.mm b/Tests/TVLPitchNames/TVLPitchNames.mm index 4eba0e6..af2a8aa 100644 --- a/Tests/TVLPitchNames/TVLPitchNames.mm +++ b/Tests/TVLPitchNames/TVLPitchNames.mm @@ -99,7 +99,7 @@ } #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((uint16_t)accidental, acc, @"VLParsePitch(%@, %lu) [accidental]", str, at); \ STAssertEquals((size_t)at, cppStr.size(), @"VLParsePitch(%@, %lu) [cleanup]", str, at); \ @@ -136,4 +136,91 @@ 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 diff --git a/Tests/TVLStringAccidentals.h b/Tests/TVLStringAccidentals.h index ca19941..11c3fb6 100644 --- a/Tests/TVLStringAccidentals.h +++ b/Tests/TVLStringAccidentals.h @@ -15,5 +15,11 @@ - (NSString *)natural; - (NSString *)doubleSharp; - (NSString *)doubleFlat; +- (NSString *)flat5; +- (NSString *)sharp5; +- (NSString *)flat9; +- (NSString *)sharp9; +- (NSString *)sharp11; +- (NSString *)flat13; @end diff --git a/Tests/TVLStringAccidentals.mm b/Tests/TVLStringAccidentals.mm index cc3633f..d099649 100644 --- a/Tests/TVLStringAccidentals.mm +++ b/Tests/TVLStringAccidentals.mm @@ -37,4 +37,34 @@ 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