Coding a sampler

So, I’ve written a multi-timbral synth (OscPocketD/Synth - portable beat/song-making studio with synths and drum machine! - #13 by StaffanMelin) and an multi-fx unit (OscPocketD/FX - Multi FX device - #2 by StaffanMelin).

Next on the list is a sampler and I am not sure where to start.

Goal is two different modes:

  1. different sounds layout across a keyboard (CV and/or MIDI control)
  2. one sound that can be played (ie change the pitch)

I’ve looked at the Daisy examples, and DaisySP/libDaisy but I can’t find any classes to show me the direction. So I am asking the community for help! :slight_smile:

I’ll start out with basic sampling through audio in and tuned playback. How to change the pitch? Run the audio in through the PitchShifter class? Or changing the rate of playback and then doing interpolation?

Thanks for any pointers!

Staffan

1 Like

So a float buffer is a good place to start with sampling.
For the pitch controls you could run it through PitchShifter.
Imo a better way is to just slow down the rate at which you’re reading from the buffer.
To get it to sound smooth you’ll have to do some interpolation. Linear interpolation is easiest.

If you read over how delayline works I think you’ll find that really helpful. It’s all about writing and reading from a buffer at different speeds.
The looper examples (petal, pod, etc.) might be of some help as well.

1 Like

Thanks a lot @ben_serge! I am looking at the DelayLine class right now, it even has the Hermite interpolation built in! :slight_smile:

@ben_serge, I don’t understand what you mean by “at different speeds”? Could you elaborate, thanks! :slight_smile:

EDIT: Ah, varying the delay time is (almost) the same as varying the playback speed (ie the pitch).

So this will be the get-next-sample code inspired by DelayLine:

// sample data in buffer, pitch is wanted pitch in Hz

factor = (pitch / 440.0f);
index += factor
index_int = static_cast<int32_t>(index);
index_fraction = index - index_int;
// get sample
float a = buffer[index_int];
float b = buffer[index_int + 1];
out = a + (b - a) * index_fraction;

(Pseudo-code) I will test this and later on improve the interpolation.

It works! :slight_smile:

I let an Oscillator fill up a buffer instead of sampling something – that is for the next step. But this short piece uses the Metro class to loop through 9 different frequencies playing (part of) the same buffer. It uses a simple linear interpolation and the technique borrowed from the DelayLine class.

Demo code:

// OscPocketD/Sampler
// Staffan Melin 2021

#include "daisysp.h"
#include "daisy_seed.h"



#define MAX_SIZE (48000 * 60 * 1) // 1 minute of floats at 48 khz

using namespace daisysp;
using namespace daisy;



static DaisySeed hardware;
float sysSampleRate;
float sysCallbackRate;

static Metro tick;



// sampler
float sIndex; // index into buffer
int32_t sIndexInt;
float sIndexFraction;
float sFactor; // how much to advance index for a new sample
float sPitch; // in Hz



float DSY_SDRAM_BSS buffer[MAX_SIZE];

void fillBuffer()
{
	Oscillator osc;

    osc.Init(sysSampleRate);
    osc.SetWaveform(osc.WAVE_TRI);
	osc.SetFreq(440.0f);
	osc.SetAmp(0.1f);

	for (uint32_t i = 0; i < MAX_SIZE; i++)
	{
		buffer[i] = osc.Process();	
	}


}



float sNotes[9] = {261.63f, 293.67f, 329.63, 349.23f, 392.00f, 440.00f, 493.88f, 523.25f, 587.33f};
int sNotesIndex = 0;

static void AudioCallback(float *in, float *out, size_t size)
{
    float a, b;
    float sOut;

    for (size_t i = 0; i < size; i += 2)
    {
        // When the Metro ticks:
        // trigger the envelope to start, and change freq of oscillator.
        if (tick.Process())
        {
        
        	// sIndex = 0;
        
			// change frequency
			sNotesIndex++;
			if (sNotesIndex > 8)
				sNotesIndex = 0;

			sFactor = (sNotes[sNotesIndex] / 440.0f);
        }

		sIndex += sFactor;
		sIndexInt = static_cast<int32_t>(sIndex);
		sIndexFraction = sIndex - sIndexInt;

		// get sample
		a = buffer[sIndexInt];
		b = buffer[sIndexInt + 1];
		sOut = a + (b - a) * sIndexFraction;

		if (sIndex > (48000 * 10))
		{
			sIndex = 0;
		}

        out[i]  = sOut;
        out[i + 1] = sOut;
    }
}



int main(void)
{
    // initialize seed hardware and daisysp modules
    hardware.Configure();
    hardware.Init();
	sysSampleRate = hardware.AudioSampleRate();
	sysCallbackRate = hardware.AudioCallbackRate();



	// create fake sample data
	fillBuffer();

	
	
	// sampler - setup
	sIndex = 0.0f;
	sPitch = 440.0f;
	sFactor = (sPitch / 440.0f);



    // Set up Metro to pulse every second
    tick.Init(1.0f, sysSampleRate);



    // start callback
    hardware.StartAudio(AudioCallback);


	// loop forever
	for(;;)
	{
		// wait
		System::Delay(25);
	}
}
1 Like

Things are moving forward!

I can now sample up to 1 minute in stereo and play it back at different pitches with a loop region.

In addition I have a working
envelope (ADSR)
SVF filter (seletcable hi, low, bandpass)
delay
reverb

Many parameters can be controlled from 3 built in LFOs as well as CV ins.

I hope to be able to publish a short video demo in a couple of days, as well as the source code.

2 Likes

Here is a short demo: https://youtu.be/6dQN-wGDRbI

I’ve recorded a short clip from my computer and I play it back using a keyboard with CG/GATE out. The last note is sustained to show that the looping works!

It has some effects, in this demo the envelope/ADSR, delay and reverb are active. The filter is taking a rest! :slight_smile:

The pitch and gate of the sampler is modulated/controlled by two CV in’s.

I’ll work on the source for a couple of more days as well as the documentation, then I’ll share it under a GPL license. The hardware is the same as for the OscPocketD/Synth (OscPocketD/Synth - portable beat/song-making studio with synths and drum machine! - #13 by StaffanMelin) and the OscPocketD/FX (OscPocketD/FX - Multi FX device - #2 by StaffanMelin).

3 Likes

Awesome work dude I’m excited to see where you take this

2 Likes

So, the project is nearly finished. Only one thing bugs me.

I record the sample and assign it a base frequency (fb = 440Hz).

I want to play it at a new frequency (fn).

I calculate the fraction to change the speed as:

sFactor = (fn / fb);

But clearly (?) this is only linear whereas the semitone interval is not. Well, it works across octaves. But a semitone step is 2^(1/12).

How to calculate the speedup/down fraction of a sample playback, that is my question, and I can’t get my head around the math involved. @ben_serge, any ideas? @shensley? :slight_smile:

In the past I’ve done something like:

playback_speed = expf(speedvar * (kSpeedLogMax - kSpeedLogMin) + kSpeedLogMin)
              * kSpeedScalar;

where speedvar is a 0-1 representation of the parameter

The logmin is 0 (log(1)), and the logmax is log(2^5) (to create a 5 octave range).
The scalar at the end is 0.125 (2^3) to shift everything down 3 octaves.

The 5 octave range was just chosen for the ease of 0-5V CV input tracking 1V/Octave.

This ends up having a response of -3 to +2 octaves.

You can quantize this by transforming the speeedvar to a MIDI note 0-60 and back, and rounding/truncating the float midi numbers.

2 Likes

Thanks a lot @shensley, tried it out, but it doesn’t work for me with my factor = fnew/fbase, as factor could go higher than 1. Strangely enough, it turned my lower octave (from a CV out keyboard) as a percfect 10 note octave! :smiley:

I’ll work on it more tomorrow when my brain has got some rest.

If it helps… for me, I use tables. They’re generally faster at the expense of using some ram, (though you can save a bit if you only do a 1 octave table and multiply, but I generally don’t bother), plus in my opinion they make working with other things like LFOs, pitch bends and portamentos much simpler.

Resolution-wise, the idea is to have 256 steps per semitone - this is more than 100 “cents” resolution per semitone so it’s practically imperceptible per step.

The table is a list of all of the note frequencies your synth / sample player can do (you can divide the result for an LFO too). And, depending on how your software is set up, you can have the table as a list of reciprocals to save some math (e.g. 1/f). The table generator can be as simple as starting with your base(lowest note) frequency, and for each item in the table multiply by 2^1/12*256. (I’m not sure if it does, but as that ends up being a reasonably precise number - to prevent any potential error accumulation you can “reset” what you’re multiplying by for each semitone or octave etc)

Now that you have this table, things in my opinion get much simpler. Got a midi note on? Take the note number, shift it left 8 times (e.g.multiply by 256), index the table with this value and you’ve got your frequency/reciprocal.

CV to frequency becomes trivial too, just multiply the CV value by the table octave size (12*256 = 3072 in this case). Shift the octave or tuning by simply adding to the virtual CV value.

Linear portamento like a minimoog is also easy - just walk up and down the table, same for doing frequency LFO / pitchbend stuff, it all is just a matter of traversing the table.

Just a thought, I generally use a table wherever I can to save CPU cycles when possible, even log / exp tables etc.

Now, as far as a sample player goes, you’re not playing the samples at 440 Hz, you’re playing them at the sample rate. So, if you know that your sample is tuned to play a 440hz note at 16khz… and you get a note 1 octave above to play… then you take the value you get from your table at 880hz, and multiply by (16000/440) for the frequency, etc - and now you have the 32khz playback rate frequency.

Or, I mean if you wanted to, you could generate the table with the actual sample playback rates too.

4 Likes

Thanks alot, very rewarding read! :slight_smile:

1 Like

Finished! OscPocketD/Sampler - an open source sampler for Daisy Seed

Man, awesome! Nice demo too.

1 Like

Thanks a lot @ben_serge! :slightly_smiling_face: And thank you for the help!