diff --git a/Resources/Templates/default.lyt b/Resources/Templates/default.lyt index bc32e0d..b645f7c 100644 --- a/Resources/Templates/default.lyt +++ b/Resources/Templates/default.lyt @@ -1,11 +1,11 @@ -\version "1.8.0" +\version "2.8.0" \header { title = "<{TITLE}>" poet = "<{POET}>" composer= "<{COMPOSER}>" arranger = "<{ARRANGER}>" - tagline = #(string-append "Vocalese <{VLVERSION}> / Lilypond" (lilypond-version)) + tagline = #(string-append "Created with VocalEasel <{VLVERSION}> / Lilypond " (lilypond-version)) } \paper { diff --git a/Sources/VLDocument.h b/Sources/VLDocument.h index 18584e6..62049d4 100644 --- a/Sources/VLDocument.h +++ b/Sources/VLDocument.h @@ -27,6 +27,10 @@ VLSong * song; VLEditable *editTarget; NSString * lilypondTemplate; + NSString * songTitle; + NSString * songLyricist; + NSString * songComposer; + NSString * songArranger; } - (VLSong *) song; diff --git a/Sources/VLDocument.mm b/Sources/VLDocument.mm index ef2314a..1a55fa2 100644 --- a/Sources/VLDocument.mm +++ b/Sources/VLDocument.mm @@ -53,6 +53,10 @@ song = new VLSong; editTarget = nil; lilypondTemplate = @"default"; + songTitle = @""; + songLyricist = @""; + songComposer = @""; + songArranger = @""; } return self; } @@ -133,20 +137,30 @@ - (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError { - if ([typeName isEqual:@"Song"]) + if ([typeName isEqual:@"VLNativeType"]) { return [self XMLDataWithError:outError]; - else if ([typeName isEqual:@"Lilypond"]) + } else if ([typeName isEqual:@"VLLilypondType"]) { return [self lilypondDataWithError:outError]; - else + } else { + if (outError) + *outError = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSPersistentStoreInvalidTypeError + userInfo:nil]; return nil; + } } - (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError { - if ([typeName isEqual:@"Song"]) + if ([typeName isEqual:@"VLNativeType"]) { return [self readFromXMLData:data error:outError]; - else + } else { + if (outError) + *outError = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSPersistentStoreInvalidTypeError + userInfo:nil]; return NO; + } } @end diff --git a/Sources/VLLilypondDocument.mm b/Sources/VLLilypondDocument.mm index a9212f1..bb1314e 100644 --- a/Sources/VLLilypondDocument.mm +++ b/Sources/VLLilypondDocument.mm @@ -19,6 +19,8 @@ - (void) substituteMacro:(NSString *)m withValue:(NSString *)value { + if ([value isEqual:@""]) + return; NSString * macro = [NSString stringWithFormat:@"<{%@}>", m]; NSRange range = [value rangeOfCharacterFromSet: @@ -82,19 +84,49 @@ @implementation VLDocument (Lilypond) +const int kMajorOffset = 6; +const int kMinorOffset = 9; + +const char * sKeyNames[] = { + "ges", "des", "as", "es", "bes", "f", + "c", "g", "d", "a", "e", "b", "fis", "cis", "gis" +}; + - (NSData *)lilypondDataWithError:(NSError **)outError { - NSBundle * bndl = [NSBundle mainBundle]; - NSString * tmpl = + const VLProperties & prop = song->fProperties.front(); + NSBundle * bndl = [NSBundle mainBundle]; + NSString * tmpl = [bndl pathForResource:lilypondTemplate ofType:@"lyt" inDirectory:@"Templates"]; - NSStringEncoding enc = NSUTF8StringEncoding; - NSError * err; - NSMutableString * ly = - [[NSString stringWithContentsOfFile:tmpl encoding:enc error:&err] - mutableCopy]; + NSStringEncoding enc = NSUTF8StringEncoding; + NSMutableString * ly = + [NSMutableString stringWithContentsOfFile:tmpl encoding:enc error:outError]; + [ly substituteMacro:@"TITLE" withValue:songTitle]; + [ly substituteMacro:@"POET" withValue:songLyricist]; + [ly substituteMacro:@"COMPOSER" withValue:songComposer]; + [ly substituteMacro:@"ARRANGER" withValue:songArranger]; [ly substituteMacro:@"VLVERSION" withValue: [bndl objectForInfoDictionaryKey:@"CFBundleVersion"]]; + [ly substituteMacro:@"PAPERSIZE" withValue:@"letter"]; + [ly substituteMacro:@"FORMATTING" withValue:@"ragged-last-bottom = ##f"]; + std::string lys; + song->LilypondChords(lys); + [ly substituteMacro:@"VLVERSION" withValue: + [bndl objectForInfoDictionaryKey:@"CFBundleVersion"]]; + [ly substituteMacro:@"CHORDS" withValue: + [NSString stringWithUTF8String:lys.c_str()]]; + [ly substituteMacro:@"TIME" withValue: + [NSString stringWithFormat:@"%d/%d", + prop.fTime.fNum, prop.fTime.fDenom]]; + [ly substituteMacro:@"KEY" withValue: prop.fMode > 0 + ? [NSString stringWithFormat:@"%s \\major", + sKeyNames[prop.fKey+kMajorOffset]] + : [NSString stringWithFormat:@"%s \\minor", + sKeyNames[prop.fKey+kMinorOffset]]]; + song->LilypondNotes(lys); + [ly substituteMacro:@"NOTES" withValue: + [NSString stringWithUTF8String:lys.c_str()]]; [ly purgeMacros]; return [ly dataUsingEncoding:enc]; } diff --git a/Sources/VLModel.cpp b/Sources/VLModel.cpp index d6514cd..47c7262 100644 --- a/Sources/VLModel.cpp +++ b/Sources/VLModel.cpp @@ -135,9 +135,42 @@ void VLNote::Name(std::string & name, bool useSharps) const name = PitchName(fPitch, useSharps); } -void VLNote::LilypondName(std::string & name, bool useSharps) const +void VLNote::LilypondName(std::string & name, VLFraction at, const VLProperties & prop) const { - name = LilypondPitchName(fPitch, useSharps); + std::string n = LilypondPitchName(fPitch, prop.fKey >= 0); + if (fPitch != kNoPitch) { + for (int ticks = (fPitch-kMiddleC+kOctave)/kOctave; ticks>0; --ticks) + n += '\''; + for (int commas = (kMiddleC-kOctave-fPitch)/kOctave; commas>0; --commas) + n += ','; + } + + std::vector durations; + VLFraction prevPart(0); + for (VLFraction dur = fDuration; dur.fNum; ) { + char duration[32]; + VLFraction part, visual; + bool triplet; + prop.PartialNote(at, dur, &part); + prop.VisualNote(at, part, &visual, &triplet); + if (!triplet && fPitch != kNoPitch && part == dur && 2*visual == prevPart) { + durations.pop_back(); + sprintf(duration, "%s%d.", n.c_str(), visual.fDenom/2); + } else if (triplet) { + sprintf(duration, "\times 2/3 { %s%d }", n.c_str(), visual.fDenom); + } else { + sprintf(duration, "%s%d", n.c_str(), visual.fDenom); + } + durations.push_back(duration); + prevPart = part; + at += part; + dur -= part; + } + for (size_t i=0; i kDim7th; --step) if (unaltered & (1 << step)) { std::string sn = kLilypondStepNames[step]; - if (ext.size() && sn.size()) + if (ext.size() && !isalpha(ext[ext.size()-1]) && sn.size()) ext += '.'; ext += sn; break; @@ -443,6 +478,32 @@ void VLProperties::PartialNote(VLFraction at, VLFraction totalDuration, *noteDuration *= VLFraction(3,4); // avoid frivolous triplets } +void VLProperties::VisualNote(VLFraction at, VLFraction actualDur, + VLFraction *visualDur, bool * triplet) const +{ + bool swing= !(fDivisions % 3); // In swing mode? + VLFraction swung(3, fDivisions*8, true); // Which notes to swing + VLFraction swingGrid(2*swung); // Alignment of swing notes + + if (*triplet = !(actualDur.fDenom % 3)) { + if (swing) { // Swing 8ths / 16ths are written as straight 8ths + if (actualDur == 4*swung/3 && (at % swingGrid) == 0) { + *visualDur = swung; + *triplet = false; + } else if (actualDur == 2*swung/3 && ((at+actualDur) % swingGrid) == 0) { + *visualDur = swung; + *triplet = false; + } else { + *visualDur = 4*actualDur/3; + } + } else { + *visualDur = 4*actualDur/3; + } + } else { + *visualDur = actualDur; + } +} + VLMeasure::VLMeasure() : fProperties(0) { @@ -691,3 +752,51 @@ void VLSong::Transpose(int semi) } } } + +void VLSong::LilypondNotes(std::string & notes) const +{ + notes = ""; + for (size_t measure=0; measureLilypondName(note, at, *fMeasures[measure].fProperties); + at += i->fDuration; + notes += note+" "; + } + notes += '|'; + if (!(measure % 4)) { + char measNo[8]; + sprintf(measNo, " %% %d", measure+1); + notes += measNo; + } + if (measure < fMeasures.size()-1) + notes += '\n'; + } +} + +void VLSong::LilypondChords(std::string & chords) const +{ + chords = ""; + for (size_t measure=0; measurefKey>=0; + VLChordList::const_iterator i = fMeasures[measure].fChords.begin(); + VLChordList::const_iterator e = fMeasures[measure].fChords.end(); + + for (; i!=e; ++i) { + std::string chord; + i->LilypondName(chord, useSharps); + chords += chord+" "; + } + if (!(measure % 4)) { + char measNo[8]; + sprintf(measNo, " %% %d", measure+1); + chords += measNo; + } + if (measure < fMeasures.size()-1) + chords += '\n'; + } +} diff --git a/Sources/VLModel.h b/Sources/VLModel.h index 5aa5ac8..1b4b541 100644 --- a/Sources/VLModel.h +++ b/Sources/VLModel.h @@ -91,12 +91,15 @@ inline bool operator>=(VLFraction one, VLFraction other) return one.fNum*other.fDenom >= other.fNum*one.fDenom; } +class VLProperties; + struct VLNote { VLFraction fDuration; int8_t fPitch; // Semitones enum { kNoPitch = -128, - kMiddleC = 60 + kMiddleC = 60, + kOctave = 12 }; // // We only allow ties BETWEEN measures. Within measures, we just store @@ -115,7 +118,7 @@ struct VLNote { VLNote(std::string name); void Name(std::string & name, bool useSharps = false) const; - void LilypondName(std::string & name, bool useSharps = false) const; + void LilypondName(std::string & name, VLFraction at, const VLProperties & prop) const; }; struct VLRest : VLNote { @@ -189,6 +192,10 @@ struct VLProperties { // Subdivide a note and adjust for swing // void PartialNote(VLFraction at, VLFraction totalDuration, VLFraction * noteDuration) const; + // + // Determine visual representation of note head + // + void VisualNote(VLFraction at, VLFraction actualDur, VLFraction *visualDur, bool * triplet) const; }; struct VLSyllable { @@ -222,6 +229,8 @@ struct VLSong { void Transpose(int semitones); size_t CountMeasures() const { return fMeasures.size(); } + void LilypondNotes(std::string & notes) const; + void LilypondChords(std::string & chords) const; }; // Local Variables: diff --git a/Sources/VLSheetViewNotes.mm b/Sources/VLSheetViewNotes.mm index 15ea6ad..a92ad0d 100644 --- a/Sources/VLSheetViewNotes.mm +++ b/Sources/VLSheetViewNotes.mm @@ -270,28 +270,11 @@ int pitch = note->fPitch; while (dur > 0) { VLFraction partialDur; // Actual value of note drawn + VLFraction noteDur; // Visual value of note + bool triplet; measure.fProperties->PartialNote(at, dur, &partialDur); + measure.fProperties->VisualNote(at, partialDur, ¬eDur, &triplet); - BOOL triplet = !(partialDur.fDenom % 3); - VLFraction noteDur(1); // Visual value of note - - if (triplet) { - if (swing) { // Swing 8ths / 16ths are written as straight 8ths - if (partialDur == 4*swung/3 && (at % swingGrid) == 0) { - noteDur = swung; - triplet = NO; - } else if (partialDur == 2*swung/3 && ((at+partialDur) % swingGrid) == 0) { - noteDur = swung; - triplet = NO; - } else { - noteDur = 4*partialDur/3; - } - } else { - noteDur = 4*partialDur/3; - } - } else { - noteDur = partialDur; - } if (pitch != VLNote::kNoPitch) { VLMusicElement accidental; NSPoint pos = diff --git a/Tests/TVLLilypond.mm b/Tests/TVLLilypond.mm index f3bafbc..1fe6714 100644 --- a/Tests/TVLLilypond.mm +++ b/Tests/TVLLilypond.mm @@ -16,11 +16,11 @@ int main(int, char *const argv[]) VLDocument * doc = [[VLDocument alloc] init]; NSString * file = [NSString stringWithUTF8String:argv[1]]; NSError * err; - [doc readFromURL:[NSURL fileURLWithPath:file] ofType:@"Song" error:&err]; + [doc readFromURL:[NSURL fileURLWithPath:file] ofType:@"VLNativeType" error:&err]; [doc writeToURL:[NSURL fileURLWithPath: [[file stringByDeletingPathExtension] stringByAppendingPathExtension:@"ly"]] - ofType:@"Lilypond" error:&err]; + ofType:@"VLLilypondType" error:&err]; [pool release]; exit(0); diff --git a/Vocalese.xcodeproj/project.pbxproj b/Vocalese.xcodeproj/project.pbxproj index 9a484bf..ed19f05 100644 --- a/Vocalese.xcodeproj/project.pbxproj +++ b/Vocalese.xcodeproj/project.pbxproj @@ -60,7 +60,7 @@ 2A37F4C5FDCFA73011CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 32DBCF750370BD2300C91783 /* Vocalese_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Vocalese_Prefix.pch; path = Sources/Vocalese_Prefix.pch; sourceTree = ""; }; 8D15AC360486D014006FF6A4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = Info.plist; path = Resources/Info.plist; sourceTree = ""; }; - 8D15AC370486D014006FF6A4 /* Vocalese.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vocalese.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8D15AC370486D014006FF6A4 /* VocalEasel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VocalEasel.app; sourceTree = BUILT_PRODUCTS_DIR; }; 952CBB98095FD19D00434E43 /* TVLSoundOut */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = TVLSoundOut; sourceTree = BUILT_PRODUCTS_DIR; }; 952CBB9A095FD1CA00434E43 /* VLSoundOut.cpp */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.cpp.cpp; name = VLSoundOut.cpp; path = Sources/VLSoundOut.cpp; sourceTree = ""; }; 952CBB9B095FD1CA00434E43 /* VLSoundOut.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = VLSoundOut.h; path = Sources/VLSoundOut.h; sourceTree = ""; }; @@ -163,7 +163,7 @@ 19C28FB0FE9D524F11CA2CBB /* Products */ = { isa = PBXGroup; children = ( - 8D15AC370486D014006FF6A4 /* Vocalese.app */, + 8D15AC370486D014006FF6A4 /* VocalEasel.app */, 955E595C0957C0FC0045FDA5 /* TVLChord */, 952CBB98095FD19D00434E43 /* TVLSoundOut */, 959408A0096922CA007CCCF8 /* TVLEdit */, @@ -275,7 +275,7 @@ name = Vocalese; productInstallPath = "$(HOME)/Applications"; productName = Vocalese; - productReference = 8D15AC370486D014006FF6A4 /* Vocalese.app */; + productReference = 8D15AC370486D014006FF6A4 /* VocalEasel.app */; productType = "com.apple.product-type.application"; }; 952CBB97095FD19D00434E43 /* TVLSoundOut */ = { @@ -512,7 +512,7 @@ INFOPLIST_FILE = Resources/Info.plist; INSTALL_PATH = "$(HOME)/Applications"; PREBINDING = NO; - PRODUCT_NAME = Vocalese; + PRODUCT_NAME = VocalEasel; WRAPPER_EXTENSION = app; ZERO_LINK = YES; }; @@ -531,7 +531,7 @@ INFOPLIST_FILE = Resources/Info.plist; INSTALL_PATH = "$(HOME)/Applications"; PREBINDING = NO; - PRODUCT_NAME = Vocalese; + PRODUCT_NAME = VocalEasel; WRAPPER_EXTENSION = app; ZERO_LINK = NO; }; @@ -547,7 +547,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(HOME)/Applications"; - PRODUCT_NAME = Vocalese; + PRODUCT_NAME = VocalEasel; WRAPPER_EXTENSION = app; }; name = Default;