This repository has been archived by the owner on Jan 6, 2021. It is now read-only.
[WIP] Midi import #30
Closed
Closed
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d8be335
Add test for calling java function
gdpelican 97a3f02
Read note_on and note_off data from midi file
gdpelican 4277220
Get better at importing and commenting functions
gdpelican 23913c6
Test each individual event-to-state application works
gdpelican e6765f4
Now allows imports of basic MIDI files to data array
gdpelican 36fb629
Cleanup comments
gdpelican 824145d
Implement style feedback
gdpelican File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
(ns alda.import | ||
"alda.import transforms other music files into Alda scores, which can then be stored | ||
or played. Currently we support importing MIDI files." | ||
(:require [potemkin.namespaces :refer (import-vars)] | ||
[alda.util :as util])) | ||
|
||
; sets log level to TIMBRE_LEVEL (if set) or :warn | ||
(util/set-log-level!) | ||
|
||
(defn- import-all-vars | ||
"Imports all public vars from a namespace into the alda.import namespace." | ||
[ns] | ||
(eval (list `import-vars (cons ns (keys (ns-publics ns)))))) | ||
|
||
(def ^:private namespaces | ||
'[alda.import.midi]) | ||
|
||
(doseq [ns namespaces] | ||
(require ns)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
(ns alda.import.midi | ||
(:import [javax.sound.midi MidiSystem] | ||
[java.io File])) | ||
|
||
(comment | ||
"This module accepts raw bytecode from a midi file, and converts it to alda syntax | ||
which is then printed to STDOUT.") | ||
|
||
(defn- find-existing | ||
"Given some criteria, find the first matching hash in a collection of hashes" | ||
[collection criteria] | ||
(first (filter #(= criteria (select-keys % (keys criteria))) collection))) | ||
|
||
(defn- note-on | ||
"We've just heard a new note beginning, update state partials to include it" | ||
[{:keys [instrument] :as state} event] | ||
(update state :partials conj { | ||
:channel (-> event .getMessage .getChannel) | ||
:pitch (-> event .getMessage .getData1) | ||
:volume (-> event .getMessage .getData2) | ||
:start (-> event .getTick) | ||
:instrument instrument | ||
; duration will be set when the note completes | ||
:duration nil})) | ||
|
||
(defn- note-off | ||
"We've just heard a note complete; remove the note from partials, add a duration, and include it as a completed note" | ||
[{:keys [partials] :as state} event] | ||
(if-let [{:keys [start] :as partial-note} | ||
(find-existing partials | ||
{:pitch (-> event .getMessage .getData1) | ||
:channel (-> event .getMessage .getChannel) | ||
:duration nil})] | ||
; (or (println "note: " partial-note) (println "end: " (.getTick event)) (println "start: " start) (println "dur: " (- (.getTick event) start))) | ||
(let [completed-note (assoc partial-note :duration (- (.getTick event) start))] | ||
(-> state | ||
(update :partials (partial remove #{partial-note})) | ||
(update :notes conj completed-note))) | ||
state)) | ||
|
||
(defn- program-change | ||
"We've just switched instruments in this channel; update state to reflect this" | ||
[state event] | ||
(assoc state :instrument (-> event .getMessage .getData1))) | ||
|
||
(defn- is-defined? | ||
"See if a java class responds to a method" | ||
[message method] | ||
(some #(= (.getName %) method) (-> message .getClass .getMethods))) | ||
|
||
(defn process-note-event | ||
[state event] | ||
(let [message (.getMessage event)] | ||
(case (if (is-defined? message "getCommand") (.getCommand message)) | ||
; a note_on event also signifies that the previous note should be turned off | ||
144 (-> state (note-off event) (note-on event)) | ||
128 (note-off state event) | ||
192 (program-change state event) | ||
state))) ; this isn't an event we care about; return the original state | ||
|
||
(defn- notes-from-track | ||
"Get a final state by reading through a series of note events" | ||
[track] | ||
(let [events (map #(.get track %) (range (.size track)))] | ||
(get (reduce process-note-event {} events) :notes ()))) | ||
|
||
(defn import-midi | ||
"Imports a .mid or .midi file specified by path, and prints it to STDOUT" | ||
[path] | ||
(mapcat notes-from-track (-> (File. path) MidiSystem/getSequence .getTracks))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
(ns alda.import.midi-test | ||
(:require [clojure.test :refer :all] | ||
[alda.test-helpers :refer (get-instrument)] | ||
[alda.import.midi :refer :all]) | ||
(:import [javax.sound.midi MidiEvent MidiMessage ShortMessage] | ||
[com.sun.media.sound FastShortMessage])) | ||
|
||
(defn- build-event | ||
"Build an event for testing" | ||
([command] (build-event command 0)) | ||
([command channel] (build-event command channel 0)) | ||
([command channel data1] (build-event command channel data1 0)) | ||
([command channel data1 data2] (build-event command channel data1 data2 10)) | ||
([command channel data1 data2 seq] | ||
(new MidiEvent (new ShortMessage command channel data1 data2) seq))) | ||
|
||
(deftest midi-tests | ||
(testing "can process a short midi file" | ||
(let [result (import-midi "./examples/midi/twotone.mid")] | ||
; we get a collection with two tones | ||
(is (= 2 (count result))) | ||
|
||
; first note | ||
(is (= 0 (get (first result) :instrument))) | ||
(is (= 0 (get (first result) :start))) | ||
(is (= 0 (get (first result) :channel))) | ||
(is (= 72 (get (first result) :pitch))) | ||
(is (= 11520 (get (first result) :duration))) | ||
(is (= 40 (get (first result) :volume))) | ||
; second note | ||
(is (= 0 (get (last result) :instrument))) | ||
(is (= 1440 (get (last result) :start))) | ||
(is (= 0 (get (last result) :channel))) | ||
(is (= 74 (get (last result) :pitch))) | ||
(is (= 7200 (get (last result) :duration))) | ||
(is (= 40 (get (last result) :volume))) | ||
) | ||
) | ||
|
||
; this doesn't work yet, I think because this file has 0-duration NOTE_ON events in lieu of NOTE_OFF events | ||
; (apparantly that's a thing that MIDI does sometimes) | ||
; (testing "can process a big midi file" | ||
; (is (= "some test data" (import-midi "./examples/midi/pokemon.mid"))) | ||
; ) | ||
|
||
(testing "note-on adds a new note partial" | ||
(let [event (build-event 144 9) | ||
state (process-note-event {} event)] | ||
(is (= 1 (count (get state :partials)))) | ||
(is (= 9 (get (first (get state :partials)) :channel))))) | ||
|
||
(testing "note-off updates an existing note with the duration" | ||
(let [event (build-event 128 0 0 0 50) | ||
prev-state (process-note-event {} (build-event 144 0 0 0 10)) | ||
state (process-note-event prev-state event)] | ||
(is (= 1 (count (get state :notes)))) | ||
(is (= 40 (get (first (get state :notes)) :duration))))) | ||
|
||
(testing "change-program updates the instrument of the state" | ||
(let [event (build-event 192 0 15) ; include '15' as the data for the instrument to change to | ||
state (process-note-event {} event)] | ||
(is (= 15 (get state :instrument))))) | ||
|
||
(testing "other events don't change the state" | ||
(let [event (build-event 208) | ||
state (process-note-event {} event)] | ||
(is (= {} state))))) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've noticed that in MIDI, playing the same note on the same note consecutively will cause a sort of implicit "NOTE OFF" for the first note so that the second note can play. Maybe we can adjust the logic for when to add a note to account for this?