Mercurial > vba-clojure
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.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))12 (def music-base new-kernel)17 (defn store [n address]18 (flatten19 [0xF520 0xE522 0x3E23 n25 0x2126 (reverse (disect-bytes-2 address))28 0x7730 0xE131 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 (flatten45 [(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 00000052 (store (Integer/parseInt "00000000" 2) 0xFF17) ;; volume 000053 (store (Integer/parseInt "00000000" 2) 0xFF18) ;; frequency-low54 (store (Integer/parseInt "00000000" 2) 0xFF19) ;; 00000 frequency-high56 (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 syntax70 ;; codes71 ;; note-code == 0x0072 ;; change-duty-code = 0x0173 ;; silence-code = 0x0275 ;; silence format76 ;; 2 bytes77 ;; [silence-code (0x02)]78 ;; [duration-8-bits]80 ;; note data format81 ;; 4 bytes82 ;; [note-code (0x00)]83 ;; [volume-4-bits 0 frequency-high-3-bits]84 ;; [frequengy-low-8-bits]85 ;; [duration-8-bits]87 ;; change-duty-format88 ;; 2 bytes89 ;; [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-message98 "Read the message which starts at the current value of HL and do99 what it says. Duration is left in A, and HL is advanced100 appropraitely."101 []102 (let [switch103 [0x2A ;; load message code into A, increment HL105 ;; switch on message106 0xFE107 note-code109 0x20110 :note-length]112 play-note113 [0x2A ;; load volume/frequency-high info114 0xF5 ;; push A115 0xE6116 (Integer/parseInt "11110000" 2) ;; volume mask117 0xE0118 0x17 ;; set volume119 0xF1 ;; pop A120 0xE6121 (Integer/parseInt "00000111" 2) ;; frequency-high mask122 0xE0123 0x19 ;; set frequency-high125 0x2A ;; load frequency low-bits126 0xE0127 0x18 ;; set frequency-low-bits129 0x2A]] ;; load duration130 (replace131 {:note-length (count play-note)}132 (concat switch play-note))))134 (defn play-note135 "Play the note referenced by HL in the appropiate channel.136 Leaves desired-duration in A."137 []138 [0x2A ;; load volume/frequency-high info139 0xF5 ;; push A140 0xE6141 (Integer/parseInt "11110000" 2) ;; volume mask142 0xE0143 0x17 ;; set volume144 0xF1 ;; pop A145 0xE6146 (Integer/parseInt "00000111" 2) ;; frequency-high mask147 0xE0148 0x19 ;; set frequency-high150 0x2A ;; load frequency low-bits151 0xE0152 0x18 ;; set frequency-low-bits154 0x2A ;; load duration155 ])157 (defn music-step []158 (flatten159 [160 0xF5 ;; push A161 0xF0162 0x05 ;; load current ticks163 0xB8 ;; B holds previous sub-ticks, subtract it from A164 ;; if A-B caused a carry, then (B > A) is true, and165 ;; A = current-sub-tics, B = previous-sub-ticks, so166 ;; current-sub-ticks < previous-sub-ticks, which means that the167 ;; timer counter HAS overflowed.168 0x30 ;; increment C only if last result caused carry169 0x01170 0x0C172 0x47 ;; update sub-ticks (A->B)174 0xF1 ;; pop AF, now A contains desired-ticks176 0xB9 ;; compare with current ticks178 ;; if desired-ticks = current ticks179 ;; go to next note ; set current set ticks to 0.181 0x20182 (+ (count (do-message)) 2)184 (do-message)186 0x0E187 0x00])) ;; 0->C (current-ticks)189 (defn music-kernel []190 (flatten191 [(clear-music-registers)193 0x21194 0x00195 0xD0 ;; set HL to 0xD000 == music-start196 0x0E197 0x00 ;; 0->C198 0x06199 0x00 ;; 0->B201 0x3E202 0x01203 0xE0204 0x06 ;; set TMA to 0206 0x3E207 (Integer/parseInt "00000110" 2)208 0xE0209 0x07 ;; set TAC to 65536 Hz and activate timer212 0xAF ;; initialiaze A to zero215 (music-step)216 0x18217 (->signed-8-bit (+ (- (count (music-step)))218 -2))]))220 (defn frequency-code->frequency221 [code]222 (assert (<= 0 code 2047))223 (/ 131072 (- 2048 code)))225 (defn clamp [x low high]226 (cond (> x high) high227 (< x low) low228 true x))230 (defn frequency->frequency-code231 [frequency]232 (clamp233 (Math/round234 (float235 (/ (- (* 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-code242 (frequency->frequency-code frequency)243 volume&high-frequency244 (+ (bit-shift-left volume 4)245 (bit-shift-right frequency-code 8))246 low-frequency247 (bit-and 0xFF frequency-code)]248 [note-code249 volume&high-frequency250 low-frequency251 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 scale263 (flatten264 [(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 0xC000275 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-program294 ([program]295 (let [target 0xC000]296 (-> (set-memory-range (second (music-base))297 target program)298 (PC! target)))))300 (defn test-timer []301 (flatten302 [0x3E303 0x01304 0xE0305 0x06 ;; set TMA to 0307 0x3E308 (Integer/parseInt "00000100" 2)309 0xE0310 0x07 ;; set TAC to 16384 Hz and activate timer312 (repeat313 500314 [0xF0315 0x05])]))317 (def third-kind318 (File. "/home/r/proj/midi/third-kind.mid"))320 (defn raw-midi-text [#^File midi-file]321 (:out322 (clojure.java.shell/sh323 "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_track334 [command] (discard-args command))336 (defmethod parse-command :End_track337 [command] (discard-args command))339 (defmethod parse-command :default340 [command] command)342 (defn parse-number-list343 [number-list-str]344 (map #(Integer/parseInt %)345 (clojure.string/split number-list-str #", ")))347 (defmethod parse-command :Tempo348 [command]349 (update-in command [:args] #(Integer/parseInt %)))351 (defn parse-midi-note-list352 [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_c359 [command]360 (update-in command [:args] parse-midi-note-list))362 (defmethod parse-command :Note_off_c363 [command]364 (update-in command [:args] parse-midi-note-list))366 (defmethod parse-command :Header367 [command]368 (let [args (:args command)369 [format num-tracks division] (parse-number-list args)]370 (assoc command :args371 {:format format372 :num-tracks num-tracks373 :division division})))375 (defmethod parse-command :Program_c376 [command]377 (let [args (:args command)378 [channel program-num] (parse-number-list args)]379 (assoc command :args380 {:channel channel381 :program-num program-num})))384 (defn parse-midi [#^File midi-file]385 (map386 (comp parse-command387 (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-last396 (clojure.string/split-lines397 (raw-midi-text midi-file)))))