Arbitrary sample-rates for outputting a chiptune wavetable

I’ve been digging through the libs and things and have made some decent progress figuring things out. For my application though I think I’ll need to do some lower level DSP work.

So in brief if anyone has a good link to a primer that would be great!

For my specific application, read on:

I’m basically generating chiptune waveforms which are a meager 32-samples in length (and 4-bit!). More info is on my gitlab for the project here: https://gitlab.com/m00dawg/gave

But for the scope of the question, probably enough to know that I’ll have a 32-sample waveform. That means I have a few parameters I need to mess with to figure out how to stretch the waveform out to the requested size (I’m not quite sure how to find what ‘size’ is for my callback though?).

So say I go full lofi chiptune and have a sample rate of 8kHz in hardware. If I wanted to play the sample a just 1Hz, that means I need to march through the 32 samples in 1 second, or 1/32. Then I need to stretch that out to the sampling rate. That ends up meaning each sample becomes 250 when resampled (32 * 250 = 8000) and works out evenly so, in this case, I don’t need to worry about where I am in my wavetable once I’m done generating the sample.

But that won’t always be the case meaning I need to also keep track of marching through the wavetable for the case where it’s not aligned so I know where to begin from for the next request.

Does all this sound within the ballpark or am I super duper off base?

Self reply fail but it’s a big enough update I thought it better to separate things. The above I think actually does work but very quickly I run into having to interpolate since most pitches won’t divide nicely between my sample size of 32 and whatever the actual audio rate is.

I noticed this post mentions being able to set the sample rate to arbitrary values, but would be limited between 8kHz to 96kHz. If I could set the sample rate to literally whatever I wanted, I can avoid all the problems of above without interpolating.

I noticed CV_OUTs appear to be using a 12-bit DAC based on their range (0-4095). Thinking since I want lofi output, I’m wondering if I could just use SetCvOut1(val) instead as my actual audio output and then use the sampling rate to control pitch.

For example, A0 (27.5Hz) at 32 samples would be a sample rate of 880Hz. For A4 (440Hz),14.08kHz on up to as far as the Daisy could update the CV_OUT.

This actually works closer to how the GameBoy’s wave channel worked, which is what I’m basing my design off of. The GB uses countdown timers where the setting of the timer is varied according to pitch.

I made some headway list night woohoo! The problem I’m running into is I naively assumed both that SetCvOut1() was available on the SM and let me call this arbitrarily (as in not tied to a clock, save for perhaps the speed of the SPI bus).

I dug through the docs some more and noticed I can actually do this:

hw.SetAudioSampleRate(12345);

However the docs mention it will pick the nearest sample rate to whatever is provided, which isn’t what I want.

So a bit stuck here, at least with using libDaisy. I can go manual and just treat the Daisy a bit like a Teensy with some DACs connected to it but that defeats part of the reason for using the Daisy platform.

Anyone know what the status is on getting variable sample rate support? I recall reading around the forums it was being worked on. Having this would handily solve my problem (doubly so if I can use the audio DAC instead of the CV DAC).

Another update. I ended up spending most of today looking at various options (including non-Daisy ones) and found some alternatives if I want to jump ship, though I did take a another crack at it and came up with something like this:

// Callback to update the wave index when our timer expires
// which, in turn, changes the value that is output in the
// audioCallback. This also resets the timer to the current
// sample_rate which we update outside of this interrupt callback.
void waveStepCallback() {
  noInterrupts();
  wave_index++;
  if(wave_index > WAVE_SLOTS)
    wave_index = 0;
  waveTimer->setOverflow(sample_rate);     
  interrupts();
}

// Surely this won't work. Output the same value constantly
// as the waveStepCallback is what is actually changing the 
// value.
void audioCallback(float**  in, float** out, size_t size) {
  for(size_t i = 0; i < size; i++)
    out[0][i] = out[1][i] = scaled_wave[wave_index];  
}

void setup() { 
  // Wave Timer Init
  waveTimer = new HardwareTimer(WAVE_TIMER);
  waveTimer->setOverflow(sample_rate, HERTZ_FORMAT);
  waveTimer->attachInterrupt(waveStepCallback);
  waveTimer->resume();

  // Patch SM Init
  patchSM = DAISY.init(DAISY_PATCH_SM);
  DAISY.SetAudioSampleRate(SaiHandle::Config:: SampleRate::SAI_96KHZ);
  DAISY.SetAudioBlockSize(1);
  DAISY.StartAudio(audioCallback);

  // Placeholder so we have a wave to work with until we read it from EPROM
  // and add user updates and stuff
  scaleWave();
}

void loop() {
  // Read V/Oct and Tune knob and set sample_rate
  updatePitch();
  updateSampleRate();
}

That’s not the full program since I wanted to keep just show the relevant part of the question I’m about to ask:

Does the DAISY.StartAudio block? As in does it disable/enable interrupts before and after the callback or is that something I would need to do if I wanted that?

I’m hoping it does NOT since, if that’s the case, it means I can use my timer and the waveStepCallback to effectively control the pitch of the wavetable independently (by changing the speed the wavetable index is updated) and just shove the current value of the wavetable into the audio buffer as it changes.

I know this may cause aliasing and find it a bit less ideal than clocking the DSP directly (like I’m doing with the Teensy approach) But since there’s no way to do that with the audio DAC and this is for a lo-fi chiptune synth anyway, aliases are something I expect. But I don’t know if I’m in store for other potential downsides by basically having 2 interrupt handlers set to different timers.