Tutorial - How to implement new channel

Implementing New Channel - A Rudimentary Drum Machine

Objective: Demonstrate how easy is it to expand the functionality using the framework, showing examples of how to:

  • add a new channel with any numeric code we want
  • enable or disable standard features that are available, to borrow some functionality from other channels
  • upload arbitrary data into FLASH memory and access it, this is handy for lot of other things too
  • decode binary 16-bit PCM sample data from a standard WAV format
  • play multiple instruments in parallel, from FLASH memory with zero waiting state and no need for buffering in SRAM
  • bind this new functionality to existing controls
  • mix the signal output from our custom function into the loop with or without delay/echo

Requirements for any Gecho firmware programming

  • The Board: Gecho Loopsynth
  • Programming IDE, recommended: Atollic True Studio
  • Framework Source Code, TODO: will be on GIT after I clean it up and add lots of comments
  • SWD Programming Adapter, if you don't have one, ST-Link V2 is great
  • STM32 ST-LINK Utility: STSW-LINK004
  • Warning: there are more versions, the pinout differs, but they are clearly labelled. It is only 4 wires, so hard to make a mistake.

Get Started

Let's assume you got the IDE installed, board connected to SWD and you tried the flashing utility to see that it communicates well.

Download some instrument samples, for example I used some form this great collection SampleRadar: 1,000 free drum samples

Select four samples. I selected these:

  • CYCdh_K2room_Snr-05.wav
  • CyCdh_K3HfHat.wav
  • CyCdh_K3Kick-01.wav
  • CyCdh_K3OpHat-02.wav

Convert them down to 22kHz, 16-bit, mono using your favourite editing software.

