changing pitch of audio samples - TeensyVariablePlayback library

Thanks for your help, but I think we're currently not on the same page.

I need help to understand the use of the looping functions to play a sound repeatedly without any audible interruptions.
 
Slightly surprised that you're surprised by the "audible interruptions", given that your audio file fades in at the start and out at the end, so there's about 0.5s of near-silence if it's looped without setting start and finish points.

Here's an edit of your code which works, as far as I can tell:
C++:
#include <Arduino.h>
#include <Audio.h>
#include <SD.h>
#include <TeensyVariablePlayback.h>

// GUItool: begin automatically generated code
AudioPlaySdResmp         playSdWav1;     //xy=324,457
AudioOutputI2S           i2s2;           //xy=840.8571472167969,445.5714416503906
AudioConnection          patchCord1(playSdWav1, 0, i2s2, 0);
AudioConnection          patchCord2(playSdWav1, 0, i2s2, 1);
AudioControlSGTL5000     audioShield;
// GUItool: end automatically generated code

double getPlaybackRate(int16_t analog);

char* _filename = "SPACE.WAV";

void setup() {
    Serial.begin(9600);

    if (!(SD.begin(10))) {
        // stop here if no SD card, but print a message
        while (1) {
            Serial.println("Unable to access the SD card");
            delay(500);
        }
    }

    AudioMemory(24);

    audioShield.enable();
    audioShield.volume(0.5);

    playSdWav1.enableInterpolation(true);
    playSdWav1.setPlaybackRate(1.0);

    playSdWav1.playWav(_filename);
    playSdWav1.setLoopType(looptype_repeat);

    // Sound sample isn't seamless, so pick some loop points
    playSdWav1.setLoopStart(19374);
    playSdWav1.setLoopFinish(643346);

    // Extra stuff to mask glitches at loop point
    playSdWav1.setUseDualPlaybackHead(true);
    playSdWav1.setCrossfadeDurationInSamples(1000);
}

float rate=1.0f, factor = 1.001f;
uint32_t next;
bool increasing;
void loop() {
    int newsensorValue = analogRead(A0);

    // I don't have a sensor, so just ramp rate up and down
    if (millis() >= next)
    {
      next = millis() + 3; // every 3ms, i.e. roughly every audio update

      if (increasing)
      {
        rate *= factor;
        increasing = rate < 2.0f;
      }
      else
      {
        rate /= factor;
        increasing = rate < 0.5f;
      }
      playSdWav1.setPlaybackRate(rate);
    }

    if(!playSdWav1.isPlaying()) {
      Serial.println("Restarted playback"); // This Never Happens
      playSdWav1.playWav(_filename);
    }
}

// map analog input 0-1023 to 0.5-3.0 pitch
double getPlaybackRate(int16_t analog) { //analog: 0..1023
    return ((analog / 1023) * 2.5) + 0.5;
}
 
Image of WAV file supplied, showing loop points I selected
1744402502246.png
 
I can't believe that I missed that the sound fades in and out, sorry for that. Will try the code and thank you a lot already
 
Slightly surprised that you're surprised by the "audible interruptions", given that your audio file fades in at the start and out at the end, so there's about 0.5s of near-silence if it's looped without setting start and finish points.

Here's an edit of your code which works, as far as I can tell:
C++:
#include <Arduino.h>
#include <Audio.h>
#include <SD.h>
#include <TeensyVariablePlayback.h>

// GUItool: begin automatically generated code
AudioPlaySdResmp         playSdWav1;     //xy=324,457
AudioOutputI2S           i2s2;           //xy=840.8571472167969,445.5714416503906
AudioConnection          patchCord1(playSdWav1, 0, i2s2, 0);
AudioConnection          patchCord2(playSdWav1, 0, i2s2, 1);
AudioControlSGTL5000     audioShield;
// GUItool: end automatically generated code

double getPlaybackRate(int16_t analog);

char* _filename = "SPACE.WAV";

