diff --git a/Sources/VLXMLDocument.h b/Sources/VLXMLDocument.h new file mode 100644 index 0000000..8a45051 --- /dev/null +++ b/Sources/VLXMLDocument.h @@ -0,0 +1,17 @@ +// +// VLXMLDocument.h +// Vocalese +// +// Created by Matthias Neeracher on 10/10/06. +// Copyright 2006 __MyCompanyName__. All rights reserved. +// + +#import +#import "VLDocument.h" + +@interface VLDocument (XML) + +- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError; +- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError; + +@end diff --git a/Sources/VLXMLDocument.mm b/Sources/VLXMLDocument.mm new file mode 100644 index 0000000..5d8eef5 --- /dev/null +++ b/Sources/VLXMLDocument.mm @@ -0,0 +1,287 @@ +// +// VLXMLDocument.mm +// Vocalese +// +// Created by Matthias Neeracher on 10/10/06. +// Copyright 2006 __MyCompanyName__. All rights reserved. +// + +#import "VLXMLDocument.h" + +@interface NSXMLNode (VLConvenience) + +- (id) nodeForXPath:(NSString *)path error:(NSError **)outError; +- (NSString *)stringForXPath:(NSString *)path error:(NSError **)outError; +- (int)intForXPath:(NSString *)path error:(NSError **)outError; + +@end + +@implementation NSXMLNode (VLConvenience) + +- (id) nodeForXPath:(NSString *)path error:(NSError **)outError +{ + return [[self nodesForXPath:path error:outError] objectAtIndex:0]; +} + +- (NSString *)stringForXPath:(NSString *)path error:(NSError **)outError +{ + return [[self nodeForXPath:path error:outError] stringValue]; +} + +- (int)intForXPath:(NSString *)path error:(NSError **)outError +{ + return [[[self nodeForXPath:path error:outError] stringValue] intValue]; +} + +@end + +@implementation VLDocument (XML) + +- (NSXMLDTD *)partwiseDTD +{ + NSString * dtdPath = [[NSBundle mainBundle] pathForResource:@"partwise" + ofType:@"dtd" + inDirectory:@"DTD"]; + NSXMLDTD * dtd = [[[NSXMLDTD alloc] + initWithContentsOfURL:[NSURL fileURLWithPath:dtdPath] + options:0 error:nil] + autorelease]; + [dtd setPublicID:@"-//Recordare//DTD MusicXML 1.1 Partwise//EN"]; + [dtd setSystemID:@"http://www.musicxml.org/dtds/partwise.dtd"]; + [dtd setName:@"score-partwise"]; + + return dtd; +} + +- (NSXMLElement *)scorePartWithID:(NSString *)id name:(NSString *)name +{ + NSXMLElement * part = [NSXMLNode elementWithName:@"score-part"]; + [part addAttribute: [NSXMLNode attributeWithName:@"id" + stringValue:id]]; + [part addChild: [NSXMLNode elementWithName:@"part-name" + stringValue:name]]; + + return part; +} + +- (NSXMLElement *)attributesWithProps:(VLProperties)props +{ + NSXMLElement * attr = [NSXMLNode elementWithName:@"attributes"]; + [attr addChild: [NSXMLNode elementWithName:@"divisions" + stringValue:[NSString stringWithFormat:@"%d", + props.fDivisions]]]; + + NSXMLElement * key = [NSXMLNode elementWithName:@"key"]; + [key addChild: [NSXMLNode elementWithName:@"fifths" + stringValue:[NSString stringWithFormat:@"%d", + props.fKey]]]; + [key addChild: [NSXMLNode elementWithName:@"mode" + stringValue: + props.fMode < 0 ? @"minor" : @"major"]]; + + NSXMLElement * time = [NSXMLNode elementWithName:@"time"]; + [time addChild: [NSXMLNode elementWithName:@"beats" + stringValue:[NSString stringWithFormat:@"%d", + props.fTime.fNum]]]; + [time addChild: [NSXMLNode elementWithName:@"beat-type" + stringValue:[NSString stringWithFormat:@"%d", + props.fTime.fDenom]]]; + + NSXMLElement * clef = [NSXMLNode elementWithName:@"clef"]; + [clef addChild: [NSXMLNode elementWithName:@"sign" + stringValue:@"G"]]; + [clef addChild: [NSXMLNode elementWithName:@"line" + stringValue:@"2"]]; + + + [attr addChild: key]; + [attr addChild: time]; + [attr addChild: clef]; + + return attr; +} + +const char * sSteps = "C DbD EbE F GbG AbA BbB "; + +- (NSXMLElement *)noteWithPitch:(int)pitch duration:(int)units useSharps:(BOOL)useSharps +{ + NSXMLElement * note = [NSXMLNode elementWithName:@"note"]; + if (pitch == VLNote::kNoPitch) { + [note addChild: [NSXMLNode elementWithName:@"rest"]]; + } else { + NSXMLElement * p = [NSXMLNode elementWithName:@"pitch"]; + int octave = pitch/12 - 1; + pitch = 2*(pitch % 12); + char step = sSteps[pitch]; + int alt = sSteps[pitch+1] == 'b'; + if (alt) + if (useSharps) { + --step; // Db -> C# + alt = 1; + } else { + alt = -1; + } + [p addChild: + [NSXMLNode elementWithName:@"step" + stringValue: [NSString stringWithFormat:@"%c", + step]]]; + if (alt) + [p addChild: + [NSXMLNode elementWithName:@"alter" + stringValue: [NSString stringWithFormat:@"%d", + alt]]]; + [p addChild: + [NSXMLNode elementWithName:@"octave" + stringValue: [NSString stringWithFormat:@"%d", + octave]]]; + [note addChild:p]; + } + [note addChild: [NSXMLNode elementWithName:@"duration" + stringValue: [NSString stringWithFormat:@"%d", + units]]]; + [note addChild: [NSXMLNode elementWithName:@"voice" + stringValue: @"1"]]; + + return note; +} + +- (void)addNotes:(VLNoteList *)notes toMeasure:(NSXMLElement *)meas +{ + VLFraction resolution(1, song->fProperties.front().fDivisions*4); + bool useSharps = song->fProperties.front().fKey > 0; + for (VLNoteList::const_iterator note = notes->begin(); + note != notes->end(); + ++note + ) { + VLFraction u = note->fDuration / resolution; + int units = (u.fNum+u.fDenom/2)/u.fDenom; + [meas addChild:[self noteWithPitch:note->fPitch duration:units useSharps:useSharps]]; + } +} + +- (void)addChords:(VLChordList *)chords toMeasure:(NSXMLElement *)meas +{ + VLFraction resolution(1, song->fProperties.front().fDivisions*4); + bool useSharps = song->fProperties.front().fKey > 0; + for (VLChordList::const_iterator chord = chords->begin(); + chord != chords->end(); + ++chord + ) { + VLFraction u = chord->fDuration / resolution; + int units = (u.fNum+u.fDenom/2)/u.fDenom; + NSXMLElement* ch = nil; + if (chord->fPitch == VLNote::kNoPitch) { + [meas addChild:[self noteWithPitch:chord->fPitch duration:units useSharps:useSharps]]; + continue; + } + if (chord->fRootPitch != VLNote::kNoPitch) { + [meas addChild:[self noteWithPitch:chord->fRootPitch + duration:units useSharps:useSharps]]; + ch = [NSXMLNode elementWithName:@"chord"]; + } + for (int step=0; step<32; ++step) + if ((1 << step) > chord->fSteps) { + break; + } else if (chord->fSteps & (1 << step)) { + NSXMLElement * note = + [self noteWithPitch:chord->fPitch+step + duration:units useSharps:useSharps]; + [note insertChild:ch atIndex:0]; + [meas addChild: note]; + ch = [NSXMLNode elementWithName:@"chord"]; + } + } +} + +- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError +{ + NSXMLElement * partList = [NSXMLNode elementWithName:@"part-list"]; + [partList addChild: [self scorePartWithID:@"HARM" name:@"Chords"]]; + [partList addChild: [self scorePartWithID:@"MELO" name:@"Melody"]]; + + NSXMLElement * chords = [NSXMLNode elementWithName:@"part"]; + [chords addAttribute: [NSXMLNode attributeWithName:@"id" + stringValue:@"HARM"]]; + + NSXMLElement * melody = [NSXMLNode elementWithName:@"part"]; + [melody addAttribute: [NSXMLNode attributeWithName:@"id" + stringValue:@"MELO"]]; + + for (int measure = 0; measure < song->CountMeasures(); ++measure) { + NSXMLElement * melMeas = [NSXMLNode elementWithName:@"measure"]; + [melMeas addAttribute: + [NSXMLNode attributeWithName:@"number" + stringValue:[NSString stringWithFormat:@"%d", + measure+1]]]; + if (!measure) + [melMeas addChild: + [self attributesWithProps:song->fProperties.front()]]; + + NSXMLElement * harMeas = [melMeas copy]; + + [self addNotes:&song->fMeasures[measure].fMelody toMeasure:melMeas]; + [self addChords:&song->fMeasures[measure].fChords toMeasure:harMeas]; + + [melody addChild:melMeas]; + [chords addChild:harMeas]; + } + + NSXMLElement * score = + [NSXMLNode + elementWithName:@"score-partwise" + children:[NSArray arrayWithObjects: + partList, chords, melody, nil] + attributes:[NSArray arrayWithObject: + [NSXMLNode attributeWithName:@"version" + stringValue:@"1.1"]]]; + NSXMLDocument * doc = [[[NSXMLDocument alloc] + initWithRootElement:score] + autorelease]; + [doc setVersion:@"1.0"]; + [doc setCharacterEncoding:@"UTF-8"]; + [doc setDTD:[self partwiseDTD]]; + [[doc DTD] setChildren: nil]; + + return [doc XMLDataWithOptions:NSXMLNodePrettyPrint|NSXMLNodeCompactEmptyElement]; +} + +- (BOOL)readPropsFromAttributes:(NSXMLElement*)attr error:(NSError **)outError +{ + VLProperties & prop = song->fProperties.front(); + + prop.fDivisions = [attr intForXPath:@"./divisions" error:outError]; + prop.fKey = [attr intForXPath:@"./key/fifths" error:outError]; + prop.fMode = [[attr stringForXPath:@"./key/mode" error:outError] + isEqual:@"minor"] ? -1 : 1; + prop.fTime.fNum = [attr intForXPath:@"./time/beats" error:outError]; + prop.fTime.fDenom = [attr intForXPath:@"./time/beat-type" error:outError]; + + return YES; +} + +- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError +{ + NSXMLDocument * doc = [[NSXMLDocument alloc] initWithData:data + options:0 + error:outError]; + // + // For now, in gross violation of MusicXML spirit, we're only reading + // our own input. + // + NSXMLElement * chords = [doc nodeForXPath:@".//part[@id=\"HARM\"]" + error:outError]; + NSXMLElement * melody = [doc nodeForXPath:@".//part[@id=\"MELO\"]" + error:outError]; + + if (!chords || !melody) + return NO; + if (![self readPropsFromAttributes: + [melody nodeForXPath:@".//attributes" error:outError] + error:outError] + ) + return NO; + + return YES; +} + +@end