Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 91c9a6c5606dbee94888ad59add639fef1de4718 @rustyio rustyio committed Sep 22, 2010
39 Makefile
@@ -0,0 +1,39 @@
+
+
+all: deps compile
+
+compile:
+ ./rebar compile
+deps:
+ ./rebar get-deps
+
+clean:
+ ./rebar clean
+
+distclean: clean devclean relclean ballclean
+ ./rebar delete-deps
+
+run: all
+ erl \
+ -name phone1@127.0.0.1 \
+ -setcookie phone \
+ -pa deps/*/ebin \
+ -pa apps/*/ebin \
+ -eval "application:start(sasl)" \
+ -eval "application:start(crypto)" \
+ -eval "application:start(webmachine)" \
+ -eval "application:start(riak_core)" \
+ -eval "application:start(riakophone)"
+
+run2: all
+ erl \
+ -name `whoami`@127.0.0.1 \
+ -setcookie phone \
+ -pa deps/*/ebin \
+ -pa apps/*/ebin \
+ -eval "application:start(sasl)" \
+ -eval "application:start(crypto)" \
+ -eval "application:start(webmachine)" \
+ -eval "application:start(riak_core)" \
+ -eval "application:start(riakophone)" \
+ -eval "riak_core_gossip:send_ring('phone1@127.0.0.1', node())"
14 apps/midilib/README.rdoc
@@ -0,0 +1,14 @@
+= Erlang MIDI Library
+
+== midifile
+
+Reads and writes type 1 MIDI files.
+
+== midilib
+
+Utility functions for handling note lengths, beats, quantization, and note
+names.
+
+== core_midi
+
+An experiment in communication with the Mac OS X Core MIDI framework.
1 apps/midilib/ebin/.gitignore
@@ -0,0 +1 @@
+.*.beam
8 apps/midilib/ebin/midilib.app
@@ -0,0 +1,8 @@
+{application,midilib,
+ [{description,[]},
+ {vsn,"1"},
+ {registered,[]},
+ {applications,[kernel,stdlib]},
+ {mod,{midilib_app,[]}},
+ {env,[]},
+ {modules,[mftext,midifile,midilib,midilib_app,midilib_sup]}]}.
47 apps/midilib/include/midi_consts.hrl
@@ -0,0 +1,47 @@
+% Channel messages
+-define(STATUS_NIBBLE_OFF, 16#8).
+-define(STATUS_NIBBLE_ON, 16#9).
+-define(STATUS_NIBBLE_POLY_PRESS, 16#A).
+-define(STATUS_NIBBLE_CONTROLLER, 16#B).
+-define(STATUS_NIBBLE_PROGRAM_CHANGE, 16#C).
+-define(STATUS_NIBBLE_CHANNEL_PRESSURE, 16#D).
+-define(STATUS_NIBBLE_PITCH_BEND, 16#E).
+
+% System common messages
+-define(STATUS_SYSEX, 16#F0).
+-define(STATUS_SONG_POINTER, 16#F2).
+-define(STATUS_SONG_SELECT, 16#F3).
+-define(STATUS_TUNE_REQUEST, 16#F6).
+-define(STATUS_EOX, 16#F7).
+
+% System realtime messages
+% MIDI clock (24 per quarter note)
+-define(STATUS_CLOCK, 16#F8).
+% Sequence start
+-define(STATUS_START, 16#FA).
+% Sequence continue
+-define(STATUS_CONTINUE, 16#FB).
+% Sequence stop
+-define(STATUS_STOP, 16#FC).
+% Active sensing (sent every 300 ms when nothing else being sent)
+-define(STATUS_ACTIVE_SENSE, 16#FE).
+% System reset
+-define(STATUS_SYSTEM_RESET, 16#FF).
+
+% Meta events
+-define(STATUS_META_EVENT, 16#FF).
+-define(META_SEQ_NUM, 16#00).
+-define(META_TEXT, 16#01).
+-define(META_COPYRIGHT, 16#02).
+-define(META_SEQ_NAME, 16#03).
+-define(META_INSTRUMENT, 16#04).
+-define(META_LYRIC, 16#05).
+-define(META_MARKER, 16#06).
+-define(META_CUE, 16#07).
+-define(META_MIDI_CHAN_PREFIX, 16#20).
+-define(META_TRACK_END, 16#2f).
+-define(META_SET_TEMPO, 16#51).
+-define(META_SMPTE, 16#54).
+-define(META_TIME_SIG, 16#58).
+-define(META_KEY_SIG, 16#59).
+-define(META_SEQUENCER_SPECIFIC, 16#7F).
BIN apps/midilib/rebar
Binary file not shown.
30 apps/midilib/src/mftext.erl
@@ -0,0 +1,30 @@
+-module(mftext).
+-export([seq_to_text/1, seq_to_text/2, track_to_text/1, track_to_text/2, event_to_text/1, event_to_text/2]).
+-author("Jim Menard, jimm@io.com").
+
+seq_to_text(Seq) ->
+ seq_to_text(Seq, false).
+seq_to_text(Seq, ShowChanEvents) ->
+ {seq, _, Tracks} = Seq,
+ lists:map(fun(T) -> track_to_text(T, ShowChanEvents) end, Tracks),
+ ok.
+
+track_to_text(Track) ->
+ track_to_text(Track, false).
+track_to_text(Track, ShowChanEvents) ->
+ io:format("Track start~n"),
+ {track, Events} = Track,
+ lists:map(fun(E) -> event_to_text(E, ShowChanEvents) end, Events),
+ ok.
+
+event_to_text(Event) ->
+ event_to_text(Event, false).
+event_to_text(Event, ShowChanEvents) ->
+ {Name, _} = Event,
+ IsChanEvent = lists:any(fun(X) -> X =:= Name end,
+ [off, on, poly_press, controller, program,
+ chan_press, pitch_bend]),
+ if
+ ShowChanEvents; not IsChanEvent ->
+ io:format("~p~n", [Event])
+ end.
381 apps/midilib/src/midifile.erl
@@ -0,0 +1,381 @@
+-module(midifile).
+-export([read/1, write/2]).
+-author("Jim Menard, jimm@io.com").
+-include("midi_consts.hrl").
+
+%% This module reads and writes MIDI files.
+
+%-define(DEBUG, true).
+-ifdef(DEBUG).
+-define(DPRINT(X, Y), io:format(X, Y)).
+-else.
+-define(DPRINT(X, Y), void).
+-endif.
+
+
+%% Returns
+%% {seq, {header...}, ConductorTrack, ListOfTracks}
+%% header is {header, Format, Division}
+%% ConductorTrack is the first track of a format 1 MIDI file
+%% each track including the conductor track is
+%% {track, ListOfEvents}
+%% each event is
+%% {event_name, DeltaTime, [values...]}
+%% where values after DeltaTime are specific to each event type. If the value
+%% is a string, then the string appears instead of [values...].
+read(Path) ->
+ case file:open(Path, [read, binary, raw]) of
+ {ok, F} ->
+ FilePos = look_for_chunk(F, 0, <<$M, $T, $h, $d>>,
+ file:pread(F, 0, 4)),
+ [Header, NumTracks] = parse_header(file:pread(F, FilePos, 10)),
+ Tracks = read_tracks(F, NumTracks, FilePos + 10, []),
+ file:close(F),
+ [ConductorTrack | RemainingTracks] = Tracks,
+ {seq, Header, ConductorTrack, RemainingTracks};
+ Error ->
+ {Path, Error}
+ end.
+
+% Look for Cookie in file and return file position after Cookie.
+look_for_chunk(_F, FilePos, Cookie, {ok, Cookie}) ->
+ FilePos + size(Cookie);
+look_for_chunk(F, FilePos, Cookie, {ok, _}) ->
+ % This isn't efficient, because we only advance one character at a time.
+ % We should really look for the first char in Cookie and, if found,
+ % advance that far.
+ look_for_chunk(F, FilePos + 1, Cookie, file:pread(F, FilePos + 1,
+ size(Cookie))).
+
+parse_header({ok, <<_BytesToRead:32/integer, Format:16/integer,
+ NumTracks:16/integer, Division:16/integer>>}) ->
+ [{header, Format, Division}, NumTracks].
+
+read_tracks(_F, 0, _FilePos, Tracks) ->
+ lists:reverse(Tracks);
+% TODO: make this distributed. Would need to scan each track to get start
+% position of next track.
+read_tracks(F, NumTracks, FilePos, Tracks) ->
+ ?DPRINT("read_tracks, NumTracks = ~p, FilePos = ~p~n",
+ [NumTracks, FilePos]),
+ [Track, NextTrackFilePos] = read_track(F, FilePos),
+ ?DPRINT("read_tracks, NextTrackFilePos = ~p~n", [NextTrackFilePos]),
+ read_tracks(F, NumTracks - 1, NextTrackFilePos, [Track|Tracks]).
+
+read_track(F, FilePos) ->
+ TrackStart = look_for_chunk(F, FilePos, <<$M, $T, $r, $k>>,
+ file:pread(F, FilePos, 4)),
+ BytesToRead = parse_track_header(file:pread(F, TrackStart, 4)),
+ ?DPRINT("reading track, FilePos = ~p, BytesToRead = ~p~n",
+ [TrackStart, BytesToRead]),
+ ?DPRINT("next track pos = ~p~n", [TrackStart + 4 + BytesToRead]),
+ put(status, 0),
+ put(chan, -1),
+ [{track, event_list(F, TrackStart + 4, BytesToRead, [])},
+ TrackStart + 4 + BytesToRead].
+
+parse_track_header({ok, <<BytesToRead:32/integer>>}) ->
+ BytesToRead.
+
+event_list(_F, _FilePos, 0, Events) ->
+ lists:reverse(Events);
+event_list(F, FilePos, BytesToRead, Events) ->
+ [DeltaTime, VarLenBytesUsed] = read_var_len(file:pread(F, FilePos, 4)),
+ {ok, ThreeBytes} = file:pread(F, FilePos+VarLenBytesUsed, 3),
+ ?DPRINT("reading event, FilePos = ~p, BytesToRead = ~p, ThreeBytes = ~p~n",
+ [FilePos, BytesToRead, ThreeBytes]),
+ [Event, EventBytesRead] =
+ read_event(F, FilePos+VarLenBytesUsed, DeltaTime, ThreeBytes),
+ BytesRead = VarLenBytesUsed + EventBytesRead,
+ event_list(F, FilePos + BytesRead, BytesToRead - BytesRead, [Event|Events]).
+
+read_event(_F, _FilePos, DeltaTime,
+ <<?STATUS_NIBBLE_OFF:4, Chan:4, Note:8, Vel:8>>) ->
+ ?DPRINT("off~n", []),
+ put(status, ?STATUS_NIBBLE_OFF),
+ put(chan, Chan),
+ [{off, DeltaTime, [Chan, Note, Vel]}, 3];
+% note on, velocity 0 is a note off
+read_event(_F, _FilePos, DeltaTime,
+ <<?STATUS_NIBBLE_ON:4, Chan:4, Note:8, 0:8>>) ->
+ ?DPRINT("off (using on vel 0)~n", []),
+ put(status, ?STATUS_NIBBLE_ON),
+ put(chan, Chan),
+ [{off, DeltaTime, [Chan, Note, 64]}, 3];
+read_event(_F, _FilePos, DeltaTime,
+ <<?STATUS_NIBBLE_ON:4, Chan:4, Note:8, Vel:8>>) ->
+ ?DPRINT("on~n", []),
+ put(status, ?STATUS_NIBBLE_ON),
+ put(chan, Chan),
+ [{on, DeltaTime, [Chan, Note, Vel]}, 3];
+read_event(_F, _FilePos, DeltaTime,
+ <<?STATUS_NIBBLE_POLY_PRESS:4, Chan:4, Note:8, Amount:8>>) ->
+ ?DPRINT("poly press~n", []),
+ put(status, ?STATUS_NIBBLE_POLY_PRESS),
+ put(chan, Chan),
+ [{poly_press, DeltaTime, [Chan, Note, Amount]}, 3];
+read_event(_F, _FilePos, DeltaTime,
+ <<?STATUS_NIBBLE_CONTROLLER:4, Chan:4, Controller:8, Value:8>>) ->
+ ?DPRINT("controller ch ~p, ctrl ~p, val ~p~n", [Chan, Controller, Value]),
+ put(status, ?STATUS_NIBBLE_CONTROLLER),
+ put(chan, Chan),
+ [{controller, DeltaTime, [Chan, Controller, Value]}, 3];
+read_event(_F, _FilePos, DeltaTime,
+ <<?STATUS_NIBBLE_PROGRAM_CHANGE:4, Chan:4, Program:8, _:8>>) ->
+ ?DPRINT("prog change~n", []),
+ put(status, ?STATUS_NIBBLE_PROGRAM_CHANGE),
+ put(chan, Chan),
+ [{program, DeltaTime, [Chan, Program]}, 2];
+read_event(_F, _FilePos, DeltaTime,
+ <<?STATUS_NIBBLE_CHANNEL_PRESSURE:4, Chan:4, Amount:8, _:8>>) ->
+ ?DPRINT("chan pressure~n", []),
+ put(status, ?STATUS_NIBBLE_CHANNEL_PRESSURE),
+ put(chan, Chan),
+ [{chan_press, DeltaTime, [Chan, Amount]}, 2];
+read_event(_F, _FilePos, DeltaTime,
+ <<?STATUS_NIBBLE_PITCH_BEND:4, Chan:4, 0:1, LSB:7, 0:1, MSB:7>>) ->
+ ?DPRINT("pitch bend~n", []),
+ put(status, ?STATUS_NIBBLE_PITCH_BEND),
+ put(chan, Chan),
+ [{pitch_bend, DeltaTime, [Chan, <<0:2, MSB:7, LSB:7>>]}, 3];
+read_event(_F, _FilePos, DeltaTime,
+ <<?STATUS_META_EVENT:8, ?META_TRACK_END:8, 0:8>>) ->
+ ?DPRINT("end of track~n", []),
+ put(status, ?STATUS_META_EVENT),
+ put(chan, 0),
+ [{track_end, DeltaTime, []}, 3];
+read_event(F, FilePos, DeltaTime, <<?STATUS_META_EVENT:8, Type:8, _:8>>) ->
+ ?DPRINT("meta event~n", []),
+ put(status, ?STATUS_META_EVENT),
+ put(chan, 0),
+ [Length, LengthBytesUsed] = read_var_len(file:pread(F, FilePos + 2, 4)),
+ LengthBeforeData = LengthBytesUsed + 2,
+ {ok, Data} = file:pread(F, FilePos + LengthBeforeData, Length),
+ TotalLength = LengthBeforeData + Length,
+ ?DPRINT(" type = ~p, var len = ~p, len before data = ~p, total len = ~p,~n data = ~p~n",
+ [Type, Length, LengthBeforeData, TotalLength, Data]),
+ case Type of
+ ?META_SEQ_NUM ->
+ [{seq_num, DeltaTime, [Data]}, TotalLength];
+ ?META_TEXT ->
+ [{text, DeltaTime, binary_to_list(Data)}, TotalLength];
+ ?META_COPYRIGHT ->
+ [{copyright, DeltaTime, binary_to_list(Data)}, TotalLength];
+ ?META_SEQ_NAME ->
+ [{seq_name, DeltaTime, binary_to_list(Data)}, TotalLength];
+ ?META_INSTRUMENT ->
+ [{instrument, DeltaTime, binary_to_list(Data)}, TotalLength];
+ ?META_LYRIC ->
+ [{lyric, DeltaTime, binary_to_list(Data)}, TotalLength];
+ ?META_MARKER ->
+ [{marker, DeltaTime, binary_to_list(Data)}, TotalLength];
+ ?META_CUE ->
+ [{cue, DeltaTime, binary_to_list(Data)}, TotalLength];
+ ?META_MIDI_CHAN_PREFIX ->
+ [{midi_chan_prefix, DeltaTime, [Data]}, TotalLength];
+ ?META_SET_TEMPO ->
+ % Data is microseconds per quarter note, in three bytes
+ <<B0:8, B1:8, B2:8>> = Data,
+ [{tempo, DeltaTime, [(B0 bsl 16) + (B1 bsl 8) + B2]}, TotalLength];
+ ?META_SMPTE ->
+ [{smpte, DeltaTime, [Data]}, TotalLength];
+ ?META_TIME_SIG ->
+ [{time_signature, DeltaTime, [Data]}, TotalLength];
+ ?META_KEY_SIG ->
+ [{key_signature, DeltaTime, [Data]}, TotalLength];
+ ?META_SEQUENCER_SPECIFIC ->
+ [{seq_name, DeltaTime, [Data]}, TotalLength];
+ _ ->
+ ?DPRINT(" unknown meta type ~p~n", [Type]),
+ [{unknown_meta, DeltaTime, [Type, Data]}, TotalLength]
+ end;
+read_event(F, FilePos, DeltaTime, <<?STATUS_SYSEX:8, _:16>>) ->
+ ?DPRINT("sysex~n", []),
+ put(status, ?STATUS_SYSEX),
+ put(chan, 0),
+ [Length, LengthBytesUsed] = read_var_len(file:pread(F, FilePos + 1, 4)),
+ {ok, Data} = file:pread(F, FilePos + LengthBytesUsed, Length),
+ [{sysex, DeltaTime, [Data]}, LengthBytesUsed + Length];
+% Handle running status bytes
+read_event(F, FilePos, DeltaTime, <<B0:8, B1:8, _:8>>) when B0 < 128 ->
+ Status = get(status),
+ Chan = get(chan),
+ ?DPRINT("running status byte, status = ~p, chan = ~p~n", [Status, Chan]),
+ [Event, NumBytes] =
+ read_event(F, FilePos, DeltaTime, <<Status:4, Chan:4, B0:8, B1:8>>),
+ [Event, NumBytes - 1];
+read_event(_F, _FilePos, DeltaTime, <<Unknown:8, _:16>>) ->
+ ?DPRINT("unknown byte ~p~n", [Unknown]),
+ put(status, 0),
+ put(chan, 0),
+% exit("Unknown status byte " ++ Unknown).
+ [{unknown_status, DeltaTime, [Unknown]}, 3].
+
+read_var_len({ok, <<0:1, B0:7, _:24>>}) ->
+ [B0, 1];
+read_var_len({ok, <<1:1, B0:7, 0:1, B1:7, _:16>>}) ->
+ [(B0 bsl 7) + B1, 2];
+read_var_len({ok, <<1:1, B0:7, 1:1, B1:7, 0:1, B2:7, _:8>>}) ->
+ [(B0 bsl 14) + (B1 bsl 7) + B2, 3];
+read_var_len({ok, <<1:1, B0:7, 1:1, B1:7, 1:1, B2:7, 0:1, B3:7>>}) ->
+ [(B0 bsl 21) + (B1 bsl 14) + (B2 bsl 7) + B3, 4];
+read_var_len({ok, <<1:1, B0:7, 1:1, B1:7, 1:1, B2:7, 1:1, B3:7>>}) ->
+ ?DPRINT("WARNING: bad var len format; all 4 bytes have high bit set~n", []),
+ [(B0 bsl 21) + (B1 bsl 14) + (B2 bsl 7) + B3, 4].
+
+-ifdef(DEBUG).
+rvl(<<0:1, B0:7>>) ->
+ read_var_len({ok, <<0:1, B0:7, 0:8, 0:8, 0:8>>});
+rvl(<<1:1, B0:7, 0:1, B1:7>>) ->
+ read_var_len({ok, <<1:1, B0:7, 0:1, B1:7, 0:8, 0:8>>});
+rvl(<<1:1, B0:7, 1:1, B1:7, 0:1, B2:7>>) ->
+ read_var_len({ok, <<1:1, B0:7, 1:1, B1:7, 0:1, B2:7, 0:8>>});
+rvl(<<1:1, B0:7, 1:1, B1:7, 1:1, B2:7, 0:1, B3:7>>) ->
+ read_var_len({ok, <<1:1, B0:7, 1:1, B1:7, 1:1, B2:7, 0:1, B3:7>>}).
+-endif.
+
+write({seq, Header, ConductorTrack, Tracks}, Path) ->
+ L = [header_io_list(Header, length(Tracks) + 1) |
+ lists:map(fun(T) -> track_io_list(T) end, [ConductorTrack | Tracks])],
+ ok = file:write_file(Path, L).
+
+header_io_list(Header, NumTracks) ->
+ {header, _, Division} = Header,
+ ["MThd",
+ 0, 0, 0, 6, % header chunk size
+ 0, 1, % format,
+ (NumTracks bsr 8) band 255, % num tracks
+ NumTracks band 255,
+ (Division bsr 8) band 255, % division
+ Division band 255].
+
+track_io_list(Track) ->
+ {track, Events} = Track,
+ put(status, 0),
+ put(chan, 0),
+ EventList = lists:map(fun(E) -> event_io_list(E) end, Events),
+ ChunkSize = chunk_size(EventList),
+ ["MTrk",
+ (ChunkSize bsr 24) band 255,
+ (ChunkSize bsr 16) band 255,
+ (ChunkSize bsr 8) band 255,
+ ChunkSize band 255,
+ EventList].
+
+% Return byte size of L, which is an IO list that contains lists, bytes, and
+% binaries.
+chunk_size(L) ->
+ lists:foldl(fun(E, Acc) -> Acc + io_list_element_size(E) end, 0,
+ lists:flatten(L)).
+io_list_element_size(E) when is_binary(E) ->
+ size(E);
+io_list_element_size(_E) ->
+ 1.
+
+event_io_list({off, DeltaTime, [Chan, Note, Vel]}) ->
+ RunningStatus = get(status),
+ RunningChan = get(chan),
+ if
+ RunningChan =:= Chan,
+ (RunningStatus =:= ?STATUS_NIBBLE_OFF orelse
+ (RunningStatus =:= ?STATUS_NIBBLE_ON andalso Vel =:= 64)) ->
+ Status = [],
+ OutVel = 0;
+ true ->
+ Status = (?STATUS_NIBBLE_OFF bsl 4) + Chan,
+ OutVel = Vel,
+ put(status, ?STATUS_NIBBLE_OFF),
+ put(chan, Chan)
+ end,
+ [var_len(DeltaTime), Status, Note, OutVel];
+event_io_list({on, DeltaTime, [Chan, Note, Vel]}) ->
+ [var_len(DeltaTime), running_status(?STATUS_NIBBLE_ON, Chan), Note, Vel];
+event_io_list({poly_press, DeltaTime, [Chan, Note, Amount]}) ->
+ [var_len(DeltaTime), running_status(?STATUS_NIBBLE_POLY_PRESS, Chan), Note,
+ Amount];
+event_io_list({controller, DeltaTime, [Chan, Controller, Value]}) ->
+ [var_len(DeltaTime), running_status(?STATUS_NIBBLE_CONTROLLER, Chan),
+ Controller, Value];
+event_io_list({program, DeltaTime, [Chan, Program]}) ->
+ [var_len(DeltaTime), running_status(?STATUS_NIBBLE_PROGRAM_CHANGE, Chan),
+ Program];
+event_io_list({chan_press, DeltaTime, [Chan, Amount]}) ->
+ [var_len(DeltaTime), running_status(?STATUS_NIBBLE_CHANNEL_PRESSURE, Chan),
+ Amount];
+event_io_list({pitch_bend, DeltaTime, [Chan, <<0:2, MSB:7, LSB:7>>]}) ->
+ [var_len(DeltaTime), running_status(?STATUS_NIBBLE_PITCH_BEND, Chan),
+ <<0:1, LSB:7, 0:1, MSB:7>>];
+event_io_list({track_end, DeltaTime}) ->
+ ?DPRINT("track_end~n", []),
+ put(status, ?STATUS_META_EVENT),
+ [var_len(DeltaTime), ?STATUS_META_EVENT, ?META_TRACK_END, 0];
+event_io_list({seq_num, DeltaTime, [Data]}) ->
+ meta_io_list(DeltaTime, ?META_SEQ_NUM, Data);
+event_io_list({text, DeltaTime, Data}) ->
+ meta_io_list(DeltaTime, ?META_TEXT, Data);
+event_io_list({copyright, DeltaTime, Data}) ->
+ meta_io_list(DeltaTime, ?META_COPYRIGHT, Data);
+event_io_list({seq_name, DeltaTime, Data}) ->
+ put(status, ?STATUS_META_EVENT),
+ meta_io_list(DeltaTime, ?META_TRACK_END, Data);
+event_io_list({instrument, DeltaTime, Data}) ->
+ meta_io_list(DeltaTime, ?META_INSTRUMENT, Data);
+event_io_list({lyric, DeltaTime, Data}) ->
+ meta_io_list(DeltaTime, ?META_LYRIC, Data);
+event_io_list({marker, DeltaTime, Data}) ->
+ meta_io_list(DeltaTime, ?META_MARKER, Data);
+event_io_list({cue, DeltaTime, Data}) ->
+ meta_io_list(DeltaTime, ?META_CUE, Data);
+event_io_list({midi_chan_prefix, DeltaTime, [Data]}) ->
+ meta_io_list(DeltaTime, ?META_MIDI_CHAN_PREFIX, Data);
+event_io_list({tempo, DeltaTime, [Data]}) ->
+ ?DPRINT("tempo, data = ~p~n", [Data]),
+ put(status, ?STATUS_META_EVENT),
+ [var_len(DeltaTime), ?STATUS_META_EVENT, ?META_SET_TEMPO, var_len(3),
+ (Data bsr 16) band 255,
+ (Data bsr 8) band 255,
+ Data band 255];
+event_io_list({smpte, DeltaTime, [Data]}) ->
+ meta_io_list(DeltaTime, ?META_SMPTE, Data);
+event_io_list({time_signature, DeltaTime, [Data]}) ->
+ meta_io_list(DeltaTime, ?META_TIME_SIG, Data);
+event_io_list({key_signature, DeltaTime, [Data]}) ->
+ meta_io_list(DeltaTime, ?META_KEY_SIG, Data);
+event_io_list({sequencer_specific, DeltaTime, [Data]}) ->
+ meta_io_list(DeltaTime, ?META_SEQUENCER_SPECIFIC, Data);
+event_io_list({unknown_meta, DeltaTime, [Type, Data]}) ->
+ meta_io_list(DeltaTime, Type, Data).
+
+meta_io_list(DeltaTime, Type, Data) when is_binary(Data) ->
+ ?DPRINT("meta_io_list (bin) type = ~p, data = ~p~n", [Type, Data]),
+ put(status, ?STATUS_META_EVENT),
+ [var_len(DeltaTime), ?STATUS_META_EVENT, Type, var_len(size(Data)), Data];
+meta_io_list(DeltaTime, Type, Data) ->
+ ?DPRINT("meta_io_list type = ~p, data = ~p~n", [Type, Data]),
+ put(status, ?STATUS_META_EVENT),
+ [var_len(DeltaTime), ?STATUS_META_EVENT, Type, var_len(length(Data)), Data].
+
+running_status(HighNibble, Chan) ->
+ RunningStatus = get(status),
+ RunningChan = get(chan),
+ if
+ RunningStatus =:= HighNibble, RunningChan =:= Chan ->
+ ?DPRINT("running status: stat = ~p, rchan = ~p~n",
+ [RunningStatus, RunningChan]),
+ [];
+ true ->
+ put(status, HighNibble),
+ put(chan, Chan),
+ (HighNibble bsl 4) + Chan
+ end.
+
+var_len(I) when I < (1 bsl 7) ->
+ <<0:1, I:7>>;
+var_len(I) when I < (1 bsl 14) ->
+ <<1:1, (I bsr 7):7, 0:1, I:7>>;
+var_len(I) when I < (1 bsl 21) ->
+ <<1:1, (I bsr 14):7, 1:1, (I bsr 7):7, 0:1, I:7>>;
+var_len(I) when I < (1 bsl 28) ->
+ <<1:1, (I bsr 21):7, 1:1, (I bsr 14):7, 1:1, (I bsr 7):7, 0:1, I:7>>;
+var_len(I) ->
+ exit("Value " ++ I ++ " is too big for a variable length number").
12 apps/midilib/src/midilib.app.src
@@ -0,0 +1,12 @@
+{application, midilib,
+ [
+ {description, ""},
+ {vsn, "1"},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib
+ ]},
+ {mod, { midilib_app, []}},
+ {env, []}
+ ]}.
74 apps/midilib/src/midilib.erl
@@ -0,0 +1,74 @@
+-module(midilib).
+-author("Jim Menard, jimm@io.com").
+-export([note_names/0, note_length/1, bpm_to_mpq/1, mpq_to_bpm/1, quantize/2,
+ note_to_string/1]).
+
+note_names() ->
+ ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"].
+
+note_length(whole) -> 4.0;
+note_length(half) -> 2.0;
+note_length(quarter) -> 1.0;
+note_length(eighth) -> 0.5;
+note_length('8th') -> 0.5;
+note_length(sixteenth) -> 0.25;
+note_length('16th') -> 0.25;
+note_length(thirtysecond) -> 0.125;
+note_length('thirty second') -> 0.125;
+note_length('32nd') -> 0.125;
+note_length(sixtyfourth) -> 0.0625;
+note_length('sixty fourth') -> 0.0625;
+note_length('64th') -> 0.0625.
+
+-define(MICROSECS_PER_MINUTE, 1000000 * 60).
+
+%% Translates beats per minute to microseconds per quarter note (beat).
+bpm_to_mpq(Bpm) ->
+ ?MICROSECS_PER_MINUTE / Bpm.
+
+%% Translates microseconds per quarter note (beat) to beats per minute.
+mpq_to_bpm(Mpq) ->
+ ?MICROSECS_PER_MINUTE / Mpq.
+
+%% Quantize a lists's event's delta times by returning a new list of events
+%% where the delta time of each is moved to the nearest multiple of Boundary.
+quantize({track, ListOfEvents}, Boundary) ->
+ quantize(ListOfEvents, Boundary);
+quantize([], _Boundary) ->
+ [];
+quantize(ListOfEvents, Boundary) ->
+ {NewListOfEvents, _} =
+ lists:mapfoldl(fun(E, BeatsFromStart) ->
+ quantized_event(E, BeatsFromStart, Boundary)
+ end,
+ 0, ListOfEvents),
+ NewListOfEvents.
+
+%% Return a tuple containing a quantized copy of Event and the beats from
+%% the start of this event before it was quantized.
+quantized_event(Event, BeatsFromStart, Boundary) ->
+ io:format("qe ~p, ~p, ~p~n", [Event, BeatsFromStart, Boundary]),
+ {Name, DeltaTime, Values} = Event,
+ Diff = (BeatsFromStart + DeltaTime) div Boundary,
+ NewDeltaTime = if
+ Diff >= Boundary / 2 ->
+ DeltaTime - Diff;
+ true ->
+ DeltaTime - Diff + Boundary
+ end,
+ {{Name, NewDeltaTime, Values}, BeatsFromStart + DeltaTime}.
+
+%% quantized_delta_time(BeatsFromStart, DeltaTime, Boundary) ->
+%% Diff = (BeatsFromStart + DeltaTime) div Boundary,
+%% NewDeltaTime = if
+%% Diff >= Boundary / 2 ->
+%% DeltaTime - Diff;
+%% true ->
+%% DeltaTime - Diff + Boundary
+%% end.
+
+%% Given a MIDI note number, return the name and octave as a string.
+note_to_string(Num) ->
+ Note = Num rem 12,
+ Octave = Num div 12,
+ lists:concat([lists:nth(Note + 1, note_names()), Octave - 1]).
16 apps/midilib/src/midilib_app.erl
@@ -0,0 +1,16 @@
+-module(midilib_app).
+
+-behaviour(application).
+
+%% Application callbacks
+-export([start/2, stop/1]).
+
+%% ===================================================================
+%% Application callbacks
+%% ===================================================================
+
+start(_StartType, _StartArgs) ->
+ midilib_sup:start_link().
+
+stop(_State) ->
+ ok.
28 apps/midilib/src/midilib_sup.erl
@@ -0,0 +1,28 @@
+
+-module(midilib_sup).
+
+-behaviour(supervisor).
+
+%% API
+-export([start_link/0]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+%% Helper macro for declaring children of supervisor
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+%% ===================================================================
+%% API functions
+%% ===================================================================
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% ===================================================================
+%% Supervisor callbacks
+%% ===================================================================
+
+init([]) ->
+ {ok, { {one_for_one, 5, 10}, []} }.
+
12 apps/riakophone/Emakefile
@@ -0,0 +1,12 @@
+{
+ [
+ "./src/*",
+ "./src/*/*",
+ "./src/*/*/*"
+ ],
+ [
+ { i, "./include" },
+ { outdir, "./ebin" },
+ debug_info
+ ]
+}.
1 apps/riakophone/ebin/.gitignore
@@ -0,0 +1 @@
+.*.beam
9 apps/riakophone/ebin/riakophone.app
@@ -0,0 +1,9 @@
+{application,riakophone,
+ [{description,[]},
+ {vsn,"1"},
+ {registered,[]},
+ {applications,[kernel,stdlib,riak_core]},
+ {mod,{riakophone_app,[]}},
+ {env,[]},
+ {modules,[riakophone,riakophone_app,riakophone_midi,
+ riakophone_sup,riakophone_utils,riakophone_vnode]}]}.
6 apps/riakophone/include/riakophone.hrl
@@ -0,0 +1,6 @@
+-define(PRINT(Var), io:format("DEBUG: ~p:~p - ~p~n~n ~p~n~n", [?MODULE, ?LINE, ??Var, Var])).
+-define(SAMPLERATE, 16000).
+-define(CHANNELS, 1).
+-record(on, {ticks, controller, note, amplitude}).
+-record(off, {ticks, controller, note, amplitude}).
+-record(note, {start, length, controller, note, amplitude}).
BIN apps/riakophone/rebar
Binary file not shown.
13 apps/riakophone/src/riakophone.app.src
@@ -0,0 +1,13 @@
+{application, riakophone,
+ [
+ {description, ""},
+ {vsn, "1"},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib,
+ riak_core
+ ]},
+ {mod, { riakophone_app, []}},
+ {env, []}
+ ]}.
81 apps/riakophone/src/riakophone.erl
@@ -0,0 +1,81 @@
+-module(riakophone).
+-export([
+ play/1,
+ stop/0,
+ stop/1
+ ]).
+
+-include("riakophone.hrl").
+
+%% Play the supplied filename.
+play(Filename) when is_list(Filename)->
+ {ok, Notes, MSPerTick} = riakophone_midi:read(Filename),
+ Pid = spawn(fun() -> play_notes(Notes, MSPerTick, 0) end),
+ %% Store our pid in process dictionary for easy stopping later.
+ case erlang:get(playing_pids) of
+ undefined -> erlang:put(playing_pids, [Pid]);
+ Pids -> erlang:put(playing_pids, [Pid|Pids])
+ end,
+ %% Return the pid.
+ Pid;
+
+%% Play the specified midi note.
+play(MidiNote) when is_integer(MidiNote) ->
+ play_note(1, MidiNote, 1, 1).
+
+stop() ->
+ case erlang:get(playing_pids) of
+ undefined ->
+ ok;
+ Pids ->
+ [stop(X) || X <- Pids]
+ end,
+ erlang:put(playing_pids, []),
+ ok.
+
+stop(Pid) ->
+ Pid ! stop.
+
+%% Private Functions
+
+play_notes([Note|Notes], MSPerTick, Ticks) ->
+ %% Delay the proper amount of ticks...
+ case Ticks < Note#note.start of
+ true ->
+ Sleep = trunc(MSPerTick * (Note#note.start - Ticks)),
+ timer:sleep(Sleep);
+ false ->
+ ok
+ end,
+
+ %% Play the next note...
+ Controller = Note#note.controller,
+ MidiNote = Note#note.note,
+ Amplitude = Note#note.amplitude,
+ Duration = (MSPerTick * Note#note.length) / 1000,
+ play_note(Controller, MidiNote, Amplitude, Duration),
+
+ %% Check if we have been stopped...
+ receive stop ->
+ stopped
+ after 0 ->
+ play_notes(Notes, MSPerTick, Note#note.start)
+ end;
+play_notes([], _, _) ->
+ ok.
+
+play_note(MidiController, MidiNote, Amplitude, Duration) ->
+ %% Route based on the MidiController/MidiNote combination...
+ ObjectName = {MidiController, MidiNote},
+ Key = chash:key_of(term_to_binary(ObjectName)),
+
+ %% Get the preflist...
+ NVal = 1,
+ PrefList = riak_core_apl:get_apl(Key, NVal, riakophone),
+
+ %% Send the play command...
+ Message = {play, MidiController, MidiNote, Amplitude, Duration},
+ riak_core_vnode_master:command(PrefList, Message, riakophone_vnode_master).
+
+
+
37 apps/riakophone/src/riakophone_app.erl
@@ -0,0 +1,37 @@
+-module(riakophone_app).
+
+-behaviour(application).
+
+%% Application callbacks
+-export([start/2, stop/1]).
+
+%% ===================================================================
+%% Application callbacks
+%% ===================================================================
+
+start(_StartType, _StartArgs) ->
+ %% Start any dependent applications.
+ riak_core_util:start_app_deps(riakophone),
+
+ %% Ensure that we can detect an audio playing method, or abort.
+ case riakophone_utils:detect_audio_method() of
+ undefined ->
+ error_logger:error_msg("~n~n~n~n~n~nERROR: Could not find method to play audio!~n~n~n~n~n~n"),
+ init:stop();
+ Method ->
+ error_logger:info_msg("Playing audio with: '~s'~n", [Method])
+ end,
+
+ %% Start the riakophone supervisor. If successful, then register
+ %% with riak_core.
+ case riakophone_sup:start_link() of
+ {ok, Pid} ->
+ riak_core:register_vnode_module(riakophone_vnode),
+ riak_core_node_watcher:service_up(riakophone, self()),
+ {ok, Pid};
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+stop(_State) ->
+ ok.
66 apps/riakophone/src/riakophone_midi.erl
@@ -0,0 +1,66 @@
+-module(riakophone_midi).
+-export([read/1]).
+-include("riakophone.hrl").
+
+read(Filename) ->
+ {seq, {header, _, Time}, GlobalTrack, Tracks} = midifile:read(Filename),
+ case <<Time:16/integer>> of
+ <<0:1/integer, TicksPerBeat:15/integer>> ->
+ ok;
+ <<1:1/integer, _:15/integer>> ->
+ throw(not_yet_supported),
+ TicksPerBeat = 0
+ end,
+ MSPerTick = calculate_ms_per_tick(TicksPerBeat, GlobalTrack),
+ Notes = transform_tracks([GlobalTrack|Tracks]),
+ {ok, Notes, MSPerTick}.
+
+calculate_ms_per_tick(TicksPerBeat, {track, TrackInfo}) ->
+ {tempo, _, [MicroSecPerQuarterNote]} = lists:keyfind(tempo, 1, TrackInfo),
+ (MicroSecPerQuarterNote * (1 / TicksPerBeat)) / 1000.
+
+transform_tracks(Tracks) ->
+ lists:sort(lists:flatten([transform_track(X) || X <- Tracks])).
+transform_track({track, Events}) ->
+ %% Filter out old events, calculate absolute times and relative velocities.
+ Events1 = transform_events1(Events, 0),
+ %% Sort by Note/Time
+ Events2 = transform_events2(Events1),
+ %% Collapse on/off into notes.
+ Events3 = transform_events3(Events2),
+ lists:sort(Events3).
+transform_events1([{OnOff, DeltaTime, [Controller, Note, Velocity]}|Rest], Ticks) when OnOff == 'on' orelse OnOff == 'off' ->
+ NewTicks = Ticks+DeltaTime,
+ NewEvent = {OnOff, NewTicks, Controller, Note, Velocity/127 * 0.8},
+ [NewEvent|transform_events1(Rest, NewTicks)];
+transform_events1([{program, _, [9, _]}|_Rest], _Ticks) ->
+ %% Don't play percussion.
+ [];
+transform_events1([_|Rest], Ticks) ->
+ transform_events1(Rest, Ticks);
+transform_events1([], _) ->
+ [].
+
+transform_events2(Events) ->
+ SortFun = fun({_, TicksA, _, NoteA, _}, {_, TicksB, _, NoteB, _}) ->
+ {NoteA, TicksA} < {NoteB, TicksB}
+ end,
+ lists:sort(SortFun, Events).
+transform_events3([On,Off|Rest]) when is_record(On, on) andalso
+ is_record(Off, off) andalso
+ On#on.note == Off#off.note andalso
+ On#on.controller == Off#off.controller ->
+ Note = #note {
+ start = On#on.ticks,
+ length = Off#off.ticks - On#on.ticks,
+ controller = On#on.controller,
+ note = On#on.note,
+ amplitude = On#on.amplitude
+ },
+ [Note|transform_events3(Rest)];
+transform_events3([_|Rest]) ->
+ transform_events3(Rest);
+transform_events3([]) ->
+ [].
+
+
31 apps/riakophone/src/riakophone_sup.erl
@@ -0,0 +1,31 @@
+
+-module(riakophone_sup).
+
+-behaviour(supervisor).
+
+%% API
+-export([start_link/0]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+%% Helper macro for declaring children of supervisor
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+%% ===================================================================
+%% API functions
+%% ===================================================================
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% ===================================================================
+%% Supervisor callbacks
+%% ===================================================================
+
+init([]) ->
+ VMaster = {riakophone_vnode_master,
+ {riak_core_vnode_master, start_link, [riakophone_vnode]},
+ permanent, 5000, worker, [riak_core_vnode_master]},
+ {ok, { {one_for_one, 5, 10}, [VMaster]} }.
+
17 apps/riakophone/src/riakophone_utils.erl
@@ -0,0 +1,17 @@
+-module(riakophone_utils).
+-export([detect_audio_method/0]).
+
+detect_audio_method() ->
+ AFPlayExists = filelib:wildcard("/usr/bin/afplay") /= [],
+ SoundDeviceExists = filelib:wildcard("/dev/sound") /= [],
+ AudioDeviceExists = filelib:wildcard("/dev/audio") /= [],
+ if
+ AFPlayExists ->
+ afplay;
+ SoundDeviceExists ->
+ sound_device;
+ AudioDeviceExists ->
+ audio_device;
+ true ->
+ undefined
+ end.
142 apps/riakophone/src/riakophone_vnode.erl
@@ -0,0 +1,142 @@
+%% This file is provided to you under the Apache License,
+%% Version 2.0 (the "License"); you may not use this file
+%% except in compliance with the License. You may obtain
+%% a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing,
+%% software distributed under the License is distributed on an
+%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%% KIND, either express or implied. See the License for the
+%% specific language governing permissions and limitations
+%% under the License.
+
+%% Copyright (c) 2007-2010 Basho Technologies, Inc. All Rights Reserved.
+
+-module(riakophone_vnode).
+-behaviour(riak_core_vnode).
+-include("riakophone.hrl").
+
+-export([start_vnode/1,
+ init/1,
+ terminate/2,
+ handle_command/3,
+ is_empty/1,
+ delete/1,
+ handle_handoff_command/3,
+ handoff_starting/2,
+ handoff_cancelled/1,
+ handoff_finished/2,
+ handle_handoff_data/2,
+ encode_handoff_item/2]).
+
+-record(state, {partition, method}).
+
+%% API
+start_vnode(I) ->
+ riak_core_vnode_master:get_vnode_pid(I, ?MODULE).
+
+init([Partition]) ->
+ %% TODO: Test for various methods of playing sounds.
+ Method = riakophone_utils:detect_audio_method(),
+ {ok, #state { partition=Partition, method=Method }}.
+
+handle_command({play, _Controller, MidiNote, Amplitude, Duration}, _Sender, State) ->
+ play(State#state.method, MidiNote, Amplitude, Duration),
+ {noreply, State};
+
+handle_command(Message, _Sender, State) ->
+ ?PRINT({unhandled_command, Message}),
+ {noreply, State}.
+
+handle_handoff_command(Message, _Sender, State) ->
+ ?PRINT({unhandled_handoff_command, Message}),
+ {noreply, State}.
+
+handoff_starting(_TargetNode, State) ->
+ {true, State}.
+
+handoff_cancelled(State) ->
+ {ok, State}.
+
+handoff_finished(_TargetNode, State) ->
+ {ok, State}.
+
+handle_handoff_data(_Data, State) ->
+ {reply, ok, State}.
+
+encode_handoff_item(_ObjectName, _ObjectValue) ->
+ <<>>.
+
+is_empty(State) ->
+ {true, State}.
+
+delete(State) ->
+ {ok, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+
+%% PRIVATE FUNCTIONS
+
+play(Method, MidiNote, Amplitude, Duration) ->
+ Filename = lists:flatten(io_lib:format("./notes/note_~w_~w_~w.au", [MidiNote, Duration, Amplitude])),
+ case filelib:is_regular(Filename) of
+ true ->
+ ok;
+ false ->
+ Data = generate_au(MidiNote, Amplitude, Duration),
+ filelib:ensure_dir(Filename),
+ file:write_file(Filename, Data)
+ end,
+ play(Method, Filename).
+
+play(afplay, Filename) ->
+ spawn(fun() -> os:cmd("afplay " ++ Filename) end).
+
+
+%% Return the bytes for an .au file of the requested duration,
+%% amplitude, and pitch.
+%% @param MidiNote is a midi note from 0 to 127.
+%% @param Amplitude from 0 to 1.0
+%% @param Duration in seconds.
+%%
+%% Info gleaned from:
+%% - http://blogs.msdn.com/b/dawate/archive/2009/06/24/intro-to-audio-programming-part-3-synthesizing-simple-wave-audio-using-c.aspx
+%% - http://en.wikipedia.org/wiki/Au_file_format
+generate_au(MidiNote, Amplitude, Duration) ->
+ %% Calculate some vars...
+ Frequency = 261 * math:pow(2, (MidiNote-60)/12.0),
+ NumSamples = trunc(?SAMPLERATE * Duration),
+ T = (math:pi() * 2 * Frequency) / ?SAMPLERATE,
+
+ %% Generate the raw PCM data
+ F = fun(X) ->
+ %% Apply a simple fade in and out of the note to make
+ %% it sound less harsh.
+ if
+ (X < NumSamples * 0.1) ->
+ Scale = (X / (NumSamples * 0.1));
+ (X > NumSamples * 0.8) ->
+ Scale = (1 - (X - NumSamples * 0.8) / (NumSamples * 0.2));
+ true ->
+ Scale = 1
+ end,
+ Value = trunc(32767 * Amplitude * Scale * math:sin(T * X)),
+ [<<Value:16/big-signed-integer>> || _ <- lists:seq(1, ?CHANNELS)]
+ end,
+ Data = iolist_to_binary([F(X) || X <- lists:seq(1, NumSamples)]),
+ Size = size(Data),
+
+ %% From
+ <<
+ ".snd", % Magic Number
+ 0024:32/unsigned-integer, % Data offset
+ Size:32/unsigned-integer, % Data size
+ 0003:32/unsigned-integer, % 16-bit linear PCM
+ ?SAMPLERATE:32/unsigned-integer, % 8000 sample rate
+ ?CHANNELS:32/unsigned-integer, % Two channels
+ Data/binary
+ >>.
BIN midi/cannon.mid
Binary file not shown.
BIN midi/canon.mid
Binary file not shown.
BIN midi/flintstone.mid
Binary file not shown.
BIN midi/mario.mid
Binary file not shown.
BIN midi/postal.mid
Binary file not shown.
BIN midi/sweden.mid
Binary file not shown.
BIN midi/usa.mid
Binary file not shown.
BIN midi/zelda.mid
Binary file not shown.
1 notes/.gitignore
@@ -0,0 +1 @@
+.*.au
BIN rebar
Binary file not shown.
17 rebar.config
@@ -0,0 +1,17 @@
+{erl_opts, [debug_info, fail_on_warning]}.
+{sub_dirs, ["apps/riakophone",
+ "apps/midilib",
+ "rel"]}.
+
+{require_otp_vsn, "R13B04|R14"}.
+
+{cover_enabled, true}.
+
+{erl_opts, [debug_info, fail_on_warning]}.
+
+%% Make cross-app includes work
+{lib_dirs, ["apps"]}.
+
+{deps, [
+ {riak_core, "0.13.0pre", {hg, "http://bitbucket.org/basho/riak_core", "tip"}}
+ ]}.

0 comments on commit 91c9a6c

Please sign in to comment.