package main import ( "encoding/csv" "encoding/xml" "flag" "fmt" "io/ioutil" "math" "os" ) const APP_VERSION = "0.1" var versionFlag *bool = flag.Bool("v", false, "Print the version number.") var inputFlag *string = flag.String("i", "", "The MusicXML file.") var outputFlag *string = flag.String("o", "", "The CSV file to generate.") var partFlag *int = flag.Int("p", 0, "The part to process.") func init() { flag.Parse() if *outputFlag == "" { *outputFlag = *inputFlag + ".csv" } } /***** MusicXML structure *****/ type XMLFile struct { Parts []Part `xml:"part"` } type Part struct { Measures []Measure `xml:"measure"` } type Measure struct { Attributes Attributes `xml:"attributes"` Directions []Direction `xml:"direction"` Notes []Note `xml:"note"` } type Attributes struct { Divisions int `xml:"divisions"` } type Direction struct { Sound Sound `xml:"sound"` } type Sound struct { Tempo float64 `xml:"tempo,attr"` } type Note struct { Pitch Pitch `xml:"pitch"` Duration int `xml:"duration"` } type Pitch struct { Step string `xml:"step"` Alter int `xml:"alter"` Octave int `xml:"octave"` } /***** Note conversion *****/ const REFERENCE_PITCH float64 = 440 var pitchOffset = map[string]int{ "C": -9, "D": -7, "E": -5, "F": -4, "G": -2, "A": +0, "B": +2, } func calcFrequency(pitch Pitch) float64 { if pitch.Step == "" { return 0 } return REFERENCE_PITCH * float64(pitch.Octave-3) * math.Pow(2, float64(pitchOffset[pitch.Step]+pitch.Alter)/12) } /***** Division calculation *****/ const DEFAULT_DIVISION = 24 func readDivisions(part Part) int { for _, measure := range part.Measures { if measure.Attributes.Divisions != 0 { return measure.Attributes.Divisions } } return DEFAULT_DIVISION } /***** Tempo calculation *****/ const DEFAULT_TEMPO = 120 func readTempo(part Part) float64 { for _, measure := range part.Measures { for _, direction := range measure.Directions { if direction.Sound.Tempo != 0 { return direction.Sound.Tempo } } } return DEFAULT_TEMPO } func calcDuration(noteDuration int, divisions int, tempo float64) float64 { return 60000 / tempo * float64(noteDuration) / float64(divisions) } /***** Main program *****/ func main() { if *versionFlag { fmt.Println("Version:", APP_VERSION) return } // read and parse MusicXML file inputFile, err := os.Open(*inputFlag) if err != nil { fmt.Println("Error opening MusicXML file:", err) return } defer inputFile.Close() data, err := ioutil.ReadAll(inputFile) if err != nil { fmt.Println("Error reading MusicXML file:", err) return } var parsed XMLFile xml.Unmarshal(data, &parsed) fmt.Println("Parsed data:", parsed) // create and write to CSV file outputFile, err := os.Create(*outputFlag) if err != nil { fmt.Println("Error creating CSV file:", err) return } defer outputFile.Close() writer := csv.NewWriter(outputFile) defer writer.Flush() if *partFlag >= len(parsed.Parts) { fmt.Println("Error: part", *partFlag, "does not exist.") return } part := parsed.Parts[*partFlag] tempo := readTempo(part) fmt.Println("Tempo:", tempo) divisions := readDivisions(part) fmt.Println("Divisions:", divisions) for _, measure := range part.Measures { for _, note := range measure.Notes { pitch := calcFrequency(note.Pitch) duration := calcDuration(note.Duration, divisions, tempo) line := []string{fmt.Sprint(pitch), fmt.Sprint(duration)} fmt.Println(line) err := writer.Write(line) if err != nil { fmt.Println("Error writing in CSV file:", err) return } } } fmt.Println("Done.") }