diff options
Diffstat (limited to 'src/mxml2csv.go')
-rw-r--r-- | src/mxml2csv.go | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/src/mxml2csv.go b/src/mxml2csv.go new file mode 100644 index 0000000..94ea5d0 --- /dev/null +++ b/src/mxml2csv.go | |||
@@ -0,0 +1,188 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "encoding/csv" | ||
5 | "encoding/xml" | ||
6 | "flag" | ||
7 | "fmt" | ||
8 | "io/ioutil" | ||
9 | "math" | ||
10 | "os" | ||
11 | ) | ||
12 | |||
13 | const APP_VERSION = "0.1" | ||
14 | |||
15 | var versionFlag *bool = flag.Bool("v", false, "Print the version number.") | ||
16 | var inputFlag *string = flag.String("i", "", "The MusicXML file.") | ||
17 | var outputFlag *string = flag.String("o", "", "The CSV file to generate.") | ||
18 | var partFlag *int = flag.Int("p", 0, "The part to process.") | ||
19 | |||
20 | func init() { | ||
21 | flag.Parse() | ||
22 | |||
23 | if *versionFlag { | ||
24 | fmt.Println("Version:", APP_VERSION) | ||
25 | return | ||
26 | } | ||
27 | |||
28 | if *outputFlag == "" { | ||
29 | *outputFlag = *inputFlag + ".csv" | ||
30 | } | ||
31 | } | ||
32 | |||
33 | /***** MusicXML structure *****/ | ||
34 | |||
35 | type XMLFile struct { | ||
36 | Parts []Part `xml:"part"` | ||
37 | } | ||
38 | |||
39 | type Part struct { | ||
40 | Measures []Measure `xml:"measure"` | ||
41 | } | ||
42 | |||
43 | type Measure struct { | ||
44 | Attributes Attributes `xml:"attributes"` | ||
45 | Directions []Direction `xml:"direction"` | ||
46 | Notes []Note `xml:"note"` | ||
47 | } | ||
48 | |||
49 | type Attributes struct { | ||
50 | Divisions int `xml:"divisions"` | ||
51 | } | ||
52 | |||
53 | type Direction struct { | ||
54 | Sound Sound `xml:"sound"` | ||
55 | } | ||
56 | |||
57 | type Sound struct { | ||
58 | Tempo float64 `xml:"tempo,attr"` | ||
59 | } | ||
60 | |||
61 | type Note struct { | ||
62 | Pitch Pitch `xml:"pitch"` | ||
63 | Duration int `xml:"duration"` | ||
64 | } | ||
65 | |||
66 | type Pitch struct { | ||
67 | Step string `xml:"step"` | ||
68 | Alter int `xml:"alter"` | ||
69 | Octave int `xml:"octave"` | ||
70 | } | ||
71 | |||
72 | /***** Note conversion *****/ | ||
73 | const REFERENCE_PITCH float64 = 440 | ||
74 | |||
75 | var pitchOffset = map[string]int{ | ||
76 | "C": -9, | ||
77 | "D": -7, | ||
78 | "E": -5, | ||
79 | "F": -4, | ||
80 | "G": -2, | ||
81 | "A": +0, | ||
82 | "B": +2, | ||
83 | } | ||
84 | |||
85 | func calcFrequency(pitch Pitch) float64 { | ||
86 | if pitch.Step == "" { | ||
87 | return 0 | ||
88 | } | ||
89 | return REFERENCE_PITCH * float64(pitch.Octave-3) * math.Pow(2, float64(pitchOffset[pitch.Step]+pitch.Alter)/12) | ||
90 | } | ||
91 | |||
92 | /***** Division calculation *****/ | ||
93 | |||
94 | const DEFAULT_DIVISION = 24 | ||
95 | |||
96 | func readDivisions(part Part) int { | ||
97 | for _, measure := range part.Measures { | ||
98 | if measure.Attributes.Divisions != 0 { | ||
99 | return measure.Attributes.Divisions | ||
100 | } | ||
101 | } | ||
102 | return DEFAULT_DIVISION | ||
103 | } | ||
104 | |||
105 | /***** Tempo calculation *****/ | ||
106 | |||
107 | const DEFAULT_TEMPO = 120 | ||
108 | |||
109 | func readTempo(part Part) float64 { | ||
110 | for _, measure := range part.Measures { | ||
111 | for _, direction := range measure.Directions { | ||
112 | if direction.Sound.Tempo != 0 { | ||
113 | return direction.Sound.Tempo | ||
114 | } | ||
115 | } | ||
116 | } | ||
117 | return DEFAULT_TEMPO | ||
118 | } | ||
119 | |||
120 | func calcDuration(noteDuration int, divisions int, tempo float64) float64 { | ||
121 | return 60000 / tempo * float64(noteDuration) / float64(divisions) | ||
122 | } | ||
123 | |||
124 | /***** Main program *****/ | ||
125 | |||
126 | func main() { | ||
127 | |||
128 | // read and parse MusicXML file | ||
129 | |||
130 | inputFile, err := os.Open(*inputFlag) | ||
131 | if err != nil { | ||
132 | fmt.Println("Error opening MusicXML file:", err) | ||
133 | return | ||
134 | } | ||
135 | defer inputFile.Close() | ||
136 | |||
137 | data, err := ioutil.ReadAll(inputFile) | ||
138 | if err != nil { | ||
139 | fmt.Println("Error reading MusicXML file:", err) | ||
140 | return | ||
141 | } | ||
142 | |||
143 | var parsed XMLFile | ||
144 | xml.Unmarshal(data, &parsed) | ||
145 | |||
146 | fmt.Println("Parsed data:", parsed) | ||
147 | |||
148 | // create and write to CSV file | ||
149 | |||
150 | outputFile, err := os.Create(*outputFlag) | ||
151 | if err != nil { | ||
152 | fmt.Println("Error creating CSV file:", err) | ||
153 | return | ||
154 | } | ||
155 | defer outputFile.Close() | ||
156 | |||
157 | writer := csv.NewWriter(outputFile) | ||
158 | defer writer.Flush() | ||
159 | |||
160 | if *partFlag >= len(parsed.Parts) { | ||
161 | fmt.Println("Error: part", *partFlag, "does not exist.") | ||
162 | return | ||
163 | } | ||
164 | part := parsed.Parts[*partFlag] | ||
165 | |||
166 | tempo := readTempo(part) | ||
167 | fmt.Println("Tempo:", tempo) | ||
168 | |||
169 | divisions := readDivisions(part) | ||
170 | fmt.Println("Divisions:", divisions) | ||
171 | |||
172 | for _, measure := range part.Measures { | ||
173 | for _, note := range measure.Notes { | ||
174 | pitch := calcFrequency(note.Pitch) | ||
175 | duration := calcDuration(note.Duration, divisions, tempo) | ||
176 | line := []string{fmt.Sprint(pitch), fmt.Sprint(duration)} | ||
177 | fmt.Println(line) | ||
178 | err := writer.Write(line) | ||
179 | if err != nil { | ||
180 | fmt.Println("Error writing in CSV file:", err) | ||
181 | return | ||
182 | } | ||
183 | } | ||
184 | } | ||
185 | |||
186 | fmt.Println("Done.") | ||
187 | |||
188 | } | ||