Adding Grain Delay to 4 Sample 12-bit Sampler

Store Forums Audio Hacker Discussion and Project Ideas Adding Grain Delay to 4 Sample 12-bit Sampler

Viewing 4 posts - 1 through 4 (of 4 total)
  • Author
    Posts
  • #9918
    kreiff
    Participant

    Hello!

    I’ve been working with the 4 sample 12-bit sampler project and trying to combine the various examples into a fully featured sampler. I’ve had good luck combining the 4 sample, 12-bit example with all the various modes (Variable sample rate, bit crushing, Filter, Reverse Playback). I’d like to add the grain delay from the Voice Changer tutorial as well, but I’ve hit a roadblock.

    My first thought was that I could simply create an array out of the grainAddress variable to assign the correct address to each sample. I also created a new array variable to store the start addresses (startAddress[4]). But this didn’t work the way I expected.

    I get more or less 1 play through of the sample and then I get very aggressive noise for a long time. The sample will eventually come back around – but I must be missing some important aspect of the code.

    I did notice that the voice changer and 12-bit sampler have a different interrupt set-up in that the voice changer does not have even / odd cycles for read / write – but I’m not sure if that’s my issue here.

    Any ideas for how I can get grain delay working with the 4 sample 12-bit sampler code?

    Here is my current code:

    #include <EEPROM.h>
    #include <AudioHacker.h>
    
    #define DEBUG
    #define OFF 0
    #define PASSTHROUGH 1
    #define RECORD 2
    #define PLAYBACK 3
    #define RECORD_DONE 4
    #define RECORD_BUTTON 5
    #define SAMPLE0_BUTTON 6
    #define SAMPLE1_BUTTON 4
    #define SAMPLE2_BUTTON 3
    #define SAMPLE3_BUTTON 2
    
    unsigned int playbackBuf = 2048;
    unsigned int passthroughSampleRate;
    unsigned int recordingSampleRate;
    unsigned int playbackSampleRate;
    unsigned int nSamplesToPlay = 2048;
    volatile unsigned int nSamplesPlayed = 0;
    unsigned int grainSize = 2048;
    byte stretch = 1;
    unsigned int timer1Start;
    volatile unsigned int timer1EndEven;
    volatile unsigned int timer1EndOdd;
    volatile boolean warning = false;
    int currentA0Position;
    volatile long address = 0;
    volatile long startAddress[4] = {0, 65535, 0, 65535};
    volatile long endAddress[4];
    volatile byte addressChipNumber = 0;
    volatile long grainAddress[4];
    volatile byte grainAddressChipNumber = 0;
    
    volatile byte mode = PASSTHROUGH;
    unsigned long lastDebugPrint = 0;
    unsigned int readBuf[2];
    unsigned int writeBuf;
    boolean evenCycle = true;
    
    // set to true if you are using a battery backup on the
    // SRAM and want to keep sample address info in EEPROM
    boolean batteryBackup = true;
    
    unsigned int recordStartTime;
    unsigned int recordEndTime;
    boolean sampleRecorded[4];
    byte sample;
    
    void setup() {
    #ifdef DEBUG
      Serial.begin(115200);        // connect to the serial port
    #endif
    
      recordingSampleRate = 16384;
      passthroughSampleRate = 16384;
      timer1Start = UINT16_MAX - (F_CPU / passthroughSampleRate);
    
      pinMode(RECORD_BUTTON, INPUT);
      pinMode(SAMPLE0_BUTTON, INPUT);
      pinMode(SAMPLE1_BUTTON, INPUT);
      pinMode(SAMPLE2_BUTTON, INPUT);
      pinMode(SAMPLE3_BUTTON, INPUT);
    
      digitalWrite(RECORD_BUTTON, HIGH);
      digitalWrite(SAMPLE0_BUTTON, HIGH);
      digitalWrite(SAMPLE1_BUTTON, HIGH);
      digitalWrite(SAMPLE2_BUTTON, HIGH);
      digitalWrite(SAMPLE3_BUTTON, HIGH);
    
      AudioHacker.begin();
    
    #ifdef DEBUG
      Serial.print("sample rate = ");
      Serial.print(passthroughSampleRate);
      Serial.print(" Hz, recording sample rate = ");
      Serial.print(recordingSampleRate);
      Serial.print(" Hz");
      Serial.println();
    #endif
    
      sampleRecorded[0] = false;
      sampleRecorded[1] = false;
      sampleRecorded[2] = false;
      sampleRecorded[3] = false;
    
      if (batteryBackup) {
        // Read endAddress[] values from EEPROM when we have a battery
        // connected to the Audio Hacker to preserve SRAM contents.
        for (byte i=0;i<4;i++) {
          byte a = i*3;
          long b;
          b = (long)EEPROM.read(a);
          endAddress[i] = (b << 16);
          b = (long)EEPROM.read(a+1);
          endAddress[i] |= (b << 8);
          b = (long)EEPROM.read(a+2);
          endAddress[i] |= b;
    
          if (endAddress[i] > 0) {
    	       sampleRecorded[i] = true;
    #ifdef DEBUG
    	       Serial.print("sample ");
    	       Serial.print(i);
    	       Serial.print(" endAddress = ");
    	       Serial.print(endAddress[i]);
    	       Serial.println();
    #endif
          }
        }
      }
    }
    
    void loop() {
    
    #ifdef DEBUG
      if ((millis() - lastDebugPrint) >= 1000) {
        lastDebugPrint = millis();
    
        // Print the number of instruction cycles remaining at the end of the ISR.
        // The more work you try to do in the ISR, the lower this number will become.
        // If the number of cycles remaining reaches 0, then the ISR will take up
        // all the CPU time and the code in loop() will not run.
    
        /*
        Serial.print("even cycles remaining = ");
        Serial.print(UINT16_MAX - timer1EndEven);
        Serial.print("   odd cycles remaining = ");
        Serial.print(UINT16_MAX - timer1EndOdd);
        Serial.println();
        if (((UINT16_MAX - timer1EndEven) < 20) || (((UINT16_MAX - timer1EndOdd) < 20))) {
          Serial.println("WARNING: ISR execution time is too long. Reduce sample rate or reduce the amount of code in the ISR.");
        }
        */
      }
    #endif
    
      if ((mode == OFF) || (mode == PASSTHROUGH)) {
        if ((digitalRead(RECORD_BUTTON) == LOW) && ((digitalRead(SAMPLE0_BUTTON) == LOW) || (digitalRead(SAMPLE1_BUTTON) == LOW) || (digitalRead(SAMPLE2_BUTTON) == LOW) || (digitalRead(SAMPLE3_BUTTON) == LOW))) {
          // enter RECORD mode
          recordStartTime = millis();
          if ((recordStartTime - recordEndTime) < 20) {
            // debounce the record button.
            recordStartTime = 0;
            return;
          }
          if (digitalRead(SAMPLE0_BUTTON) == LOW) {
            sample = 0;
            address = 0;
            addressChipNumber = 0;
          }
          if (digitalRead(SAMPLE1_BUTTON) == LOW) {
            sample = 1;
            address = 65535;
            addressChipNumber = 0;
          }
          if (digitalRead(SAMPLE2_BUTTON) == LOW) {
            sample = 2;
            address = 0;
            addressChipNumber = 1;
          }
          if (digitalRead(SAMPLE3_BUTTON) == LOW) {
            sample = 3;
            address = 65535;
            addressChipNumber = 1;
          }
          mode = RECORD;
          timer1Start = UINT16_MAX - (F_CPU / recordingSampleRate);
          currentA0Position = analogRead(0);
        } else {
          // enter PLAYBACK mode
          if ((digitalRead(SAMPLE0_BUTTON) == LOW) && (sampleRecorded[0])) {
            address = 0;
            addressChipNumber = 0;
            sample = 0;
            mode = PLAYBACK;
            grainAddress[0] = 0;
            grainAddressChipNumber = 0;
            nSamplesPlayed = 0;
          }
          if ((digitalRead(SAMPLE1_BUTTON) == LOW) && (sampleRecorded[1])) {
            address = 65535;
            addressChipNumber = 0;
            sample = 1;
            mode = PLAYBACK;
            grainAddress[1] = 65535;
            grainAddressChipNumber = 0;
            nSamplesPlayed = 0;
          }
          if ((digitalRead(SAMPLE2_BUTTON) == LOW) && (sampleRecorded[2])) {
            address = 0;
            addressChipNumber = 1;
            sample = 2;
            mode = PLAYBACK;
            grainAddress[2] = 0;
            grainAddressChipNumber = 1;
            nSamplesPlayed = 0;
          }
          if ((digitalRead(SAMPLE3_BUTTON) == LOW) && (sampleRecorded[3])) {
            address = 65535;
            addressChipNumber = 1;
            sample = 3;
            mode = PLAYBACK;
            grainAddress[3] = 65535;
            grainAddressChipNumber = 1;
            nSamplesPlayed = 0;
          }
        }
      }
    
      if (mode == PASSTHROUGH) {
        timer1Start = UINT16_MAX - (F_CPU / passthroughSampleRate);
      }
    
      if (mode == RECORD) {
        if (((sample == 0) && (digitalRead(SAMPLE0_BUTTON) == HIGH)) ||
            ((sample == 1) && (digitalRead(SAMPLE1_BUTTON) == HIGH)) ||
            ((sample == 2) && (digitalRead(SAMPLE2_BUTTON) == HIGH)) ||
            ((sample == 3) && (digitalRead(SAMPLE3_BUTTON) == HIGH))) {
              // recording stopped
              recordEndTime = millis();
              if (recordEndTime - recordStartTime < 20) {
                // debounce
                return;
              }
              sampleRecorded[sample] = true;
              endAddress[sample] = address;
    #ifdef DEBUG
              Serial.print("sample ");
              Serial.print(sample);
              Serial.print(" recording time = ");
              Serial.print(recordEndTime - recordStartTime);
              Serial.println(" ms");
              Serial.print(" endAddress = ");
              Serial.println(endAddress[sample]);
    #endif
    
              if (batteryBackup) {
                // Write endAddress to EEPROM for battery backup use.
                byte a = sample*3;
                EEPROM.write(a, (endAddress[sample] >> 16) & 0xFF);
                EEPROM.write(a+1, (endAddress[sample] >> 8) & 0xFF);
                EEPROM.write(a+2, endAddress[sample] & 0xFF);
              }
              mode = PASSTHROUGH;
            }
      } else {
      }
    
      if (mode == RECORD_DONE) {
        if (recordStartTime != 0) {
    #ifdef DEBUG
          Serial.print("sample ");
          Serial.print(sample);
          Serial.print(" recording time = ");
          Serial.print(millis() - recordStartTime);
          Serial.println(" ms");
          Serial.print(" endAddress = ");
          Serial.println(endAddress[sample]);
    #endif
          sampleRecorded[sample] = true;
          recordStartTime = 0;
    
          if (batteryBackup) {
            // Write endAddress to EEPROM for battery backup use.
            byte a = sample*3;
            EEPROM.write(a, (endAddress[sample] >> 16) & 0xFF);
            EEPROM.write(a+1, (endAddress[sample] >> 8) & 0xFF);
            EEPROM.write(a+2, endAddress[sample] & 0xFF);
          }
        }
        if (digitalRead(RECORD_BUTTON) == HIGH) {
          // record button released
          mode = PASSTHROUGH;
        }
      }
    
      if (mode == PLAYBACK) {
        digitalWrite(A5, HIGH);
        if (((sample == 0) && (digitalRead(SAMPLE0_BUTTON) == HIGH)) ||
            ((sample == 1) && (digitalRead(SAMPLE1_BUTTON) == HIGH)) ||
            ((sample == 2) && (digitalRead(SAMPLE2_BUTTON) == HIGH)) ||
            ((sample == 3) && (digitalRead(SAMPLE3_BUTTON) == HIGH))) {
              // play button released
              mode = PASSTHROUGH;
        } else {
          grainSize = map(analogRead(1), 0, 1023, 2, 2048);
          stretch = map(analogRead(2), 0, 1023, 1, 10);
          playbackSampleRate = map(analogRead(0), 0, 1023, 1000, 16384);
          // nSamplesToPlay is the number of samples to play from each grain.
          nSamplesToPlay = stretch * grainSize * ((float)playbackSampleRate/(float)recordingSampleRate);
          // compute the start value for counter1 to achieve the chosen playback rate
          timer1Start = UINT16_MAX - (F_CPU / playbackSampleRate);
        }
      }
    }
    
    ISR(TIMER1_OVF_vect) {
      TCNT1 = timer1Start;
      unsigned int signal;
    
      if (mode != RECORD_DONE) {
        AudioHacker.writeDAC(playbackBuf);
      }
    
      if ((mode != PLAYBACK) && (mode != RECORD_DONE)) {
        // Read ADC
        signal = AudioHacker.readADC();
      }
    
      if (mode == RECORD) {
        if (evenCycle) {
          // we only write to memory on odd cycles, so buffer the sampled signal.
          writeBuf = signal;
        } else {
          // Write to SRAM
          AudioHacker.writeSRAMPacked(addressChipNumber, address, writeBuf, signal);
    
          address += 3;
          if (((sample == 0) && (address > 65532)) ||
              ((sample == 1) && (address > MAX_ADDR)) ||
              ((sample == 2) && (address > 65532)) ||
              ((sample == 3) && (address > MAX_ADDR))) {
                // end of memory, stop recording
                mode = RECORD_DONE;
                endAddress[sample] = address;
          }
        }
      }
    
      if (mode == PLAYBACK) {
        if (evenCycle) {
          // Read from SRAM
          AudioHacker.readSRAMPacked(addressChipNumber, address, readBuf);
          signal = readBuf[0];
    
          nSamplesPlayed++;
          if (nSamplesPlayed >= nSamplesToPlay) {
          // proceed to the next grain
          nSamplesPlayed = 0;
          grainAddress[sample] += grainSize;
          if (grainAddress[sample] > endAddress[sample]) {
            grainAddress[sample] = startAddress[sample];
          }
          address = grainAddress[sample];
          playbackBuf = signal;
          return;
          }
    
          address += 3;
          if ((sample == 0) && (address == endAddress[0])) {
            address = 0;
            addressChipNumber = 0; 
          }
          if ((sample == 1) && (address == endAddress[1])) {
            address = 65535;
            addressChipNumber = 0;
          }
          if ((sample == 2) && (address == endAddress[2])) {
            address = 0;
            addressChipNumber = 1;
          }
          if ((sample == 3) && (address == endAddress[3])) {
            address = 65535;
            addressChipNumber = 1;
          }
          if (address == (grainAddress[sample] + grainSize)) {
            address = grainAddress[sample]; // start over within the grain
           }
        } else {
          signal = readBuf[1];
        }
      } // PLAYBACK
    
      playbackBuf = signal;
    
    #ifdef DEBUG
      if (evenCycle) {
        timer1EndEven = TCNT1;
      } else {
        timer1EndOdd = TCNT1;
      }
    #endif
      evenCycle = !evenCycle;
    }
    #9926
    kreiff
    Participant

    Okay – so, I’ve been messing around with this for a few days now and I think the issue had to do with the grainSize and the address increments. I still have to test this – but I think the addresses can only be incremented by 3 because of the way the bytes are packed and read back in increments of 3.

    My plan is to create a new volatile integer variable that equals the grainSize pot value multiplied by 3. Update the analog read like this:

    
          grainSize = map(analogRead(1), 0, 1023, 3, 1365);
          stretch = map(analogRead(2), 0, 1023, 1, 10);
    

    And then update the granular delay code like this:

     nSamplesPlayed++;
      if (nSamplesPlayed >= nSamplesToPlay) {
      // proceed to the next grain
      nSamplesPlayed = 0;
      grainAddress[sample] += grainSizeTimesThree;
      if (grainAddress[sample] > endAddress[sample]) {
      grainAddress[sample] = startAddress[sample];
      }
      address = grainAddress[sample];
      playbackBuf = signal;
      return;
      }
    

    Am I on the right track?

    I’ll report back if my test is successful

    #9930
    Michael
    Keymaster

    I think you are close. Just keep experimenting. Yes, the increment of 3 bytes for addresses is due to packing two 12-bit samples into 3 bytes. It makes things more complicated, though.

    #9932
    kreiff
    Participant

    Thanks for the feedback, Michael! I’ve got it working now. The 3-byte increment was the big piece of the puzzle. I’m also incrementing the “nSamplesPlayed” variable by 3 now and I’ve added some more window stages (20 as opposed to 10) and that seems to give me good results.

    Now I’m going to try to figure out how to get it to playback the grains in reverse!

Viewing 4 posts - 1 through 4 (of 4 total)
  • You must be logged in to reply to this topic.