Store › Forums › Audio Hacker › Discussion and Project Ideas › Adding Grain Delay to 4 Sample 12-bit Sampler
Tagged: 12-bit sampler, grain delay, voice changer
- This topic has 3 replies, 2 voices, and was last updated 6 years, 1 month ago by kreiff.
-
AuthorPosts
-
October 14, 2018 at 3:09 pm #9918kreiffParticipant
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; }
October 19, 2018 at 3:31 pm #9926kreiffParticipantOkay – 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
October 21, 2018 at 1:02 pm #9930MichaelKeymasterI 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.
October 21, 2018 at 1:42 pm #9932kreiffParticipantThanks 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!
-
AuthorPosts
- You must be logged in to reply to this topic.