Skip to content

Example : Creating a simple drum machine

Igor Zinken edited this page Jan 19, 2023 · 13 revisions

Setup for this example

For all explanations on this page, we'll assume we're working with a sample rate of 44.1 kHz (although note that all examples on this page have been written to take the actual device sample rate into account). In our example we'd like to create a loop consisting of 8 measures in 4/4 time at 100 BPM. For simplicity we are creating a sixteen step sequencer, so a full measure is subdivided into 16 steps. We'll refer to this using constant:

int MEASURE_SUBDIVISIONS = 16;

If you desire more precision (such as for working with 64th notes, you can change this number accordingly).

This example also assumes you're familiar with loading audio content into the SampleManager and SampleEvents.

Setting up the Sequencer using SequencerController

First up we instruct the sequencer that it should loop through 8 measures, run at a tempo of 100 BPM and we are using 4/4 time.

_sequencerControllerInstance->setTempo( 100, 4, 4 );
_sequencerControllerInstance->updateMeasures( 8, MEASURE_SUBDIVISIONS );

What has happened now is that the engine's Sequencer will adjust its tempo to 100 BPM at the time signature of 4/4 upon the next iteration of the render loop (in case the engine hasn't been running yet, you can use its "setTempoNow"-method instead). The reason for this delayed approach is that it ensures it is possible to change tempo and time signature while running.

The "updateMeasures"-invocation will tell the sequencer to work between a range of 0 to 846720 samples. (A single bar of 4/4 time at 100 BPM at 44.1 kHz will hold 105840 samples (also see BufferUtility::getSamplesPerBar-convenience method for this calculation).

The sample start offset of the first note of the second bar would be at position 105840. Now these numbers might get quite large, but luckily this isn't an issue for the processor, it is just cumbersome during development having to think that the position of a note is relative to the start of the song/loop. So for these purposes you might resort to have a convenience method which allows you to translate musically timed concepts into engine buffer offsets:

Java:

void enqueueSample( SampleEvent sample, int startMeasure, int offset )
{
    final int samplesPerBar = sequencerController.getSamplesPerBar(); // will always match current tempo, time sig at right sample rate
  
    int startOffset = samplesPerBar * startMeasure; // is start of given measure
    startOffset   += ( samplesPerBar / MEASURE_SUBDIVISIONS ) * offset;

    sample.setEventStart( startOffset );
    sample.setEventEnd( startOffset + sample.getSampleLength() );
}

C++:

void enqueueSample( SampleEvent* sample, int startMeasure, int offset )
{
    int samplesPerBar = AudioEngine::samples_per_bar; // will always match current tempo, time sig at right sample rate
  
    int startOffset = samplesPerBar * startMeasure; // is start of given measure
    startOffset   += ( samplesPerBar / MEASURE_SUBDIVISIONS ) * offset;

    sample->setEventStart( startOffset );
    sample->setEventEnd( startOffset + sample->getSampleLength() );
}

Where the use case is that you pass in the SampleEvent you'd like to work with (loaded with a valid AudioBuffer sample and with a valid sampleLength-value!), the measure in which the SampleEvent should be played (note that the first measure starts at 0, just think of it as an Array index) and where offset is a bar subdivision relative to MEASURE_SUBDIVISIONS, stating the offset of the note within the given measure (where 0 is the first note). So for a single bar subdivided into 16th notes, the strong beats occur on indices 0, 4, 8 and 12 :

0 1 2 3 | 4 5 6 7 | 8 9 10 11 | 12 13 14 15

By having set MEASURE_SUBDIVISIONS to be 16 (as mentioned above) valid values for "offset" in above list are between 0 to 15 (sixteen minus 1).

So, assuming you wish to queue a SampleEvent to play on the second beat of the third measure, you would invoke the above method like so:

enqueueSample( sampleEvent, 2, 4 );

Once more : first argument is the SampleEvent, the two indicates the third measure ( remember to subtract 1 ), the 4 indicates the second beat.

Playing a simple beat

We'll assume we have a single measure pattern that goes :

KICK x x x | KICK x SNARE x | KICK x x x | KICK x SNARE x |

where x is silence. Note we don't have to consciously think about silence (i.e. enqueuing a "silent buffer"), we simply enqueue nothing at all at that point.

Let's assume we have SampleEvents (holding a reference to a kick sample inside the SampleManager) called kickEvent1, kickEvent2, kickEvent3 and kickEvent4 and two SampleEvents (holding a reference to a snare sample inside the SampleManager) called snareEvent1 and snareEvent2.

Note all references to samples inside the SampleManager can be shared. For instance : if you want all the kickEvents to sound the same, they can all point to the same sample (via the same unique identifier) inside the SampleManager. You don't have to reload the same sample under a different identifier, this will just cause unnecessary memory consumption!

Let's first enqueue all the kick drums:

enqueueSample( kickEvent1, 0, 0 );  // first measure, first beat
enqueueSample( kickEvent2, 0, 4 );  // first measure, second beat
enqueueSample( kickEvent3, 0, 8 );  // first measure, third beat
enqueueSample( kickEvent3, 0, 12 ); // first measure, fourth beat

and now the snare drums:

enqueueSample( snareEvent1, 0, 6 );  // first measure, seventh sixteenth note
enqueueSample( snareEvent2, 0, 14 ); // first measure, fifteenth sixteenth note

Et voila. We now have a sequencer looping through eight measures, playing a simple drum pattern during the first measure ;)

Clone this wiki locally