RedMOD a .MOD file player
required ugen: [RedPhasor].
RedMOD is a class for loading and playing back .MOD files. this file format that was very popular on the Amiga tracker scene back in the 90s. .MOD files bundles samples and patterns and in addition to playing back the actual music, RedMOD lets you extract and convert this data into more sc-friendly formats.
as straight playback of .MOD files quickly becomes boring, the point of this class is rather to write custom play routines to replace the 'correct' built-in one. you can also overwrite the bundled samples and/or patterns importing your own data.
with little effort, you can do some pretty weird remixes of these songs. it will help to know rougly how trackers work and a little about how the MOD. file format functions (ticks, effects, patterns).
so for more info see...
http://en.wikipedia.org/wiki/MOD_(file_format)
http://www.ilkertemir.com/source/diger/Modplay_Routines/FMODDOC.TXT
and there are heaps of MODs for testing with...
note that there are many different tracker module formats but only plain .MOD is supported here.
thanks to Lukas Nystrand aka Mortimer Twang for supplying the demo eva.mod and quality control of this class.
and last, please acknowledge the original composers if you borrow their songs (compare software revision history). also consider creating your own MODs from scratch using for instance http://www.milkytracker.net/
*read(path)
create a RedMOD by reading and parsing a .MOD file from disk
*<>sampleRate
ntsc= 7159090.5 (default), pal= 7093789.2
it is possible to change sample rate while playing
*period2note(period)
*note2period(note)
convert between midinote (centered around 0) and amiga period
prepareForPlay(group, action)
load sample data into buffers and send synthdefs to the server
group - server group to play on. use nil for the defaultGroup
action - function to evaluate after all buffers finished loading
play(outbus, clock, quant)
start playback. outbus can be an array for routing each track
default is 0 and that means mix down all tracks to busses 0&1
if no clock or quant then an internal clock is created in <clock
pause
resume
stop
stop playback and free synths
free
stop and free all buffers
save(path)
write the mod back to disk. note that imported samples are
converted to 8363.42Hz (Amiga) samplingrate
<isPlaying
<isPaused
<isPrepared
true if server booted, samples loaded and synthdefs sent
<server
current server in use. from group passed in with .play
<outBusses
which audio busses currently in use. set when .play
eg [0, 0, 0, 0] means mix all tracks to the stereopair 0&1
[0, 2, 4, 8] play all tracks on separate stereopair busses
<clock
a tempo clock. may be passed in with .play
<>speed
get or set current speed. setting this will override all mod speed settings
revertSpeed
revert back to use the mod speed settings. ie read speed from the file
<>muteTracks
an array of track indexes. eg [0, 2] will mute playback of tracks 0&2
non tick based effects are still played though eg for setting the speed
<>index
force playback to jump to some position in the order (0 to order.size-1)
<>row
force playback to jump to some row (0 to 63)
<>tick
force playback to jump to some tick (0 to speed-1)
<>patterns
array of RedMODPattern objects. should be played in a certain order.
see also <>order below
<>samples
array of 31 RedMODSample objects. can be nil if sample slot is empty
for most MOD files not all slots are in use
<name
song name string
<>order
the arrangement of the song. ie which pattern to play in which order
<>index above steps through this array to find current pattern
<magic
id string
<format
additional information string
<numTracks
number of tracks or channels for this song. 4 is a common number
helper classes:
RedMODSlot - an event
information about which sample to play, period and effect index and parameter
RedMODPattern - a form part or section
a chunk of slots with 64 rows times x columns (usually x= 4 for 4-track)
RedMODEmptySample - an empty sound sample
no data, just keeper of the name as some mods store additional information here
RedMODSample - a sound sample
name, finetuning, volume, looppoints, sample rate and the actual sample data
it also holds a buffer object. note that the data has 8363.42Hz (Amiga) samplingrate
.ripSample(server, action, interpolate= false)
convert the data to server samplingrate and return a new buffer object
.ripSample2(server, action, interpolate= false)
same as above but also keeps finetuning, volume and looppoints
.importSoundFile(path, server, action)
read sample data from a soundfile.
remember to update name, tuning, loopstart, looplen manually for this object
.importBuffer(buffer, action)
read sample data from an existing buffer. will use buffer samplingrate.
remember to update name, tuning, loopstart, looplen manually for this object
//--ex.1
a= RedMOD.read("/Users/red/mod/lukas/eva.mod"); //modify this path!
a.prepareForPlay //boot server, load buffers, send synthdefs
a.play //start playback
a.isPlaying
a.isPaused
a.pause
a.resume
a.speed //current speed (max num ticks)
a.speed= 3 //override mod speed settings
a.revertSpeed //use mod speed settings again
a.stop
t= TempoClock(180/60)
a.play(clock:t, quant:1) //restart in sync with a clock passed in
t.tempo_(60/60)
t.tempo_(125/60) //back to default
a.free //stop and free synths and buffers
//--ex.2
a= RedMOD.read("/Users/red/mod/lukas/eva.mod");
a.prepareForPlay(action:{a.play})
a.name
a.order //indexes for song form
a.magic
a.format
a.numTracks
a.index //current form index
a.row //current row (0 - 63)
a.tick //current tick (0 - (speed-1))
a.index_(a.order.choose).row_(64.rand) //jump somewhere random
a.order_(a.order.scramble[0..1]).index_(0).row_(0) //rearrange song at random -only 2 rows long
a.order
a.order_({(a.patterns.size-1).rand}.dup(8)) //rearrange song at random -8 rows long
a.order
a.play //restart
a.muteTracks= [0, 2] //do not play the tracks 0 and 2
a.muteTracks= [1, 3] //do not play the other 2 tracks
a.muteTracks= []
RedMOD.sampleRate= 900000 //global samplerate
RedMOD.sampleRate= 1900000
RedMOD.sampleRate= 90000000
RedMOD.sampleRate= 190000000
RedMOD.sampleRate= 7093789.2 //PAL
RedMOD.sampleRate= 7159090.5 //NTSC (default)
a.free
//--ex.3
a= RedMOD.read("/Users/red/mod/lukas/eva.mod");
a.prepareForPlay(action:{a.play})
a.samples //all sample objects
~smp= a.samples.select{|x| x.empty.not} //find loaded samples -ie not empty samples
~smp[2].dump
~smp[2].buffer.plot
//swapping buffers while playing - destructive effect - modify paths
~smp[0].importSoundFile("sounds/drumkits/ErnysPercussion/2Bongo Low.wav");
~smp[1].importSoundFile("sounds/drumkits/ErnysPercussion/2Conga Slap.wav");
~smp[2].importSoundFile("sounds/drumkits/ErnysPercussion/Agogo Hi.wav");
~smp[3].importSoundFile("sounds/drumkits/ErnysPercussion/3Guiro Short.wav");
~smp[4].importSoundFile("sounds/drumkits/ErnysPercussion/1Conga Hi.wav");
//record a short sample from internal mic and import that buffer into mod sample slot
~buf= Buffer.alloc(s, 44100*0.25, 1)
~rec= {EnvGen.kr(Env.linen(0.01, 0.23, 0.01), doneAction:2)*RecordBuf.ar(AudioIn.ar([1]), ~buf.bufnum, run:1)}
~rec.play //start recording for 1/4 sec
~buf.plot
a.play //restart
~smp[2].importBuffer(~buf)
~smp[2].buffer.plot //now imported
a.muteTracks= [0, 1, 3]
a.muteTracks= []
//replace all samples with this buffer
Routine.run{~smp.do{|x| x.postln.importBuffer(~buf); 0.5.wait}}
~rec.play //record again
~buf.plot
Routine.run{~smp.do{|x| x.postln.importBuffer(~buf); 0.5.wait}}
a.save("RedMODHelp_mstrpc.mod") //save your masterpiece in sc folder
a.free
~buf.free
a= RedMOD.read("RedMODHelp_mstrpc.mod") //read it back and play
a.prepareForPlay
a.play
a.free
//--ex.4
a= RedMOD.read("/Users/red/mod/lukas/eva.mod");
a.prepareForPlay(action:{a.play})
~index= a.order[2];
~index2= a.order[4];
a.order_(~index.dup(20)) //only play first index in a loop
a.patterns[~index].slots //the 64 slots for this index 1 pattern
a.patterns[~index].slots[0][3].dump //access first slot, forth channel
a.patterns[~index].slots[0][3].period= 227 //set slot pitch (as amiga period)
//destructive
10.do{|x| a.patterns[~index].slots[x][3].period= 227} //set the 10 first pitches to same period
//swap around tracks - non destructive
a.patterns[~index].slots= a.patterns[~index].slots.collect{|x, i| x.rotate(i)}
//transpose - non destructive
a.patterns[~index].slots= a.patterns[~index].slots.collect{|x| x.collect{|y| y.period= y.period+10}}
a.patterns[~index].slots= a.patterns[~index].slots.collect{|x| x.collect{|y| y.period= y.period-10}}
RedMOD.note2period(13) //conversion
RedMOD.period2note(202)
//copy first half and paste over the second - destructive
a.patterns[~index].slots= a.patterns[~index].slots.collect{|x, i| a.patterns[~index].slots[i%32]}
//wrap around 12 slots - destructive
a.patterns[~index].slots= a.patterns[~index].slots.collect{|x, i| a.patterns[~index].slots[i%12]}
a.patterns[~index].slots= a.patterns[~index].slots.collect{|x, i| a.patterns[~index].slots[i%8]}
a.patterns[~index].slots= a.patterns[~index].slots.collect{|x, i| a.patterns[~index].slots[i%4]}
a.order_(~index2.dup(20)) //new order and jump to index2 saved above
//swap buffers around between tracks for second pattern - destructive
~smp= a.samples.select{|x| x.empty.not} //find loaded samples
a.patterns[~index2].slots= a.patterns[~index2].slots.collect{|x, i| x.collect{|y| y.sample= ~smp.foldAt(i)}}
a.free
//--ex.5
a= RedMOD.read("/Users/red/mod/lukas/eva.mod");
a.prepareForPlay
~smp= a.samples.select{|x| x.empty.not} //find loaded samples
~smp[2].buffer.plot
b= ~smp[2].ripSample
b.plot
b.play
b.sampleRate //converted to server samplerate
b.write("RedMODHelp_rippedSample.aiff") //write soundfile to sc folder
b.free
a.free
//--ex.6
a= RedMOD.read("/Users/red/mod/lukas/eva.mod");
a.prepareForPlay
( //custom play routine - no effects and just barebone playback - sandbox
var clock= TempoClock(125/60);
Routine{
var speed= 6, index= 0, row= 0, tick= 0; //counters
var mute= [];
var tracks; //current slots (usually 4)
var filter= Synth(\redMODfilter); //amiga filer wannabe
var synths= {Synth(\redMODplayer)}.dup(a.numTracks);//one synth per track (usually 4)
var trackLoopStart= 0.dup(a.numTracks);
var trackLoopEnd= 0.dup(a.numTracks);
var trackVolume= 0.dup(a.numTracks);
var trackPeriod= 0.dup(a.numTracks); //amiga periods
while({index<a.order.size and:{a.isPrepared}}, { //loop ticks
if(tick==0, { //slots update every 0 tick, rest sub efxs
tracks= a.patterns[a.order[index]].slots[row];
tracks.do{|slot, i| //for each track (usually 4)
var trackSynth= synths[i]; //find corresponding synth object
s.makeBundle(nil, {
if(mute.includes(i).not, {
//--update synth if sample given
if(slot.sample.notNil and: {slot.sample.empty.not}, {
trackSynth.set(\bufnum, slot.sample.buffer.bufnum);
trackSynth.set(\sr, slot.sample.sampleRate);
trackSynth.set(\loop, slot.sample.loop.binaryValue);
trackLoopStart[i]= slot.sample.loopstart;
trackSynth.set(\loopstart, trackLoopStart[i]);
trackLoopEnd[i]= slot.sample.loopend;
trackSynth.set(\loopend, slot.sample.loopend);
trackVolume[i]= slot.sample.volume;
});
if(slot.period>0, { //update track period if note given
if(slot.sample.notNil, {
trackPeriod[i]= slot.tunedPeriod;
trackSynth.set(\rate, slot.tunedNote.midiratio);
}, {
trackPeriod[i]= slot.period;
trackSynth.set(\rate, slot.note.midiratio);
});
});
});
if(mute.includes(i).not, {
trackSynth.set(\vol, trackVolume[i].clip(0, 64)/64);
if(slot.period>0, { //trigger synth if note given
trackSynth.set(\t_trig, 1);
});
});
}); //end makeBundle
};
if(row==63, { //check if time to jump in order
index= index+1;
});
row= (row+1)%64; //jump to next row
tick= 1;
}, { //update per tick (except tick 0)
tick= tick+1%speed;
});
(1/4/6).wait;
});
1.wait; //wait 1 beat before freeing synths
synths.do{|x| x.free};
filter.free;
}.play(clock);
)
a.free
//--ex.7
a= RedMOD.read("/Users/red/mod/lukas/eva.mod");
a.prepareForPlay
( //severely hacked play routine
var clock= TempoClock(200/60);
Routine{
var speed= 6, index= 0, row= 0, tick= 0;
var tracks;
var filter= Synth(\redMODfilter);
var delay= SynthDef(\redDel, {Out.ar(0, CombN.ar(InFeedback.ar(10, 2), 0.2, 0.2, 4))}).play(s);
var synths= {Synth(\redMODplayer)}.dup(a.numTracks);
var trackVolume= 0.dup(a.numTracks);
while({index<a.order.size and:{a.isPrepared}}, {
//hack:
if(tick==(index%2), {
tracks= a.patterns[a.order[index]].slots[row];
tracks.do{|slot, i|
var trackSynth= synths[i];
s.makeBundle(nil, {
if(slot.sample.notNil and:{slot.sample.empty.not}, {
//hack: add delay effect to all sample index 5
if(a.samples.indexOf(slot.sample)==5, {
trackSynth.set(\out, 10);
});
trackSynth.set(\bufnum, slot.sample.buffer.bufnum);
trackSynth.set(\sr, slot.sample.sampleRate);
trackSynth.set(\loop, slot.sample.loop.binaryValue);
trackSynth.set(\loopstart, slot.sample.loopstart);
trackSynth.set(\loopend, slot.sample.loopend);
trackVolume[i]= slot.sample.volume;
});
if(slot.period>0, {
if(slot.sample.notNil, {
//hack: index controls how much filter
filter.set(\mix, index/a.order.size);
trackSynth.set(\rate, slot.tunedNote.midiratio);
}, {
trackSynth.set(\rate, slot.note.midiratio);
});
});
trackSynth.set(\vol, trackVolume[i].clip(0, 64)/64);
//hack: always trigger everything
trackSynth.set(\t_trig, 1);
});
};
//hack: sometimes jump
if(row==63 or:{row==(63-index)}, {
index= index+1;
});
row= (row+1)%64;
tick= 1;
}, {
tick= tick+1%speed;
});
(1/4/6).wait;
});
1.wait;
synths.do{|x| x.free};
filter.free;
}.play(clock);
)
a.free
//--ex.8
a= RedMOD.read("/Users/red/mod/lukas/eva.mod");
a.prepareForPlay
a.play
//reverse all sample data while playing
~smp= a.samples.select{|x| x.empty.not} //find loaded samples
~smp.do{|x| x.data= x.data.reverse; x.load(s)};
//shift all sample data 50% while playing (rotate buffers9
~smp.do{|x| x.data= x.data.rotate(x.data.size.div(2)); x.load(s)};
//distort
~smp.do{|x| x.data= (x.data*6).softclip; x.load(s)};
//zero out after 500samples
~smp.do{|x| x.data= x.data.collect{|y, j| y*(j<500).binaryValue}; x.load(s)};
//fill with 100 random values
~smp.do{|x| x.data= {1.0.rand2}.dup(100); x.load(s)};
a.free
//--ex.9
a= RedMOD.read("/Users/red/mod/lukas/eva.mod");
a.prepareForPlay
a.play
//shift data around between samples
~smp= a.samples.select{|x| x.empty.not} //find loaded samples
~smp.do{|x, i| x.data= ~smp.wrapAt(i+1).data.copy; x.load(s)}
//copy data randomly between samples
~smp.do{|x| x.data= ~smp.choose.data.copy; x.load(s)}
//fill with wave
~smp.do{|x| x.data= Signal.sineFill(x.data.size).chebyFill(#[1, 0.2, 3]); x.load(s)}
~smp[0].buffer.plot
a.free
//--ex.10
a= RedMOD.read("/Users/red/mod/lukas/eva.mod");
a.prepareForPlay
a.play
//shift data independently around between samples with an offset in samples
(
var offset= 500;
var smp= a.samples.select{|x| x.empty.not}; //find loaded samples
var alldata= smp.collect{|x| x.data}.flat;
var allpositions= [0]++smp.collect{|x| x.data.size}.integrate;
smp.do{|x, i| x.data= alldata.copyRange(allpositions[i]+offset, allpositions[i+1]-1+offset); x.load(s)}
)
a.free
//--ex.11
a= RedMOD.read("/Users/red/mod/lukas/eva.mod");
a.prepareForPlay
//create 2 stereo effects
(
SynthDef(\ring, {|out= 0, in= 10, freq= 400|
var i= InFeedback.ar(in, 2);
var z= i*SinOsc.ar(freq, 0, 1);
Out.ar(out, z);
}).send(s);
SynthDef(\dist, {|out= 0, in= 12, dist= 10|
var i= InFeedback.ar(in, 2);
var z= Clip.ar(i*dist, -1, 1);
Out.ar(out, z);
}).send(s);
)
//start the effects on busses 10 and 12
b= Synth(\ring, #[\out, 0, \in, 10, \freq, 1600]);
c= Synth(\dist, #[\out, 0, \in, 12, \dist, 10]);
//start playing the mod with ringmodulation on channel 1
a.play([10, 0, 0, 0])
b.set(\freq, 2600)
a.stop
//distort channel 2 and 4
a.play([0, 12, 0, 12])
a.stop
//effects on all channels
a.play([12, 10, 10, 12])
a.stop
a.free
b.free
c.free