Tuesday, February 10, 2009

Simple Drum Machine

After shamelessly flashing my Moog synth, I think it's time to reveal another SuperCollider creation. A very simple drum machine, made up of two parts. A SynthDef, defining my drums and a pattern template to play them. 

Here's the SynthDef 

(
SynthDef(\drums, {|out = 0, bassLevel = 0 , snareLevel = 0, hatLevel = 0, tomLevel = 0, pan1 = 0, pan2 = 0, pan3 = 0, pan4 = 0|

 var env1, env2, env3, env4, bass, snare, hat, tom, bassOut, snareOut, hatOut, tomOut, mixer; 
 env1 = EnvGen.kr(Env.perc(0.001, 0.2, 1, -4), 1, doneAction:2);
 env2 = EnvGen.kr(Env.perc(0.001, 0.5, 1, -1), 1, doneAction:2);
 env3 = EnvGen.kr(Env.perc(0.002, 0.3, 1, -2), 1, doneAction:2);
 env4 = EnvGen.kr(Env.perc(0.001, 0.1, 1, -5), 1, doneAction:2); 
 

bass = SinOsc.ar(80) + Crackle.ar(1, 0.5);
 bassOut = Pan2.ar(bass*env1, pan1, bassLevel);
 
 snare = SinOsc.ar(120) - WhiteNoise.ar(0.5, 0.5);
 snareOut = Pan2.ar(snare*env4, pan2, snareLevel); 
 
 hat = Klank.ar(`[ [ 6563, 9875 ],
  [ 0.6, 0.5 ],
  [ 0.002, 0.003] ], PinkNoise.ar(1)); 
 hatOut = Pan2.ar(hat*env3, pan2, hatLevel); 
 
 tom = SinOsc.ar(440); 
 tomOut = Pan2.ar(tom*env4, pan4, tomLevel); 
 
 mixer = Mix.new([bassOut, snareOut, hatOut, tomOut]); 
 
 
 Out.ar(out, mixer);
 
 }).store
 
 )

I have four sound sources, four envelopes and four panners.

I have to admit that the drum sounds are a bit crappy and need some more work, they've got a certain Casio feel to them, and not in a good way, but they do the job.

After some experimentation I've discovered that pretty much anything sounds something like a drum if you put it through a percussive envelope, but so far the best results seem to come from some sort of noise source and/or a SinOsc. 

The bass drum is a fairly low frequency sine with a bit of crackle added to it at a low level.

The snare is a sine with some white noise subtracted from it, I don't fully understand this yet, but subtracting a white noise source from the sine sounded better than adding it. 

A wild guess lead me to the klank UGen in creating the hat( I think it might be a felt one)  and the tom is just a simple SinOsc. 

All these sound sources are controlled with percussive envelopes, each with slightly varying attack and release times. 

These are then passed to a two channel panner, which apart from allowing me to control the level of the sound also allows me to change the position of the sound between two channels. 

   Here's the pattern I use to control it. 
 

(
   
   
  

 a = Pseq ([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]);
 b = Pseq ([0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0]);
 c = Pseq ([0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0]);
 d = Pseq ([0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]); 
 
 
 p = Pbind(
  \instrument, \drums,
  \dur, 0.12,
  \bassLevel, Pseq ([a], inf), 
  \snareLevel, Pseq ([b], inf),
  \hatLevel, Pseq ([c], inf),  
  \tomLevel, Pseq ([d], inf)
   
  ).play;
   
   
  )  

The args passed to my SynthDef include levels for each of the drums. Sequencing these using a pattern seemed like the easiest way of triggering them. Each value of the pattern can be set at either 0 for no drum or >0 to trigger a drum sound. This might not be the most efficient way of doing things but it does work.

I still don't understand exactly how it works, but the /dur argument sets the tempo, I think each event is timed as a fraction of the default clock setting of 1, but I could be wrong. 

You can do all kinds of great things with patterns, above I'm nesting them rather than defining them directly which will allow me to more easily create complex drum sequences. If I want to compose a song I can define more patterns and sequence them with the main pattern. 

(
   
   
  

 a = Pseq ([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]);
 b = Pseq ([0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0]);
 c = Pseq ([0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0]);
 d = Pseq ([0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]); 
 e = Pseq ([0, 0,  1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1]);
 
 p = Pbind(
  \instrument, \drums,
  \dur, 0.12,
  \bassLevel, Pseq ([a], inf), 
  \snareLevel, Pseq ([b, e], inf),
  \hatLevel, Pseq ([c], inf),  
  \tomLevel, Pseq ([d], inf)
   
  ).play;
   
   
  )  

Like that. 



5 comments:

  1. Hm, actually I disagree pretty strongly with the approach here.

    I find synthesis and sequencing work best when synths are *modular* -- that is, when a SynthDef does one and only one job. Putting all of your drums in one SynthDef is inefficient -- if you need only the snare drum for part of the track, the big-SynthDef approach has to calculate audio for all of the drums, and only silence the ones that you don't hear.

    If you had one def for the kick drum, another for snare, another for hats and so on, then you can play only the drums you need. This will keep the UGen count down and save CPU.

    Another problem:

    env1 = EnvGen.kr(Env.perc(0.001, 0.2, 1, -4), 1, doneAction:2);
    env2 = EnvGen.kr(Env.perc(0.001, 0.5, 1, -1), 1, doneAction:2);
    env3 = EnvGen.kr(Env.perc(0.002, 0.3, 1, -2), 1, doneAction:2);
    env4 = EnvGen.kr(Env.perc(0.001, 0.1, 1, -5), 1, doneAction:2);

    Whichever of these envelopes reaches the end first will cut off the entire synth -- and, since you haven't separated the sounds into different SynthDefs/synth nodes, the shortest sound will cut off longer ones! That is, you want the snare drum to be 0.5 seconds, but env4 will make sure that your snare drum is never longer than 0.1 seconds.

    I realize, after four years, this is kind of a necrobump -- but, somebody just found this example online and then e-mailed me privately to ask me about "my" code and how to convert it to use drum samples. So I thought it would be worth documenting a more modular, adaptable, easier-to-use approach.

    (
    SynthDef(\kick, {|out = 0, amp = 0, pan|
    var env, bass;
    env = EnvGen.kr(Env.perc(0.001, 0.2, 1, -4), 1, doneAction:2);
    bass = SinOsc.ar(80) + Crackle.ar(1, 0.5);
    Out.ar(out, Pan2.ar(bass*env, pan, amp));
    }).add;

    SynthDef(\snare, {|out = 0, amp = 0, pan|
    var env, snare;
    env = EnvGen.kr(Env.perc(0.001, 0.1, 1, -5), 1, doneAction:2);
    snare = SinOsc.ar(120) - WhiteNoise.ar(0.5, 0.5);
    Out.ar(out, Pan2.ar(snare*env, pan, amp));
    }).add;

    SynthDef(\hat, {|out = 0, amp = 0, pan|
    var env, hat;
    env = EnvGen.kr(Env.perc(0.002, 0.3, 1, -2), 1, doneAction:2);
    hat = Klank.ar(`[ [ 6563, 9875 ],
    [ 0.6, 0.5 ],
    [ 0.002, 0.003] ], PinkNoise.ar(1));
    Out.ar(out, Pan2.ar(hat*env, pan, amp));
    }).add;

    SynthDef(\tom, {|out = 0, amp = 0, pan|
    var env, tom;
    env = EnvGen.kr(Env.perc(0.001, 0.1, 1, -5), 1, doneAction:2);
    tom = SinOsc.ar(440);
    Out.ar(out, Pan2.ar(tom*env, pan, amp));
    }).add;
    )

    (
    a = Pseq ([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]);
    b = Pseq ([0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0]);
    c = Pseq ([0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0]);
    d = Pseq ([0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]);


    p = Ppar(
    [a, b, c, d].collect { |pattern, i|
    Pbind(
    \instrument, [\kick, \snare, \hat, \tom].at(i),
    \dur, 0.30,
    \amp, 0.1,
    \noteOrRest, Pif(pattern > 0, 1, Rest)
    )
    }
    ).play;
    )

    ReplyDelete
  2. @jamshark70, if i want it to loop indefinitely and mute/play stuff without it stopping? how would i change your code?

    ReplyDelete
  3. if i want it to loop indefinitely and mute stuff on the fly? what then, @jamshark70?

    ReplyDelete
    Replies
    1. If you want to loop indefinitely you can add ,inf to the Pseq's after the sequence. This tells it to run infinitely. A pattern is an event stream player so you can add a quant value after play and just reevaluate the code with the changes in place.

      Delete
    2. If you want to loop indefinitely you can add ,inf to the Pseq's after the sequence. This tells it to run infinitely. A pattern is an event stream player so you can add a quant value after play and just reevaluate the code with the changes in place.

      Delete