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...

http://modarchive.org/

http://totem.fix.no/pub/mods/

http://amp.dascene.net/

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