void setup() {
    Serial.begin(9600);

    if (!(SD.begin(10))) {
        // stop here if no SD card, but print a message
        while (1) {
            Serial.println("Unable to access the SD card");
            delay(500);
        }
    }

    AudioMemory(24);

    audioShield.enable();
    audioShield.volume(0.5);

    playSdWav1.enableInterpolation(true);
    playSdWav1.setPlaybackRate(1.0);

    playSdWav1.playWav(_filename);
    playSdWav1.setLoopType(looptype_repeat);

    // Sound sample isn't seamless, so pick some loop points
    playSdWav1.setLoopStart(19374);
    playSdWav1.setLoopFinish(643346);

    // Extra stuff to mask glitches at loop point
    playSdWav1.setUseDualPlaybackHead(true);
    playSdWav1.setCrossfadeDurationInSamples(1000);
}

float rate=1.0f, factor = 1.001f;
uint32_t next;
bool increasing;
void loop() {
    int newsensorValue = analogRead(A0);

    // I don't have a sensor, so just ramp rate up and down
    if (millis() >= next)
    {
      next = millis() + 3; // every 3ms, i.e. roughly every audio update

      if (increasing)
      {
        rate *= factor;
        increasing = rate < 2.0f;
      }
      else
      {
        rate /= factor;
        increasing = rate < 0.5f;
      }
      playSdWav1.setPlaybackRate(rate);
    }

    if(!playSdWav1.isPlaying()) {
      Serial.println("Restarted playback"); // This Never Happens
      playSdWav1.playWav(_filename);
    }
}

// map analog input 0-1023 to 0.5-3.0 pitch
double getPlaybackRate(int16_t analog) { //analog: 0..1023
    return ((analog / 1023) * 2.5) + 0.5;
}
Just wanted to report back that the code works. Thanks!

I removed the fade in Audacity, so I removed setLoopStart() and setLoopFinish() aswell.
When I extend the crossfade, the loop is almost inaudible.
 
Hi @h4yn0nnym0u5e, I'm testing the array player, the pitch, loop type and crossfade works but not having luck with the start and end points of the loop, same with the setPlayStart (it always plays the whole sample starting at the beginning).
Could you confirm that these functions are also implemented for the array player?
here's the relevant part of my code. The variable audio_samples will be the size of the array audioBuffer/2

Code:
AudioPlayArrayResmp wave;

    uint32_t audio_samples = 0; 
 
      wave.enableInterpolation(true);
      wave.setUseDualPlaybackHead(true);
      wave.setPlayStart(play_start_loop);
      wave.setLoopType(looptype_pingpong);
   
    void noteOn(uint8_t MIDInote, uint8_t MIDIvel, bool slide)
    {
      wave.playRaw(audio_buffer, audio_samples, 1); // 1 = mono
    }

    void setParam(uint8_t param, uint8_t val)
    {
      switch(param)
      {
        case setPitch:
          wave.setPlaybackRate(pitch[val]);
          break;
        case setLoopStart:
          wave.setLoopStart((audio_samples * val) / 127);
          break;
        case setLoopEnd:
          wave.setLoopFinish((audio_samples * val) / 127);
          break;
        case setCrossfade:
          wave.setCrossfadeDurationInSamples((audio_samples * val) / 127);
          break;
      }
    }
 
Most functions are agnostic about the audio source (SD, array, etc.), as far as I know, though there may be exceptions - I didn't look too closely at anything other than SD (filesystem) playback.