Remove headers, so we have raw data (if you don't, some glitches might be heard).

Note down file sizes and merge all files into one (append one after another).

Upload the bin file to a location in memory, where there is free space, using ST-Link utility (detailed steps here).

The Code

To add new channel, open custom_program.cpp, find function "void custom_program_init(uint64_t prog)" and add new channel definition towards function's end. The channel number can be anything that is not yet allocated in that function.

Note: What we call a "sample" in the code, is actually one 16-bit wide piece of the instrument's sample data, not the entire recording of it.

if(prog == 11223) //drum machine
{
selected_song = 2; //you see we are choosing from pre-defined songs,
//but if you typed 222 here, last user-defined song will play
PROG_drum_machine = true;
PROG_enable_jamming = true;
PROG_enable_chord_loop = true;
PROG_enable_LED_indicators = true;
PROG_add_echo = true; //false;
PROG_enable_filters = true;
PROG_add_plain_noise = true;
PROG_enable_S1_control_noise_boost = true;
PROG_enable_S2_control_noise_attenuation = true; //only when jamming active
PROG_enable_S3_control_resonance = true;
PROG_enable_S4_control_arpeggiator = true; //by default all song channels do have arpeggiator
}

Add some definitions into file custom_program.cpp (e.g. at the end of file)

#define DRUM_BASE_ADDR1 0x080A8000 //base address where we uploaded samples
#define DRUM_LENGTH1 17416 //length of first sample in bytes
#define DRUM_BASE_ADDR2 (DRUM_BASE_ADDR1 + DRUM_LENGTH1)
#define DRUM_LENGTH2 39104
#define DRUM_BASE_ADDR3 (DRUM_BASE_ADDR2 + DRUM_LENGTH2)
#define DRUM_LENGTH3 12416
#define DRUM_BASE_ADDR4 (DRUM_BASE_ADDR3 + DRUM_LENGTH3)
#define DRUM_LENGTH4 68310

The base address is not really important, just find enough space somewhere in upper memory range to fit all samples

Few more definitions and variables

#define DRUM_SAMPLES 4 //we uploaded 4 samples to FLASH memory
#define DRUM_CHANNELS_MAX 4 //no real polyphony for now, just one of each samples at the time
int drum_samples_ptr[DRUM_CHANNELS_MAX]; //pointers for currently playing sample
int drum_trigger[DRUM_CHANNELS_MAX]; //triggers for execution of each drum
int drum_lengths[DRUM_SAMPLES]; //lengths of samples in bytes
int drum_bases[DRUM_SAMPLES]; //base addresses of samples
int drum_machine_init = 0; //has drum machine been initialized?
float drum_machine_return_sample = 0; //variable to mix all samples into one value
#define DRUM_SENSOR_THRESHOLD 300 //how far from the sensor to flip trigger
#define DRUM_SAMPLE_VOLUME 0.19f //general adjustment volume for samples we used

Now, the new function. It needs to return the sample (which will later be mixed into the "loop" and from outside world, it takes global static variable sampleCounter. This is very useful, it helps to determine whether current sample is left or right channel, and also it can be used to count milliseconds for various timing purposes.

uint16_t drum_machine_process() //returns 16-bit sample
{

Using the "drum_machine_init" variable, we will know whether the state machine has been already initialized. If not, some variables need cleaning or setting.

if(!drum_machine_init)
{
drum_machine_init = 1;
for(int i=0;i<DRUM_CHANNELS_MAX;i++) //init all values
{
drum_samples_ptr[i] = -1; //reset to "stopped" state
drum_trigger[i] = 0; //reset to "released" state
}
drum_lengths[0] = DRUM_LENGTH1 / 2; //each sample is 2 bytes
drum_lengths[1] = DRUM_LENGTH2 / 2;
drum_lengths[2] = DRUM_LENGTH3 / 2;
drum_lengths[3] = DRUM_LENGTH4 / 2;
drum_bases[0] = DRUM_BASE_ADDR1; //copy the values to variables...
drum_bases[1] = DRUM_BASE_ADDR2; //...for easy access in "for" loops
drum_bases[2] = DRUM_BASE_ADDR3;
drum_bases[3] = DRUM_BASE_ADDR4;
}

Now, let's go throught all four channels, check state of sensor and if threshold crossed, flip the trigger variable.

for(int i=0;i<DRUM_CHANNELS_MAX;i++)
{
//if not playing and IR Sensor threshold detected, start playing
if((ADC_last_result[i] > DRUM_SENSOR_THRESHOLD) && !drum_trigger[i])
{
drum_trigger[i] = 1; //flip the trigger
drum_samples_ptr[i] = 0; //set pointer to the beginning of the sample
}
//if playing and IR Sensor reports finger moved away, allow restart
if((ADC_last_result[i] < DRUM_SENSOR_THRESHOLD) && drum_trigger[i])
{
drum_trigger[i] = 0; //release trigger
}
}

Since our samples are mono, we only need to care about every other sample that we are sending to the codec.

if(sampleCounter%2==0) //calculate the value only on each "left" sample
{
drum_machine_return_sample = 0; //clear the previous value
for(int i=0;i<DRUM_CHANNELS_MAX;i++)
{
if(drum_samples_ptr[i] >= 0) //if playing
{
//translate from 16-bit binary format to float
drum_machine_return_sample +=
((float)((int16_t*)(drum_bases[i]))[drum_samples_ptr[i]])
* DRUM_SAMPLE_VOLUME;
drum_samples_ptr[i]++; //move on to the next sample
if(drum_samples_ptr[i]==drum_lengths[i]) //if reached the end of sample
{
drum_samples_ptr[i] = -1; //reset the pointer to "stopped" state
}
}
}
}
else //return previous value for the other channel
{
//nothing needs to be done
}

Finally, function needs to return samples from all channels mixed together in one variable. SAMPLE_VOLUME_BOOST is an external variable which controls global mixing volume.

return (int16_t)(drum_machine_return_sample * SAMPLE_VOLUME_BOOST); //return sample
}

Curious how it sounds?

In this recording, it was the first time I used this new function, so please excuse quality of the performance :) Hope it gives you an idea about how much fun is it to play with - mainly if you programmed it there yourself, following the tutorial.

 

OK, it's not really a "drum machine", sequencer is missing - but, at least you know where to go from here :)