Mercurial > vba-clojure
view clojure/com/aurellem/run/music.clj @ 454:bf87b87a4ad7
exporting to midi file.
author | Robert McIntyre <rlm@mit.edu> |
---|---|
date | Thu, 03 May 2012 07:25:06 -0500 |
parents | 067ea3f0d951 |
children | 1c10fa8366a7 |
line wrap: on
line source
1 (ns com.aurellem.run.music2 (:use (com.aurellem.gb saves gb-driver util constants3 items vbm characters money4 rlm-assembly))5 (:use (com.aurellem.run util title save-corruption6 bootstrap-0 bootstrap-1))7 (:require clojure.string)8 (:import [com.aurellem.gb.gb_driver SaveState])9 (:import java.io.File))11 (def third-kind12 (File. "/home/r/proj/midi/third-kind.mid"))14 (def pony "/home/r/proj/vba-clojure/music/pony-title.mid")16 (defn raw-midi-text [#^File midi-file]17 (:out18 (clojure.java.shell/sh19 "midicsv"20 (.getCanonicalPath midi-file)21 "-")))23 (def command-line #"^(\d+), (\d+), ([^,]+)(.*)$")25 (defmulti parse-command :command)27 (defn discard-args [command] (dissoc command :args))29 (defmethod parse-command :Start_track30 [command] (discard-args command))32 (defmethod parse-command :End_track33 [command] (discard-args command))35 (defmethod parse-command :default36 [command] command)38 (defn parse-number-list39 [number-list-str]40 (map #(Integer/parseInt %)41 (clojure.string/split number-list-str #", ")))43 (defmethod parse-command :Tempo44 [command]45 (update-in command [:args] #(Integer/parseInt %)))47 (defn parse-midi-note-list48 [midi-note-list-str]49 (let [[channel note velocity]50 (parse-number-list midi-note-list-str)]51 {:channel channel :note note :velocity velocity}))53 (defmethod parse-command :Note_on_c54 [command]55 (update-in command [:args] parse-midi-note-list))57 (defmethod parse-command :Note_off_c58 [command]59 (update-in command [:args] parse-midi-note-list))61 (defmethod parse-command :Header62 [command]63 (let [args (:args command)64 [format num-tracks division] (parse-number-list args)]65 (assoc command :args66 {:format format67 :num-tracks num-tracks68 :division division})))70 (defmethod parse-command :Program_c71 [command]72 (let [args (:args command)73 [channel program-num] (parse-number-list args)]74 (assoc command :args75 {:channel channel76 :program-num program-num})))78 (defn parse-midi [#^File midi-file]79 (map80 (comp parse-command81 (fn [line]82 (let [[[_ channel time command args]]83 (re-seq command-line line)]84 {:channel (Integer/parseInt channel)85 :time (Integer/parseInt time)86 :command (keyword command)87 :args (apply str (drop 2 args))})))88 (drop-last89 (clojure.string/split-lines90 (raw-midi-text midi-file)))))92 (def music-base new-kernel)94 (defn store [n address]95 (flatten96 [0xF597 0xE599 0x3E100 n102 0x21103 (reverse (disect-bytes-2 address))105 0x77107 0xE1108 0xF1]))110 (defn infinite-loop []111 [0x18 0xFE])113 (def divider-register 0xFF04)115 (defrecord Bit-Note [frequency volume duration duty])117 (defn clear-music-registers []118 (flatten119 [(store (Integer/parseInt "00000000" 2) 0xFF10) ;; sweep120 (store (Integer/parseInt "00000000" 2) 0xFF11) ;; pattern duty121 (store (Integer/parseInt "00000000" 2) 0xFF12) ;; volume122 (store (Integer/parseInt "00000000" 2) 0xFF13) ;; frequency-low123 (store (Integer/parseInt "00000000" 2) 0xFF14) ;; frequency-high125 (store (Integer/parseInt "00000000" 2) 0xFF16) ;; pattern duty 000000126 (store (Integer/parseInt "00000000" 2) 0xFF17) ;; volume 0000127 (store (Integer/parseInt "00000000" 2) 0xFF18) ;; frequency-low128 (store (Integer/parseInt "00000000" 2) 0xFF19) ;; 00000 frequency-high130 (store (Integer/parseInt "00000000" 2) 0xFF1A)131 (store (Integer/parseInt "00000000" 2) 0xFF1B)132 (store (Integer/parseInt "00000000" 2) 0xFF1C)133 (store (Integer/parseInt "00000000" 2) 0xFF1D)134 (store (Integer/parseInt "00000000" 2) 0xFF1E)136 (store (Integer/parseInt "00000000" 2) 0xFF20) ;; length137 (store (Integer/parseInt "00000000" 2) 0xFF21) ;; volume138 (store (Integer/parseInt "00000000" 2) 0xFF22) ;; noise-frequency139 (store (Integer/parseInt "00000000" 2) 0xFF23) ;; control140 ]))143 ;; mini-midi syntax145 ;; codes146 ;; note-code == 0x00147 ;; change-duty-code = 0x01148 ;; silence-code = 0x02150 ;; silence format151 ;; 2 bytes152 ;; [silence-code (0x02)]153 ;; [duration-8-bits]155 ;; note data format156 ;; 4 bytes157 ;; [note-code (0x00)]158 ;; [volume-4-bits 0 frequency-high-3-bits]159 ;; [frequengy-low-8-bits]160 ;; [duration-8-bits]162 ;; change-duty-format163 ;; 2 bytes164 ;; [change-duty-code (0x01)]165 ;; [new-duty]167 (def note-code 0x00)168 (def change-duty-code 0x01)169 (def silence-code 0x02)171 (defn do-message172 "Read the message which starts at the current value of HL and do173 what it says. Duration is left in A, and HL is advanced174 appropraitely."175 ([] (do-message 0x16))176 ([sound-base-address]177 (let [switch178 [0x2A ;; load message code into A, increment HL180 ;; switch on message181 0xFE182 note-code184 0x20185 :note-length]187 play-note188 [0x2A ;; load volume/frequency-high info189 0xF5 ;; push A190 0xE6191 (Integer/parseInt "11110000" 2) ;; volume mask192 0xE0193 (inc sound-base-address) ;;0x17 ;; set volume194 0xF1 ;; pop A195 0xE6196 (Integer/parseInt "00000111" 2) ;; frequency-high mask197 0xE0198 (+ 3 sound-base-address) ;;0x19 ;; set frequency-high200 0x2A ;; load frequency low-bits201 0xE0202 (+ 2 sound-base-address) ;;0x18 ;; set frequency-low-bits203 0x2A]] ;; load duration204 (replace205 {:note-length (count play-note)}206 (concat switch play-note)))))208 ;; (defn play-note209 ;; "Play the note referenced by HL in the appropiate channel.210 ;; Leaves desired-duration in A."212 ;; [0x2A ;; load volume/frequency-high info213 ;; 0xF5 ;; push A214 ;; 0xE6215 ;; (Integer/parseInt "11110000" 2) ;; volume mask216 ;; 0xE0217 ;; 0x17 ;; set volume218 ;; 0xF1 ;; pop A219 ;; 0xE6220 ;; (Integer/parseInt "00000111" 2) ;; frequency-high mask221 ;; 0xE0222 ;; 0x19 ;; set frequency-high224 ;; 0x2A ;; load frequency low-bits225 ;; 0xE0226 ;; 0x18 ;; set frequency-low-bits228 ;; 0x2A ;; load duration229 ;; ])231 (defn music-step [sound-base-address]232 ;; C == current-ticks233 ;; A == desired-ticks235 (flatten236 [;; restore variables from stack237 0xE1 ;; pop HL238 0xC1 ;; pop CB239 0xF1 ;; pop AF242 0xF5 ;; push A243 0xF0244 0x05 ;; load current ticks from 0xF005245 0xB8 ;;246 0x30 ;; increment C only if last result caused carry247 0x01248 0x0C250 0x47 ;; update sub-ticks (A->B)252 0xF1 ;; pop AF, now A contains desired-ticks254 0xB9 ;; compare with current ticks256 ;; if desired-ticks = current ticks257 ;; go to next note ; set current set ticks to 0.259 0x20260 (+ (count (do-message 0)) 2)262 (do-message sound-base-address)264 0x0E265 0x00 ;; 0->C (current-ticks)267 ;; save variables to stack268 0xF5 ;; push AF269 0xC5 ;; push CB270 0xE5 ;; push HL273 ]))275 (def music-1 0x11)276 (def music-2 0x16)278 (defn music-kernel []279 (flatten280 [;; global initilization section281 (clear-music-registers)283 0x3E284 0x01285 0xE0286 0x06 ;; set TMA to 0288 0x3E289 (Integer/parseInt "00000110" 2)290 0xE0291 0x07 ;; set TAC to 65536 Hz and activate timer293 ;; initialize frame 1294 0x21295 0x00296 0xA0 ;; set HL to 0xA000 == music-start 1297 0x0E298 0x00 ;; 0->C299 0x06300 0x00 ;; 0->B302 0xAF ;; 0->A304 0xF5 ;; push AF305 0xC5 ;; push CB306 0xE5 ;; push HL308 ;; initialize frame 2309 0x21310 0x00311 0xB0 ;; set HL to 0xB000 == music-start 2313 0xF5 ;; push AF314 0xC5 ;; push CB315 0xE5 ;; push HL318 ;; main music loop320 0xE8 ;; SP + 6; activate frame 1321 6322 (music-step music-1)323 ;;(repeat (count (music-step music-1)) 0x00)325 0xE8 ;; SP - 6; activate frame 2326 (->signed-8-bit -6)327 ;;(repeat (count (music-step music-2)) 0x00)328 (music-step music-2)331 0x18332 (->signed-8-bit (+333 ;; two music-steps334 (- (* 2 (count (music-step 0))))335 -2 ;; this jump instruction336 -2 ;; activate frame 1337 -2 ;; activate frame 2338 ))]))340 (defn frequency-code->frequency341 [code]342 (assert (<= 0 code 2047))343 (/ 131072 (- 2048 code)))345 (defn clamp [x low high]346 (cond (> x high) high347 (< x low) low348 true x))350 (defn frequency->frequency-code351 [frequency]352 (clamp353 (Math/round354 (float355 (/ (- (* 2048 frequency) 131072) frequency)))356 0x00 2048))358 (defn note-codes [frequency volume duration]359 (assert (<= 0 volume 0xF))360 (if (<= duration 0xFF)361 (let [frequency-code362 (frequency->frequency-code frequency)363 volume&high-frequency364 (+ (bit-shift-left volume 4)365 (bit-shift-right frequency-code 8))366 low-frequency367 (bit-and 0xFF frequency-code)]368 [note-code369 volume&high-frequency370 low-frequency371 duration])372 (vec373 (flatten374 [(note-codes frequency volume 0xFF)375 (note-codes frequency volume (- duration 0xFF))]))))378 (defn midi-code->frequency379 [midi-code]380 (* 8.1757989156381 (Math/pow 2 (* (float (/ 12)) midi-code))))383 ;; division == clock-pulses / quarter-note384 ;; tempo == microseconds / quarter-note386 ;; have: clock-pulses387 ;; want: seconds390 (defn silence [length]391 {:frequency 1392 :duration length393 :volume 0})395 (defn commands396 "return all events where #(= (:command %) command)"397 [command s]398 (filter #(= command (:command %)) s))400 (defn midi-track->mini-midi [#^File midi-file track-num]401 (let [midi-events (parse-midi midi-file)403 note-on-events (commands :Note_on_c midi-events)404 note-off-events (commands :Note_off_c midi-events)406 select-channel407 (fn [n s]408 (sort-by :time (filter #(= n (:channel (:args %))) s)))410 channel-on (select-channel track-num note-on-events)412 channel-off (select-channel track-num note-off-events)415 tempo (:args (first (commands :Tempo midi-events)))416 division417 (:division (:args (first (commands :Header midi-events))))419 notes420 (map421 (fn [note-on note-off]422 {:frequency (midi-code->frequency (:note (:args note-on)))423 :duration424 (/ (* (/ tempo division)425 (- (:time note-off) (:time note-on)))426 1e6) ;; convert clock-pulses into seconds427 :volume (int (/ (:velocity (:args note-on)) 10))428 :time-stamp (/ (* (/ tempo division)429 (:time note-on)) 1e6)})430 channel-on channel-off)432 silences433 (map (fn [note-1 note-2]434 (let [note-1-space (- (:time-stamp note-2)435 (:time-stamp note-1))436 note-1-length (:duration note-1)]437 (silence (- note-1-space note-1-length))))438 ;; to handle silence at the beginning.439 (concat [(assoc (silence 0)440 :time-stamp 0)] notes)441 notes)443 notes-with-silence444 (filter (comp not zero? :duration)445 (interleave silences notes))]446 (map447 (fn [note-event]448 (note-codes (:frequency note-event)449 (:volume note-event)450 (int (* (:duration note-event) 0x100))))451 notes-with-silence)))453 (defn midi->mini-midi [#^File midi-file]454 {:track-1 (flatten (midi-track->mini-midi midi-file 1))455 :track-2 (flatten (midi-track->mini-midi midi-file 2))})457 (defn play-midi [#^File midi-file]458 (let [track-1-target 0xA000459 track-2-target 0xB000460 program-target 0xC000461 mini-midi (midi->mini-midi midi-file)462 long-silence (flatten (note-codes 20 0 9001))]464 (-> (second (music-base))465 (set-memory-range track-1-target long-silence)466 (set-memory-range track-2-target long-silence)467 (set-memory-range track-1-target (:track-1 mini-midi))468 (set-memory-range track-2-target (:track-2 mini-midi))469 (set-memory-range program-target (music-kernel))470 (PC! program-target))))475 (def C4 (partial note-codes 261.63))476 (def D4 (partial note-codes 293.66))477 (def E4 (partial note-codes 329.63))478 (def F4 (partial note-codes 349.23))479 (def G4 (partial note-codes 392))480 (def A4 (partial note-codes 440))481 (def B4 (partial note-codes 493.88))482 (def C5 (partial note-codes 523.3))484 (def scale485 (flatten486 [(C4 0xF 0x40)487 (D4 0xF 0x40)488 (E4 0xF 0x40)489 (F4 0xF 0x40)490 (G4 0xF 0x40)491 (A4 0xF 0x40)492 (B4 0xF 0x40)493 (C5 0xF 0x40)]))495 (defn play-music [music-bytes]496 (let [program-target 0xC000497 music-target 0xA000]498 (-> (set-memory-range (second (music-base))499 program-target (music-kernel))500 (set-memory-range music-target music-bytes)501 (PC! program-target))))505 ;; (defn test-note [music-bytes]506 ;; (-> (set-memory-range (second (music-base))507 ;; 0xC000 (concat (clear-music-registers)508 ;; (play-note)509 ;; (infinite-loop)))510 ;; (set-memory-range 0xD000 music-bytes)511 ;; (PC! 0xC000)512 ;; (HL! 0xD000)513 ;; ))516 (defn run-program517 ([program]518 (let [target 0xC000]519 (-> (set-memory-range (second (music-base))520 target program)521 (PC! target)))))523 (defn test-timer []524 (flatten525 [0x3E526 0x01527 0xE0528 0x06 ;; set TMA to 0530 0x3E531 (Integer/parseInt "00000100" 2)532 0xE0533 0x07 ;; set TAC to 16384 Hz and activate timer535 (repeat536 500537 [0xF0538 0x05])]))