One thing I do know is that the various play() methods have a tendency to reset the loop start and end values, so you need to (re)set those after you start playback. (It's not an ideal API, but I wasn't about to change it since I didn't know how many existing applications would break as a result.) Your code fragment above suggests you're only setting them from MIDI values, without making a record of those settings that you can re-use in subsequent noteOn() calls.

Further than that, it's hard to guess. If you can craft a small sketch requiring no hardware apart from a Teensy, audio adaptor of some sort, and a file to play, and where you can state specifically "at this point I expect to hear A, but what I get is B", then maybe I can help more.
 
  1. Stop using the samples
  2. Free the PSRAM they’re in
  3. Allocate PSRAM for the new samples
  4. Load the samples into the allocated space
 
Load new Sample from file list. It works a few times but then my Teensy 4.1 with 16MB PSRAM is crashes (see Video)

Video


C:
// stop all samples
                for (size_t i = 0; i < 8; i++)
                {
                     sampleOsc[i].stop();
                }

// load new sample from list
                String Folder = "Samples/";
                String name_ = Folder + fileNames[new_index - 1];    // sample name from string list
                char *_filename = const_cast<char*>(name_.c_str());
                newdigate::flashloader loader;
                Sample = loader.loadSample(_filename);
 
Last edited:
flashloader isn't very good - it has no way of freeing up unused samples. And your posted code fragment doesn't even attempt to address my point 2 in post #236...
 
How make free the PSRAM they’re in ?
The line "newdigate::flashloader loader;" resets the sample address to the beginning. After that, I load the new sample. Is that correct?
 
The line "newdigate::flashloader loader;" resets the sample address to the beginning
No, it just creates an instance of the loader. You only need, and ideally should only have, one such instance in your entire program. It attempts (rather poorly...) to keep track of the amount of PSRAM used, but each instance has its own count.
How make free the PSRAM they’re in ?
I think it should be something like
C++:
extmem_free(Sample->sampledata);
delete Sample;
Sample = nullptr;
That’s completely untested code…

It would be much better if that code had been a method provided by the flashloader class, of course. Once again, I’d say that flashloader is really so poor that it’s hardly worth the effort to understand and use it.
 
Thanks for your help. Loading samples now works without crashes (see my Video). I also made some changes to the flashloader.cpp file. As an example, I used the psramloader.cpp from MicroDexed-touch.

new flashloader.cpp
C:
// new flashloader.cpp

#include "flashloader.h"
#include <Audio.h>

namespace newdigate {

    audiosample * flashloader::loadSample(const char *filename ) {
        uint8_t *data = nullptr;
        Serial.printf("Reading %s\n", filename);
        File f = SD.open(filename, O_READ);
        if (f) {
            if (f.size() < _bytesavailable) {
               //noInterrupts();
               AudioNoInterrupts();
                uint32_t total_read = 0;
                //auto *data = (int8_t*)extmem_malloc(f.size() );
                data = (uint8_t*)extmem_malloc(f.size());
                uint8_t *index = data;
                while (f.available()) {
                    size_t bytesRead = f.read(index, flashloader_default_sd_buffersize);
                    if (bytesRead == 0)
                        break;
                    total_read += bytesRead;
                    index += bytesRead;
                }
                //interrupts();
                AudioInterrupts();
                _bytesavailable -= total_read;

                audiosample *sample = new audiosample();
                sample->sampledata = (int16_t*)data;
                sample->samplesize = f.size();

                Serial.printf("\tsample start %x\n", (uint32_t)data);
                Serial.printf("\tsample size %d\n", sample->samplesize);
                Serial.printf("\tavailable: %d\n", _bytesavailable);

                return sample;
            }
        }

        Serial.printf("not found %s\n", filename);
        return nullptr;
    }

}


load sample from file list
C:
// press patch list on page1 -----------------------------------
void press_patch_list_page1()
{
    uint8_t new_index = 0;
    static uint8_t old_index = 0;
    static uint8_t old_key_index = 1;

    new_index = touch_patch_list();
    if (new_index != old_index)
    {
        old_index = new_index;

        if (new_index >= 1 && new_index <= 7)
        {
            if (new_index != old_key_index)
            {
                old_key_index = new_index;

                sample_busy_flag = false;
                extmem_free(Sample->sampledata);
                //delete Sample;
                Sample = nullptr;

                print_file_List(new_index - 1);
                 tft.updateScreenAsync(false); // draw screen
                // stop all samples
                for (size_t i = 0; i < 8; i++)
                {
                     sampleOsc[i].stop();
                }
                String Folder = "Samples/";
                String name_ = Folder + fileNames[new_index - 1]; // sample name from string list
                char *_filename = const_cast<char*>(name_.c_str());
                Sample = loader.loadSample(_filename);
                sample_busy_flag = true;
                
            }
        }
        else if (new_index == 0)
        {
            for (size_t i = 0; i < 7; i++)
            {
                TK_state_List_P1[i] = false; // clear touch state
            }
        }
    }

    if (new_index > 0)
    {
        Reset_screensaver();
    }
}

Video
 
Because of
C++:
audiosample *sample = new audiosample();
you need to un-comment
C++:
//delete Sample;
or you will have a slow memory leak…
 
When loading larger samples > 1MB there are random crashes :(

C:
#ifndef PSRAMLOADER_H
#define PSRAMLOADER_H

#include <Arduino.h>
#include <SD.h>

extern "C" uint8_t external_psram_size;

namespace newdigate {

    const uint32_t flashloader_default_sd_buffersize = 4 * 1024;
    
    struct audiosample {
        int16_t *sampledata;
        uint32_t samplesize;
    };

    class Psramloader {
    public:

        Psramloader() {
            _bytesavailable = external_psram_size * 1048576;
        }

        uint32_t _bytesavailable = 0;
        audiosample * loadSample(const char *filename);
    };
};
#endif

C:
#include "psramloader.h"
#include <Audio.h>

namespace newdigate {
    audiosample * Psramloader::loadSample(const char *filename ) {
        uint8_t *data = nullptr;
        Serial.printf("Reading %s\n", filename);
        File f = SD.open(filename, O_READ);
        if (f) {
            if (f.size() < _bytesavailable) {
               AudioNoInterrupts();
                uint32_t total_read = 0;
                data = (uint8_t*)extmem_malloc(f.size());
                uint8_t *index = data;
                while (f.available()) {
                    size_t bytesRead = f.read(index, flashloader_default_sd_buffersize);
                    if (bytesRead == 0)
                        break;
                    total_read += bytesRead;
                    index += bytesRead;
                }
                AudioInterrupts();
                _bytesavailable -= total_read;
                audiosample *sample = new audiosample();
                sample->sampledata = (int16_t*)data;
                sample->samplesize = f.size();
                Serial.printf("\tsample start %x\n", (uint32_t)data);
                Serial.printf("\tsample size %d\n", sample->samplesize);
                Serial.printf("\tavailable: %d\n", _bytesavailable);
                return sample;
            }
        }
        Serial.printf("not found %s\n", filename);
        return nullptr;
    }
}
 
Last edited:
Have you used CrashReport to try to find out where the crash is occurring? Does the synth have to be playing, or will it crash if you just keep loading different big samples?

I just noticed your code in post #241 only stops playing after freeing the sample memory, which is not the correct order given in my post #236.
 
Thanks for your comment about stopping sample playback and the crash report. I'll modify the code and test again. The crash occurred intermittently after loading large samples.
 
So … what code is at 0xA09C? There’s a thread on the use of addr2line here, if you haven’t used it before. It’s still a TODO on the PJRC website…
 
Oh... There's something wrong with the Psramloader. The available memory keeps getting smaller, even though I delete the old sample before reloading it.

psram.png
 
I used the psramloader.cpp from MicroDexed-touch.
Really? It doesn’t look much like it…
There's something wrong with the Psramloader. The available memory keeps getting smaller, even though I delete the old sample before reloading it.
…it looks like you still have a slightly modified copy of the original newdigate code. Which as I’ve repeatedly said, is not fit for serious use!

The MicroDexed-touch code has an unloadSample method that keeps track of PSRAM memory that it has consumed. Though it may or may not keep track of memory allocated by other modules - I haven’t looked very closely at the source code. You could probably adopt it, with changes, if you don’t use PSRAM for anything other than samples. But it looks a bit specific to the Microdexed-touch … it would probably be easier to write your own code from scratch.
 
Back
Top