view clojure/com/aurellem/run/music.clj @ 426:c03f28aa98d9

completed basic midi parser using midicsv
author Robert McIntyre <rlm@mit.edu>
date Mon, 23 Apr 2012 07:02:39 -0500
parents df4e03672b05
children fbccf46cf34d
line wrap: on
line source
1 (ns com.aurellem.run.music
2 (:use (com.aurellem.gb saves gb-driver util constants
3 items vbm characters money
4 rlm-assembly))
5 (:use (com.aurellem.run util title save-corruption
6 bootstrap-0 bootstrap-1))
7 (:require clojure.string)
8 (:import [com.aurellem.gb.gb_driver SaveState])
9 (:import java.io.File))
12 (def music-base new-kernel)
17 (defn store [n address]
18 (flatten
19 [0xF5
20 0xE5
22 0x3E
23 n
25 0x21
26 (reverse (disect-bytes-2 address))
28 0x77
30 0xE1
31 0xF1]))
33 (defn infinite-loop []
34 [0x18 0xFE])
38 (def divider-register 0xFF04)
41 (defrecord Bit-Note [frequency volume duration duty])
43 (defn clear-music-registers []
44 (flatten
45 [(store (Integer/parseInt "00000000" 2) 0xFF10)
46 (store (Integer/parseInt "00000000" 2) 0xFF11)
47 (store (Integer/parseInt "00000000" 2) 0xFF12)
48 (store (Integer/parseInt "00000000" 2) 0xFF13)
49 (store (Integer/parseInt "00000000" 2) 0xFF14)
51 (store (Integer/parseInt "00000000" 2) 0xFF16) ;; pattern duty 000000
52 (store (Integer/parseInt "00000000" 2) 0xFF17) ;; volume 0000
53 (store (Integer/parseInt "00000000" 2) 0xFF18) ;; frequency-low
54 (store (Integer/parseInt "00000000" 2) 0xFF19) ;; 00000 frequency-high
56 (store (Integer/parseInt "00000000" 2) 0xFF1A)
57 (store (Integer/parseInt "00000000" 2) 0xFF1B)
58 (store (Integer/parseInt "00000000" 2) 0xFF1C)
59 (store (Integer/parseInt "00000000" 2) 0xFF1D)
60 (store (Integer/parseInt "00000000" 2) 0xFF1E)
62 (store (Integer/parseInt "00000000" 2) 0xFF20)
63 (store (Integer/parseInt "00000000" 2) 0xFF21)
64 (store (Integer/parseInt "00000000" 2) 0xFF22)
65 (store (Integer/parseInt "00000000" 2) 0xFF23)]))
68 ;; mini-midi syntax
70 ;; codes
71 ;; note-code == 0x00
72 ;; change-duty-code = 0x01
73 ;; silence-code = 0x02
75 ;; silence format
76 ;; 2 bytes
77 ;; [silence-code (0x02)]
78 ;; [duration-8-bits]
80 ;; note data format
81 ;; 4 bytes
82 ;; [note-code (0x00)]
83 ;; [volume-4-bits 0 frequency-high-3-bits]
84 ;; [frequengy-low-8-bits]
85 ;; [duration-8-bits]
87 ;; change-duty-format
88 ;; 2 bytes
89 ;; [change-duty-code (0x01)]
90 ;; [new-duty]
92 (def note-code 0x00)
93 (def change-duty-code 0x01)
94 (def silence-code 0x02)
97 (defn do-message
98 "Read the message which starts at the current value of HL and do
99 what it says. Duration is left in A, and HL is advanced
100 appropraitely."
101 []
102 (let [switch
103 [0x2A ;; load message code into A, increment HL
105 ;; switch on message
106 0xFE
107 note-code
109 0x20
110 :note-length]
112 play-note
113 [0x2A ;; load volume/frequency-high info
114 0xF5 ;; push A
115 0xE6
116 (Integer/parseInt "11110000" 2) ;; volume mask
117 0xE0
118 0x17 ;; set volume
119 0xF1 ;; pop A
120 0xE6
121 (Integer/parseInt "00000111" 2) ;; frequency-high mask
122 0xE0
123 0x19 ;; set frequency-high
125 0x2A ;; load frequency low-bits
126 0xE0
127 0x18 ;; set frequency-low-bits
129 0x2A]] ;; load duration
130 (replace
131 {:note-length (count play-note)}
132 (concat switch play-note))))
134 (defn play-note
135 "Play the note referenced by HL in the appropiate channel.
136 Leaves desired-duration in A."
137 []
138 [0x2A ;; load volume/frequency-high info
139 0xF5 ;; push A
140 0xE6
141 (Integer/parseInt "11110000" 2) ;; volume mask
142 0xE0
143 0x17 ;; set volume
144 0xF1 ;; pop A
145 0xE6
146 (Integer/parseInt "00000111" 2) ;; frequency-high mask
147 0xE0
148 0x19 ;; set frequency-high
150 0x2A ;; load frequency low-bits
151 0xE0
152 0x18 ;; set frequency-low-bits
154 0x2A ;; load duration
155 ])
157 (defn music-step []
158 (flatten
159 [
160 0xF5 ;; push A
161 0xF0
162 0x05 ;; load current ticks
163 0xB8 ;; B holds previous sub-ticks, subtract it from A
164 ;; if A-B caused a carry, then (B > A) is true, and
165 ;; A = current-sub-tics, B = previous-sub-ticks, so
166 ;; current-sub-ticks < previous-sub-ticks, which means that the
167 ;; timer counter HAS overflowed.
168 0x30 ;; increment C only if last result caused carry
169 0x01
170 0x0C
172 0x47 ;; update sub-ticks (A->B)
174 0xF1 ;; pop AF, now A contains desired-ticks
176 0xB9 ;; compare with current ticks
178 ;; if desired-ticks = current ticks
179 ;; go to next note ; set current set ticks to 0.
181 0x20
182 (+ (count (do-message)) 2)
184 (do-message)
186 0x0E
187 0x00])) ;; 0->C (current-ticks)
189 (defn music-kernel []
190 (flatten
191 [(clear-music-registers)
193 0x21
194 0x00
195 0xD0 ;; set HL to 0xD000 == music-start
196 0x0E
197 0x00 ;; 0->C
198 0x06
199 0x00 ;; 0->B
201 0x3E
202 0x01
203 0xE0
204 0x06 ;; set TMA to 0
206 0x3E
207 (Integer/parseInt "00000110" 2)
208 0xE0
209 0x07 ;; set TAC to 65536 Hz and activate timer
212 0xAF ;; initialiaze A to zero
215 (music-step)
216 0x18
217 (->signed-8-bit (+ (- (count (music-step)))
218 -2))]))
220 (defn frequency-code->frequency
221 [code]
222 (assert (<= 0 code 2047))
223 (/ 131072 (- 2048 code)))
225 (defn clamp [x low high]
226 (cond (> x high) high
227 (< x low) low
228 true x))
230 (defn frequency->frequency-code
231 [frequency]
232 (clamp
233 (Math/round
234 (float
235 (/ (- (* 2048 frequency) 131072) frequency)))
236 0x00 2048))
238 (defn note-codes [frequency volume duration]
239 (assert (<= 0 volume 0xF))
240 (assert (<= 0 duration 0xFF))
241 (let [frequency-code
242 (frequency->frequency-code frequency)
243 volume&high-frequency
244 (+ (bit-shift-left volume 4)
245 (bit-shift-right frequency-code 8))
246 low-frequency
247 (bit-and 0xFF frequency-code)]
248 [note-code
249 volume&high-frequency
250 low-frequency
251 duration]))
253 (def C4 (partial note-codes 261.63))
254 (def D4 (partial note-codes 293.66))
255 (def E4 (partial note-codes 329.63))
256 (def F4 (partial note-codes 349.23))
257 (def G4 (partial note-codes 392))
258 (def A4 (partial note-codes 440))
259 (def B4 (partial note-codes 493.88))
260 (def C5 (partial note-codes 523.3))
262 (def scale
263 (flatten
264 [(C4 0xF 0x40)
265 (D4 0xF 0x40)
266 (E4 0xF 0x40)
267 (F4 0xF 0x40)
268 (G4 0xF 0x40)
269 (A4 0xF 0x40)
270 (B4 0xF 0x40)
271 (C5 0xF 0x40)]))
273 (defn play-music [music-bytes]
274 (let [program-target 0xC000
275 music-target 0xD000]
276 (-> (set-memory-range (second (music-base))
277 program-target (music-kernel))
278 (set-memory-range music-target music-bytes)
279 (PC! program-target))))
282 (defn test-note [music-bytes]
283 (-> (set-memory-range (second (music-base))
284 0xC000 (concat (clear-music-registers)
285 (play-note)
286 (infinite-loop)))
287 (set-memory-range 0xD000 music-bytes)
288 (PC! 0xC000)
289 (HL! 0xD000)
290 ))
293 (defn run-program
294 ([program]
295 (let [target 0xC000]
296 (-> (set-memory-range (second (music-base))
297 target program)
298 (PC! target)))))
300 (defn test-timer []
301 (flatten
302 [0x3E
303 0x01
304 0xE0
305 0x06 ;; set TMA to 0
307 0x3E
308 (Integer/parseInt "00000100" 2)
309 0xE0
310 0x07 ;; set TAC to 16384 Hz and activate timer
312 (repeat
313 500
314 [0xF0
315 0x05])]))
317 (def third-kind
318 (File. "/home/r/proj/midi/third-kind.mid"))
320 (defn raw-midi-text [#^File midi-file]
321 (:out
322 (clojure.java.shell/sh
323 "midicsv"
324 (.getCanonicalPath midi-file)
325 "-")))
327 (def command-line #"^(\d+), (\d+), ([^,]+)(.*)$")
329 (defmulti parse-command :command)
331 (defn discard-args [command] (dissoc command :args))
333 (defmethod parse-command :Start_track
334 [command] (discard-args command))
336 (defmethod parse-command :End_track
337 [command] (discard-args command))
339 (defmethod parse-command :default
340 [command] command)
342 (defn parse-number-list
343 [number-list-str]
344 (map #(Integer/parseInt %)
345 (clojure.string/split number-list-str #", ")))
347 (defmethod parse-command :Tempo
348 [command]
349 (update-in command [:args] #(Integer/parseInt %)))
351 (defn parse-midi-note-list
352 [midi-note-list-str]
353 (let [[channel note velocity]
354 (parse-number-list midi-note-list-str)]
355 {:channel channel :note note :velocity velocity}))
358 (defmethod parse-command :Note_on_c
359 [command]
360 (update-in command [:args] parse-midi-note-list))
362 (defmethod parse-command :Note_off_c
363 [command]
364 (update-in command [:args] parse-midi-note-list))
366 (defmethod parse-command :Header
367 [command]
368 (let [args (:args command)
369 [format num-tracks division] (parse-number-list args)]
370 (assoc command :args
371 {:format format
372 :num-tracks num-tracks
373 :division division})))
375 (defmethod parse-command :Program_c
376 [command]
377 (let [args (:args command)
378 [channel program-num] (parse-number-list args)]
379 (assoc command :args
380 {:channel channel
381 :program-num program-num})))
384 (defn parse-midi [#^File midi-file]
385 (map
386 (comp parse-command
387 (fn [line]
388 (let [[[_ channel time command args]]
389 (re-seq command-line line)]
390 ;;(println (re-seq command-parse-1 line))
391 {:channel (Integer/parseInt channel)
392 :time (Integer/parseInt time)
393 :command (keyword command)
394 :args (apply str (drop 2 args))})))
395 (drop-last
396 (clojure.string/split-lines
397 (raw-midi-text midi-file)))))