Audio Sampling

Status
Not open for further replies.

chandrian

Member
Hello All,

I am trying to figure out how to just sample a chunk (.5 seconds) of Audio once and replay it. I want to fill a single buffer and I do not want to keep filling and clearing it. I was looking at the delay class because I really only need a half of a second or so and it seems the Teensy 3.2 can store .45 seconds or so. The delay class from the library seems to make sense but I am a little confused how the classes interact with the Update() and the envelope manipulation.

Basically my end goal is an infinite sustain that samples once and repeats. I realize there will need to be some audio cleaning since it will most likely pop when the sample starts over but I am just looking for basic functionality right now.

I have tried messing with buffer and queue1() but there is latency and I would rather use Pauls library since he uses very efficient coding.

Any Direction would be greatly appreciated! I have posted about this a few times so you could look at my older posts for more clarification (or confusion)

Thanks all.
 
Hey Chandrian,

I've had great success doing exactly the thing you want here. Take a look at the effect in the audio design tool called "Granular", it was contributed by the awesomeness that is Bleep labs.

You can find an example that works straight out of the box, in the examples, please do attach all the potentiometers and the knobs to hear the effect. Now there's a caveat in the way it's set up, and I thought after hooking everything up correctly it wasn't working as I was expecting the buttons to be one push on, one push off, but I found I had to hold down the button to get it to start sampling the audio.

Now the memory you think may be enough, but 100% it's not, out of the word hello I could maybe if I was lucky and caught it exactly at the right time I could loop some sort of fragment like "ello". I think with the granular effect it states in the code around 290ms which may be just under your "budget", I reckon with an added RAM chip in the extra memory slot would do the trick.

Code:
// Granular Effect Example - Pitch shift or freeze sound
//
// This example is meant to be used with 3 buttons (pin 0,
// 1, 2) and 2 knobs (pins 16/A2, 17/A3), which are present
// on the audio tutorial kit.
//   https://www.pjrc.com/store/audio_tutorial_kit.html
//
// Data files to put on your SD card can be downloaded here:
//   http://www.pjrc.com/teensy/td_libs_AudioDataFiles.html
//
// This example code is in the public domain.

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
#include <Bounce.h>

AudioPlaySdWav           playSdWav1;     //xy=163,135
AudioMixer4              mixer1;         //xy=332,167
AudioEffectGranular      granular1;      //xy=504,155
AudioOutputI2S           i2s1;           //xy=664,185
AudioConnection          patchCord1(playSdWav1, 0, mixer1, 0);
AudioConnection          patchCord2(playSdWav1, 1, mixer1, 1);
AudioConnection          patchCord3(mixer1, granular1);
AudioConnection          patchCord4(granular1, 0, i2s1, 0);
AudioConnection          patchCord5(granular1, 0, i2s1, 1);
AudioControlSGTL5000     sgtl5000_1;     //xy=236,248

Bounce button0 = Bounce(0, 15);
Bounce button1 = Bounce(1, 15);
Bounce button2 = Bounce(2, 15);

#define GRANULAR_MEMORY_SIZE 12800  // enough for 290 ms at 44.1 kHz
int16_t granularMemory[GRANULAR_MEMORY_SIZE];

// Use these with the Teensy Audio Shield
#define SDCARD_CS_PIN    10
#define SDCARD_MOSI_PIN  7
#define SDCARD_SCK_PIN   14

// Use these with the Teensy 3.5 & 3.6 SD card
//#define SDCARD_CS_PIN    BUILTIN_SDCARD
//#define SDCARD_MOSI_PIN  11  // not actually used
//#define SDCARD_SCK_PIN   13  // not actually used

// Use these for the SD+Wiz820 or other adaptors
//#define SDCARD_CS_PIN    4
//#define SDCARD_MOSI_PIN  11
//#define SDCARD_SCK_PIN   13

#define NUM_FILES  4
const char *filenames[NUM_FILES]={"SDTEST1.WAV", "SDTEST2.WAV", "SDTEST3.WAV", "SDTEST4.WAV"};
int nextfile=0;

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

  pinMode(0, INPUT_PULLUP);
  pinMode(1, INPUT_PULLUP);
  pinMode(2, INPUT_PULLUP);

  sgtl5000_1.enable();
  sgtl5000_1.volume(0.5);

  mixer1.gain(0, 0.5);
  mixer1.gain(1, 0.5);

  // the Granular effect requires memory to operate
  granular1.begin(granularMemory, GRANULAR_MEMORY_SIZE);

  SPI.setMOSI(SDCARD_MOSI_PIN);
  SPI.setSCK(SDCARD_SCK_PIN);
  if (!(SD.begin(SDCARD_CS_PIN))) {
    // stop here, but print a message repetitively
    while (1) {
      Serial.println("Unable to access the SD card");
      delay(500);
    }
  }
}

void loop() {
  if (playSdWav1.isPlaying() == false) {
    // start the next song playing
    playSdWav1.play(filenames[nextfile]);
    Serial.print("Playing: ");
    Serial.println(filenames[nextfile]);
    delay(5); // brief delay for the library read WAV info
    nextfile = nextfile + 1;
    if (nextfile >= NUM_FILES) {
      nextfile = 0;
    }
  }

  // read pushbuttons
  button0.update();
  button1.update();
  button2.update();
  // read knobs, scale to 0-1.0 numbers
  float knobA2 = (float)analogRead(A2) / 1023.0;
  float knobA3 = (float)analogRead(A3) / 1023.0;

  // Button 0 starts Freeze effect
  if (button0.fallingEdge()) {
    float msec = 100.0 + (knobA3 * 190.0);
    granular1.beginFreeze(msec);
    Serial.print("Begin granular freeze using ");
    Serial.print(msec);
    Serial.println(" grains");
  }
  if (button0.risingEdge()) {
    granular1.stop();
  }

  // Button 1 starts Pitch Shift effect
  if (button1.fallingEdge()) {
    float msec = 25.0 + (knobA3 * 75.0);
    granular1.beginPitchShift(msec);
    Serial.print("Begin granular pitch phift using ");
    Serial.print(msec);
    Serial.println(" grains");
  }
  if (button1.risingEdge()) {
    granular1.stop();
  }

  // Continuously adjust the speed, based on the A3 pot
  float ratio;
  ratio = powf(2.0, knobA2 * 2.0 - 1.0); // 0.5 to 2.0
  //ratio = powf(2.0, knobA2 * 6.0 - 3.0); // 0.125 to 8.0 -- uncomment for far too much range!
  granular1.setSpeed(ratio);
}
 
Could be an idea to adapt the "Recorder" example code.

"Record sound as raw data to a SD card, and play it back."

Could trigger the playback function directly after the stop recording command comes in, and observe to see if the effect is, for lack of a better term "musically useful", as I'm guessing timing could be critical here. Just see how long the start playback function takes, if it's instantaneous then great! If not please report your findings!
 
I think with the granular effect it states in the code around 290ms which may be just under your "budget"
That's only because of the size defined for the 'granularMemory' array. You can make it larger.


I reckon with an added RAM chip in the extra memory slot would do the trick.
MUCH easier just to use a T3.5 or T3.6 with 256K RAM. Should be able to get >0.5 seconds with no problem.
 
That's only because of the size defined for the 'granularMemory' array. You can make it larger.

MUCH easier just to use a T3.5 or T3.6 with 256K RAM. Should be able to get >0.5 seconds with no problem.


My bad, first thing I did was double the 'granularMemory' number and threw out an error, tried to mess to find where the limit was, 20000 works so somewhere over 20000 and less than 25600 on a 3.2.

I didn't know 3.5/3.6 had that much RAM, I guess that's the easier way if you have the luxury of time and money to order a new board, vs the obviously ludicrous cost of ordering a single chip.

I apologise your emboldened text has lead me to question my life's choices.
 
I should have quantified "much easier". I meant "much easier" in the sense that what you proposed to do is impossible. The processor on the T3.2 (like most stand-alone microcontrollers) does not have the external Address / Data bus required to add a RAM chip. Sure, you can connect various other types of memory (SPI, I2C, SD, perhaps USB). But nothing that's even close to fast enough that it could augment the internal RAM and run at the speed required for the Audio library. Or, even exist in the same address space that the Audio Library accesses.
 
Last edited:
I should have quantified "much easier". I meant "much easier" in the sense that what you proposed to do is impossible. The processor on the T3.2 (like most stand-alone microcontrollers) does not have the external Address / Data bus required to add a RAM chip. Sure, you can connect various other types of memory (SPI, I2C, SD, perhaps USB). But nothing that's even close to fast enough that it could augment the internal RAM and run at the speed required for the Audio library. Or, even exist in the same address space that the Audio Library accesses.

Wow ok maybe I made a mistake when I ordered a "23LC1024" to attach to the audio adapter for the Teensy 3.2; like I've seen Paul and many others suggest; maybe I've slipped into a different dimension where the memory board doesn't exist and damn it guys... just for your interest; it's not pleasant!!! Resurrect the memory man!

Money wasted again from my ignorance. Gfvolvo thanks for tat
 
But nothing that's even close to fast enough that it could augment the internal RAM and run at the speed required for the Audio library.

This really depends on what "even close to fast enough" really means.

The audio library does indeed support using the 23LC1024 RAM chip for an audio delay effect. The SPI communication does have a lot of overhead, even for this limited use case, but it does work well.

But yeah, the SPI communication overhead is huge compared to accessing the on-chip RAM. That's why delay is the only supported effect, and if any others are ever written they'll probably be limited to recording & playback of short samples. The overhead is just too much for anything where access to the RAM isn't so simple.

However, saying it's not even close to fast enough ignores the fact that it is indeed fast enough (with imposing substantial overhead) to implement the delay effect.
 
Correct, SPI-RAM is more than fast enough. Some years ago I used a SPI-Flash it to play > 15(?) Samples simultanously. Don't remember if I even used the max SPI speed..
 
...That's why delay is the only supported effect, and if any others are ever written they'll probably be limited to recording & playback of short samples...

Volvo may still be correct then (although not about the "impossible" statement)

If 'Delay' is the only supported function then I assume trying to use the RAM chip in conjunction with the 'Granular' isn't going to work then?

Also I'll just say sorry to the OP, I tried to answer an old thread with no replies, I didn't mean to unleash the floodgates
 
Well, if you're not willing to write code for it, yes in this case it's not working - for you.

Low level code or high level arduino code?

Willing to write code if I don't need Paul's skills to implement or support the chip in the granular function.

"yes in this case it's not working - for you."

Implies slightly different things to:

"delay is the only supported effect"

I need to know what "Supported" really means in this context and at what level actions need to be taken place to support the extra memory in the granular function.

I haven't got my chip yet, ordered, but haven't been able to play around with it just yet, just researching and considering possibilities. Willing to code the support for granular and invest my free time to help out if I can; that is when the chip does arrive.
 
I stand corrected. I should have been more specific by saying that simply adding the external SPI RAM chip would not allow you to increase the size of the 'granularMemory[]' array in the 'Granular Effect Example' beyond what can be accommodated by the Teensy's internal RAM. This is because the AudioEffectGranular class is not written to use the external SPI RAM chip.

I've since looked over the source code for the 'AudioEffectDelayExternal' class. It seems within the realm of possibility that the same techniques could be used to develop a 'AudioEffectGranularExternal' class that uses the external SPI RAM for its storage buffer.
 
Last edited:
I stand corrected. I should have been more specific by saying that simply adding the external SPI RAM chip would not allow you to increase the size of the 'granularMemory[]' array in the 'Granular Effect Example' beyond what can be accommodated by the Teensy's internal RAM. This is because the AudioEffectGranular class is not written to use the external SPI RAM chip.

I've since looked over the source code for the 'AudioEffectDelayExternal' class. It seems within the realm of possibility that the same techniques could be used to develop a 'AudioEffectGranularExternal' class that uses the external SPI RAM for its storage buffer.

Playing spot the difference with Effect_delay_ext / Effect__delay / effect_granular over here but looks fairly baffling to me.

Looks like the Granular effect uses this audio_block_t in a similar way to effect_delay, is that why you think a granular_ext would be possible?

Delay_ext seems really complicated, there’s a lot going on, the last parts seem to be slightly changed duplicates to account for the different types of extra mem 23LC / CY15B and memory board. In part I can see talking to to SPI through the MISO/MOSI/SCK and chucking the audio block into it.

As for the actual delay function seems to be radically different to the actual effect_delay, and seems a bit baffling how it’s operating. I wonder whether the function of granular would need to be re-written or this could be a straight forward cut and paste job.

Looking at the actual delay function makes me think if it’s just an array storing samples then maybe that array could be manipulated and sorted in weird and unusual ways. For instance nonlinear playback of samples, or randomly playing within clusters of samples/frames. Could be quite an interesting route to explore.
 
Looks like the Granular effect uses this audio_block_t in a similar way to effect_delay, is that why you think a granular_ext would be possible?
All of the Audio Library classes use the audio_block_t type to get their input data samples from preceding blocks and send their processed data samples to subsequent blocks. The trick is what they do in between to presses the data.

I spent some time a couple of months ago studying the AudioEffectGranular class to learn how it works. Fairly straight forward, mostly careful tracking of read / write indices into several buffers -- same as the various delay classes. Much simpler than say the input / output classes that use DMA -- took me quite a while in the weeds to understand those.
 
Perhaps that's your cue to simply buy Teensy 3.6?

I don't know why it should be. Is there a reason it should be? I've already got three Teensy's I don't use, how bout a trade in?

Have you checked out the Axoloti?

Teensy 3.6 + audio shield doesn't really compare. For a dedicated audio project that would be a far easier route.

They even have a granular pitch shifting delay, this is what I've been thinking about how to make with the teensy!

Anyway back to the task at hand.

Started having a go at combining the delay_ext and granular into granular_ext.cpp

Code:
/*
 * Copyright (c) 2018 John-Michael Reed
 * bleeplabs.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <Arduino.h>
#include "effect_granular_ext.h”

//#define INTERNAL_TEST

// While 20 MHz (Teensy actually uses 16 MHz in most cases) and even 24 MHz
// have worked well in testing at room temperature with 3.3V power, to fully
// meet all the worst case timing specs, the SPI clock low time would need
// to be 40ns (12.5 MHz clock) for the single chip case and 51ns (9.8 MHz
// clock) for the 6-chip memoryboard with 74LCX126 buffers.
//

#define SPISETTING SPISettings(20000000, MSBFIRST, SPI_MODE0)

// Use these with the audio adaptor board  (should be adjustable by the user...)
#define SPIRAM_MOSI_PIN  7
#define SPIRAM_MISO_PIN  12
#define SPIRAM_SCK_PIN   14

#define SPIRAM_CS_PIN    6

#define MEMBOARD_CS0_PIN 2
#define MEMBOARD_CS1_PIN 3
#define MEMBOARD_CS2_PIN 4

void AudioEffectGranularExternal::begin(int16_t *sample_bank_def, int16_t max_len_def)
{
	max_sample_len = max_len_def;
	grain_mode = 0;
	read_head = 0;
	write_head = 0;
	prev_input = 0;
	playpack_rate = 65536;
	accumulator = 0;
	allow_len_change = true;
	sample_loaded = false;
	sample_bank = sample_bank_def;
}

void AudioEffectGranularExternal::beginFreeze_int(int grain_samples)
{
	__disable_irq();
	grain_mode = 1;
	if (grain_samples < max_sample_len) {
		freeze_len = grain_samples;
	} else {
		freeze_len = grain_samples;
	}
	sample_loaded = false;
	write_en = false;
	sample_req = true;
	__enable_irq();
}

void AudioEffectGranularExternal::beginPitchShift_int(int grain_samples)
{
	__disable_irq();
	grain_mode = 2;
	if (allow_len_change) {
		if (grain_samples < 100) grain_samples = 100;
		int maximum = (max_sample_len - 1) / 3;
		if (grain_samples > maximum) grain_samples = maximum;
		glitch_len = grain_samples;
	}
	sample_loaded = false;
	write_en = false;
	sample_req = true;
	__enable_irq();
}

void AudioEffectGranularExternal::stop()
{
	grain_mode = 0;
	allow_len_change = true;
}

void AudioEffectGranularExternal::update(void)
{
	audio_block_t *block;

	// This is where the magic needs to happen

	if (sample_bank == NULL) {
		//block = receiveReadOnly(0);

		//if (block) release(block); // Copied the following from delay_ext:

		block = receiveReadOnly();
	if (memory_type >= AUDIO_MEMORY_UNDEFINED) {
		// ignore input and do nothing if undefined memory type
		release(block);
		return;
	}
	if (block) {
		if (head_offset + AUDIO_BLOCK_SAMPLES <= memory_length) {
			// a single write is enough
			write(head_offset, AUDIO_BLOCK_SAMPLES, block->data);
			head_offset += AUDIO_BLOCK_SAMPLES;
		} else {
			// write wraps across end-of-memory
			n = memory_length - head_offset;
			write(head_offset, n, block->data);
			head_offset = AUDIO_BLOCK_SAMPLES - n;
			write(0, head_offset, block->data + n);
		}
		release(block);
	} else {
		// if no input, store zeros, so later playback will
		// not be random garbage previously stored in memory
		if (head_offset + AUDIO_BLOCK_SAMPLES <= memory_length) {
			zero(head_offset, AUDIO_BLOCK_SAMPLES);
			head_offset += AUDIO_BLOCK_SAMPLES;
		} else {
			n = memory_length - head_offset;
			zero(head_offset, n);
			head_offset = AUDIO_BLOCK_SAMPLES - n;
			zero(0, head_offset);
		}
	}

		return; // ?
	}

	block = receiveWritable(0); // receiveWritable?
	if (!block) return;

	// I don’t understand enough of how the code works to merge these functions together

	if (grain_mode == 0) 
{
		// passthrough, no granular effect
		prev_input = block->data[AUDIO_BLOCK_SAMPLES-1];
	}
	else if (grain_mode == 1) {
		// Freeze - sample 1 grain, then repeatedly play it back
		for (int j = 0; j < AUDIO_BLOCK_SAMPLES; j++) {
			if (sample_req) {
				// only begin capture on zero cross
				int16_t current_input = block->data[j];
				if ((current_input < 0 && prev_input >= 0) ||
				  (current_input >= 0 && prev_input < 0)) {
					write_en = true;
					write_head = 0;
					read_head = 0;
					sample_req = false;
				} else {
					prev_input = current_input;
				}
			}
			if (write_en) {
				sample_bank[write_head++] = block->data[j];
				if (write_head >= freeze_len) {
					sample_loaded = true;
				}
				if (write_head >= max_sample_len) {
					write_en = false;
				}
			}
			if (sample_loaded) {
				if (playpack_rate >= 0) {
					accumulator += playpack_rate;
					read_head = accumulator >> 16;
				}
				if (read_head >= freeze_len) {
					accumulator = 0;
					read_head = 0;
				}
				block->data[j] = sample_bank[read_head];
			}
		}
	}
	else if (grain_mode == 2) {
		//GLITCH SHIFT
		//basic granular synth thingy
		// the shorter the sample the max_sample_len the more tonal it is.
		// Longer it has more definition.  It's a bit roboty either way which
		// is obv great and good enough for noise music.

		for (int k = 0; k < AUDIO_BLOCK_SAMPLES; k++) {
			// only start recording when the audio is crossing zero to minimize pops
			if (sample_req) {
				int16_t current_input = block->data[k];
				if ((current_input < 0 && prev_input >= 0) ||
				  (current_input >= 0 && prev_input < 0)) {
					write_en = true;
				} else {
					prev_input = current_input;
				}
			}

			if (write_en) {
				sample_req = false;
				allow_len_change = true; // Reduces noise by not allowing the
						// length to change after the sample has been
						// recored.  Kind of not too much though
				if (write_head >= glitch_len) {
					write_head = 0;
					sample_loaded = true;
					write_en = false;
					allow_len_change = false;
				}
				sample_bank[write_head] = block->data[k];
				write_head++;
			}

			if (sample_loaded) {
				//move it to the middle third of the bank.
				//3 "seperate" banks are used
				float fade_len = 20.00;
				int16_t m2 = fade_len;

				for (int m = 0; m < 2; m++) {
					// I'm off by one somewhere? why is there a tick at the
					// beginning of this only when it's combined with the
					// fade out???? ooor am i osbserving that incorrectly
					// either wait it works enough
					sample_bank[m + glitch_len] = 0;
				}

				for (int m = 2; m < glitch_len-m2; m++) {
					sample_bank[m + glitch_len] = sample_bank[m];
				}

				for (int m = glitch_len-m2; m < glitch_len; m++) {
					// fade out the end. You can just make fadet=0
					// but it's a little too daleky
					float fadet = sample_bank[m] * (m2 / fade_len);
					sample_bank[m + glitch_len] = (int16_t)fadet;
					m2--;
				}
				sample_loaded = false;
				prev_input = block->data[k];
				sample_req = true;
			}

			accumulator += playpack_rate;
			read_head = (accumulator >> 16);

			if (read_head >= glitch_len) {
				read_head -= glitch_len;
				accumulator = 0;

				for (int m = 0; m < glitch_len; m++) {
					sample_bank[m + (glitch_len*2)] = sample_bank[m+glitch_len];
					//  sample_bank[m + (glitch_len*2)] = (m%20)*1000;
				}
			}
			block->data[k] = sample_bank[read_head + (glitch_len*2)];
		}
	}
	transmit(block);
	release(block);
}

// Notes: AudioEffectGranularExternal needs Initialise, Read and Write functions to talk to the SPI memory chip. What’s happening and how to get it out and mapped back into the granular function is not the easiest for me to work out. 

//Also unsure what this line means in the delay code:
//uint32_t AudioEffectDelayExternal::allocated[2] = {0, 0};

//Granular function looks like it’s moving the samples between three different banks, sample_req (Record?), write_en and sample_loaded. Record, write and loaded I suspect it should be re-written after the “sample_bank == NULL” part whether is still needs wrapping in that section


void AudioEffectGranularExternal::initialize(AudioEffectDelayMemoryType_t type, uint32_t samples)
{
	uint32_t memsize, avail;

	activemask = 0;
	head_offset = 0;
	memory_type = type;

	SPI.setMOSI(SPIRAM_MOSI_PIN);
	SPI.setMISO(SPIRAM_MISO_PIN);
	SPI.setSCK(SPIRAM_SCK_PIN);

	SPI.begin();	
	
	if (type == AUDIO_MEMORY_23LC1024) {
#ifdef INTERNAL_TEST
		memsize = 8000;
#else
		memsize = 65536;
#endif
		pinMode(SPIRAM_CS_PIN, OUTPUT);
		digitalWriteFast(SPIRAM_CS_PIN, HIGH);
	} else if (type == AUDIO_MEMORY_MEMORYBOARD) {
		memsize = 393216;
		pinMode(MEMBOARD_CS0_PIN, OUTPUT);
		pinMode(MEMBOARD_CS1_PIN, OUTPUT);
		pinMode(MEMBOARD_CS2_PIN, OUTPUT);
		digitalWriteFast(MEMBOARD_CS0_PIN, LOW);
		digitalWriteFast(MEMBOARD_CS1_PIN, LOW);
		digitalWriteFast(MEMBOARD_CS2_PIN, LOW);		
	} else if (type == AUDIO_MEMORY_CY15B104) {
#ifdef INTERNAL_TEST
		memsize = 8000;
#else		
		memsize = 262144;
#endif	
		pinMode(SPIRAM_CS_PIN, OUTPUT);
		digitalWriteFast(SPIRAM_CS_PIN, HIGH);
			
	} else {
		return;
	}
	avail = memsize - allocated[type];
	if (avail < AUDIO_BLOCK_SAMPLES*2+1) {
		memory_type = AUDIO_MEMORY_UNDEFINED;
		return;
	}
	if (samples > avail) samples = avail;
	memory_begin = allocated[type];
	allocated[type] += samples;
	memory_length = samples;

	zero(0, memory_length);
}


#ifdef INTERNAL_TEST
static int16_t testmem[8000]; // testing only
#endif

void AudioEffectGranularExternal::read(uint32_t offset, uint32_t count, int16_t *data)
{
	uint32_t addr = memory_begin + offset;

#ifdef INTERNAL_TEST
	while (count) { *data++ = testmem[addr++]; count--; } // testing only
#else
	if (memory_type == AUDIO_MEMORY_23LC1024 || 
		memory_type == AUDIO_MEMORY_CY15B104) {
		addr *= 2;
		SPI.beginTransaction(SPISETTING);
		digitalWriteFast(SPIRAM_CS_PIN, LOW);
		SPI.transfer16((0x03 << 8) | (addr >> 16));
		SPI.transfer16(addr & 0xFFFF);
		while (count) {
			*data++ = (int16_t)(SPI.transfer16(0));
			count--;
		}
		digitalWriteFast(SPIRAM_CS_PIN, HIGH);
		SPI.endTransaction();
	} else if (memory_type == AUDIO_MEMORY_MEMORYBOARD) {
		SPI.beginTransaction(SPISETTING);
		while (count) {
			uint32_t chip = (addr >> 16) + 1;
			digitalWriteFast(MEMBOARD_CS0_PIN, chip & 1);
			digitalWriteFast(MEMBOARD_CS1_PIN, chip & 2);
			digitalWriteFast(MEMBOARD_CS2_PIN, chip & 4);
			uint32_t chipaddr = (addr & 0xFFFF) << 1;
			SPI.transfer16((0x03 << 8) | (chipaddr >> 16));
			SPI.transfer16(chipaddr & 0xFFFF);
			uint32_t num = 0x10000 - (addr & 0xFFFF);
			if (num > count) num = count;
			count -= num;
			addr += num;
			do {
				*data++ = (int16_t)(SPI.transfer16(0));
			} while (--num > 0);
		}
		digitalWriteFast(MEMBOARD_CS0_PIN, LOW);
		digitalWriteFast(MEMBOARD_CS1_PIN, LOW);
		digitalWriteFast(MEMBOARD_CS2_PIN, LOW);
		SPI.endTransaction();
	}
#endif
}

void AudioEffectGranularExternal::write(uint32_t offset, uint32_t count, const int16_t *data)
{
	uint32_t addr = memory_begin + offset;

#ifdef INTERNAL_TEST
	while (count) { testmem[addr++] = *data++; count--; } // testing only
#else
	if (memory_type == AUDIO_MEMORY_23LC1024) {
		addr *= 2;
		SPI.beginTransaction(SPISETTING);
		digitalWriteFast(SPIRAM_CS_PIN, LOW);
		SPI.transfer16((0x02 << 8) | (addr >> 16));
		SPI.transfer16(addr & 0xFFFF);
		while (count) {
			int16_t w = 0;
			if (data) w = *data++;
			SPI.transfer16(w);
			count--;
		}
		digitalWriteFast(SPIRAM_CS_PIN, HIGH);
		SPI.endTransaction();
	} else if (memory_type == AUDIO_MEMORY_CY15B104) {
		addr *= 2;

		SPI.beginTransaction(SPISETTING);
		digitalWriteFast(SPIRAM_CS_PIN, LOW);
		SPI.transfer(0x06); //write-enable before every write
		digitalWriteFast(SPIRAM_CS_PIN, HIGH);
		asm volatile ("NOP\n NOP\n NOP\n NOP\n NOP\n NOP\n");
		digitalWriteFast(SPIRAM_CS_PIN, LOW);
		SPI.transfer16((0x02 << 8) | (addr >> 16));
		SPI.transfer16(addr & 0xFFFF);
		while (count) {
			int16_t w = 0;
			if (data) w = *data++;
			SPI.transfer16(w);
			count--;
		}
		digitalWriteFast(SPIRAM_CS_PIN, HIGH);
		SPI.endTransaction();	
	} else if (memory_type == AUDIO_MEMORY_MEMORYBOARD) {		
		SPI.beginTransaction(SPISETTING);
		while (count) {
			uint32_t chip = (addr >> 16) + 1;
			digitalWriteFast(MEMBOARD_CS0_PIN, chip & 1);
			digitalWriteFast(MEMBOARD_CS1_PIN, chip & 2);
			digitalWriteFast(MEMBOARD_CS2_PIN, chip & 4);
			uint32_t chipaddr = (addr & 0xFFFF) << 1;
			SPI.transfer16((0x02 << 8) | (chipaddr >> 16));
			SPI.transfer16(chipaddr & 0xFFFF);
			uint32_t num = 0x10000 - (addr & 0xFFFF);
			if (num > count) num = count;
			count -= num;
			addr += num;
			do {
				int16_t w = 0;
				if (data) w = *data++;
				SPI.transfer16(w);
			} while (--num > 0);
		}
		digitalWriteFast(MEMBOARD_CS0_PIN, LOW);
		digitalWriteFast(MEMBOARD_CS1_PIN, LOW);
		digitalWriteFast(MEMBOARD_CS2_PIN, LOW);
		SPI.endTransaction();
	}
#endif
}
 
Perhaps that's your cue to simply buy Teensy 3.6?

In reference to baffling. Was trying to give credit to the awesome work by saying it's a work of complexity. If you read that as literally baffling then I apologise for not being explicit enough.

it's a work of great artisitic merit, I don't understand all of it, but the way it functions properly is bafllingly awesome. A processor running at 96Mhz a second storing, reading and manipulating frames of audio enough to fool our ear drums into believing it's a continuous signal is for lack of a better term fucking magic.
 
Have you checked out the Axoloti?

Only a quick cursory look. I see that board has a SDRAM chip, which is presumably memory mapped. But I don't see any support for SPI interfaced RAM like 23LC1024.

The reality of SPI RAM like 23LC1024 is a pretty substantial overhead for any access, including stating and ending the SPI transaction, asserting and de-asserting chip select, and 4 bytes to send a command and address. Then after all that overhead, each byte takes 8 clocks of the SPI at 24 MHz. That's a lot slower than read or writing directly from the internal memory. It also means you get to access only one continuous range, starting at that 24 bit address.

As the external delay proves, it is fast enough to do several audio streams before you consume all the CPU time. But that's suffering the overhead only once or twice, for reading only 1 range of data for the new incoming audio to be stored, and 1 range of data for the previously stored (now delayed) audio to be read.

If you're going to start from the existing code, probably your very first step should be to delete all the AUDIO_MEMORY_MEMORYBOARD and AUDIO_MEMORY_CY15B104 stuff, so you're working with much simpler code that accesses only a single 23LC1024 chip. When I wrote the original external delay, that's how it was created. All that extra stuff for different hardware came later. Get rid of it to keep things simple. After you get things working, and presumably by then you'll have a much better understanding, you can put that stuff back in. Like all programming, best to keep things as simple as reasonably possible when you're struggling to just get it working at all.
 
Only a quick cursory look. I see that board has a SDRAM chip, which is presumably memory mapped. But I don't see any support for SPI interfaced RAM like 23LC1024.

The reality of SPI RAM like 23LC1024 is a pretty substantial overhead for any access, including stating and ending the SPI transaction, asserting and de-asserting chip select, and 4 bytes to send a command and address. Then after all that overhead, each byte takes 8 clocks of the SPI at 24 MHz. That's a lot slower than read or writing directly from the internal memory. It also means you get to access only one continuous range, starting at that 24 bit address.

As the external delay proves, it is fast enough to do several audio streams before you consume all the CPU time. But that's suffering the overhead only once or twice, for reading only 1 range of data for the new incoming audio to be stored, and 1 range of data for the previously stored (now delayed) audio to be read.

If you're going to start from the existing code, probably your very first step should be to delete all the AUDIO_MEMORY_MEMORYBOARD and AUDIO_MEMORY_CY15B104 stuff, so you're working with much simpler code that accesses only a single 23LC1024 chip. When I wrote the original external delay, that's how it was created. All that extra stuff for different hardware came later. Get rid of it to keep things simple. After you get things working, and presumably by then you'll have a much better understanding, you can put that stuff back in. Like all programming, best to keep things as simple as reasonably possible when you're struggling to just get it working at all.

Thank you very much, I really appreciate that, after looking at another thread you shared this link: https://www.pjrc.com/teensy/td_libs_AudioNewObjects.html so I've just been going over it as well.

It was actually my intuition to take out all the parts that were for the memory board and other mem chip, really good to know; keep it simple.

in ref to the Axoloti I meant have you checked it out as it looks like an utter beast, iirc part funded by a government initiative, looks pretty packed with everything and more to do ridiculously sophisticated projects 96k/24bit 8mb (iirc) of RAM. It's like a teensy with an audio shield and all the interface gubbings and an intuitive modular environment where no coding is required.

Just did as you said and tidied up the code
Code:
/*
 * Copyright (c) 2018 John-Michael Reed
 * bleeplabs.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <Arduino.h>
#include "effect_granular_ext.h”

//#define INTERNAL_TEST

// While 20 MHz (Teensy actually uses 16 MHz in most cases) and even 24 MHz
// have worked well in testing at room temperature with 3.3V power, to fully
// meet all the worst case timing specs, the SPI clock low time would need
// to be 40ns (12.5 MHz clock) for the single chip case and 51ns (9.8 MHz
// clock) for the 6-chip memoryboard with 74LCX126 buffers.
//

#define SPISETTING SPISettings(20000000, MSBFIRST, SPI_MODE0)

// Use these with the audio adaptor board  (should be adjustable by the user...)
#define SPIRAM_MOSI_PIN  7
#define SPIRAM_MISO_PIN  12
#define SPIRAM_SCK_PIN   14

#define SPIRAM_CS_PIN    6

void AudioEffectGranularExternal::begin(int16_t *sample_bank_def, int16_t max_len_def)
{
	max_sample_len = max_len_def;
	grain_mode = 0;
	read_head = 0;
	write_head = 0;
	prev_input = 0;
	playpack_rate = 65536;
	accumulator = 0;
	allow_len_change = true;
	sample_loaded = false;
	sample_bank = sample_bank_def;
}

void AudioEffectGranularExternal::beginFreeze_int(int grain_samples)
{
	__disable_irq();
	grain_mode = 1;
	if (grain_samples < max_sample_len) {
		freeze_len = grain_samples;
	} else {
		freeze_len = grain_samples;
	}
	sample_loaded = false;
	write_en = false;
	sample_req = true;
	__enable_irq();
}

void AudioEffectGranularExternal::beginPitchShift_int(int grain_samples)
{
	__disable_irq();
	grain_mode = 2;
	if (allow_len_change) {
		if (grain_samples < 100) grain_samples = 100;
		int maximum = (max_sample_len - 1) / 3;
		if (grain_samples > maximum) grain_samples = maximum;
		glitch_len = grain_samples;
	}
	sample_loaded = false;
	write_en = false;
	sample_req = true;
	__enable_irq();
}

void AudioEffectGranularExternal::stop()
{
	grain_mode = 0;
	allow_len_change = true;
}

void AudioEffectGranularExternal::update(void)
{
	audio_block_t *block;
	// This is where the magic needs to happen
	if (sample_bank == NULL) // If Sample bank is empty then...
	{
		//block = receiveReadOnly(0);
		//if (block) release(block); // Copied the following from delay_ext:
		block = receiveReadOnly();

	if (block) {
		if (head_offset + AUDIO_BLOCK_SAMPLES <= memory_length) {
			// a single write is enough
			write(head_offset, AUDIO_BLOCK_SAMPLES, block->data);
			head_offset += AUDIO_BLOCK_SAMPLES;
		} else {
			// write wraps across end-of-memory
			n = memory_length - head_offset; // head offset and memory length ??
			write(head_offset, n, block->data); // are they similar/related to write_head and read_head??
			head_offset = AUDIO_BLOCK_SAMPLES - n;
			write(0, head_offset, block->data + n);
		}
		release(block);
	} else {
		// if no input, store zeros, so later playback will
		// not be random garbage previously stored in memory
		if (head_offset + AUDIO_BLOCK_SAMPLES <= memory_length) {
			zero(head_offset, AUDIO_BLOCK_SAMPLES);
			head_offset += AUDIO_BLOCK_SAMPLES;
		} else {
			n = memory_length - head_offset;
			zero(head_offset, n);
			head_offset = AUDIO_BLOCK_SAMPLES - n;
			zero(0, head_offset);
		}
	} //
		return; // ?
	}

	block = receiveWritable(0); // receiveWritable?
	if (!block) return;
	// I don’t understand enough of how the code works to merge these functions together

	if (grain_mode == 0)
	{
		// passthrough, no granular effect
		prev_input = block->data[AUDIO_BLOCK_SAMPLES-1];
	}

	else if (grain_mode == 1)
	{
		// Freeze - sample 1 grain, then repeatedly play it back
		for (int j = 0; j < AUDIO_BLOCK_SAMPLES; j++) {
			if (sample_req) {
				// only begin capture on zero cross
				int16_t current_input = block->data[j];
				if ((current_input < 0 && prev_input >= 0) ||
				  (current_input >= 0 && prev_input < 0)) {
					write_en = true;
					write_head = 0;
					read_head = 0;
					sample_req = false;
				} else {
					prev_input = current_input;
				}
			}
			if (write_en) {
				sample_bank[write_head++] = block->data[j];
				if (write_head >= freeze_len) {
					sample_loaded = true;
				}
				if (write_head >= max_sample_len) {
					write_en = false;
				}
			}
			if (sample_loaded) {
				if (playpack_rate >= 0) {
					accumulator += playpack_rate;
					read_head = accumulator >> 16;
				}
				if (read_head >= freeze_len) {
					accumulator = 0;
					read_head = 0;
				}
				block->data[j] = sample_bank[read_head];
			}
		}
	}
	else if (grain_mode == 2) {
		//GLITCH SHIFT
		//basic granular synth thingy
		// the shorter the sample the max_sample_len the more tonal it is.
		// Longer it has more definition.  It's a bit roboty either way which
		// is obv great and good enough for noise music.

		for (int k = 0; k < AUDIO_BLOCK_SAMPLES; k++) {
			// only start recording when the audio is crossing zero to minimize pops
			if (sample_req) {
				int16_t current_input = block->data[k];
				if ((current_input < 0 && prev_input >= 0) ||
				  (current_input >= 0 && prev_input < 0)) {
					write_en = true;
				} else {
					prev_input = current_input;
				}
			}

			if (write_en) {
				sample_req = false;
				allow_len_change = true; // Reduces noise by not allowing the
						// length to change after the sample has been
						// recored.  Kind of not too much though
				if (write_head >= glitch_len) {
					write_head = 0;
					sample_loaded = true;
					write_en = false;
					allow_len_change = false;
				}
				sample_bank[write_head] = block->data[k];
				write_head++;
			}

			if (sample_loaded) {
				//move it to the middle third of the bank.
				//3 "seperate" banks are used
				float fade_len = 20.00;
				int16_t m2 = fade_len;

				for (int m = 0; m < 2; m++) {
					// I'm off by one somewhere? why is there a tick at the
					// beginning of this only when it's combined with the
					// fade out???? ooor am i osbserving that incorrectly
					// either wait it works enough
					sample_bank[m + glitch_len] = 0;
				}

				for (int m = 2; m < glitch_len-m2; m++) {
					sample_bank[m + glitch_len] = sample_bank[m];
				}

				for (int m = glitch_len-m2; m < glitch_len; m++) {
					// fade out the end. You can just make fadet=0
					// but it's a little too daleky
					float fadet = sample_bank[m] * (m2 / fade_len);
					sample_bank[m + glitch_len] = (int16_t)fadet;
					m2--;
				}
				sample_loaded = false;
				prev_input = block->data[k];
				sample_req = true;
			}

			accumulator += playpack_rate;
			read_head = (accumulator >> 16);

			if (read_head >= glitch_len) {
				read_head -= glitch_len;
				accumulator = 0;

				for (int m = 0; m < glitch_len; m++) {
					sample_bank[m + (glitch_len*2)] = sample_bank[m+glitch_len];
					//  sample_bank[m + (glitch_len*2)] = (m%20)*1000;
				}
			}
			block->data[k] = sample_bank[read_head + (glitch_len*2)];
		}
	}
	transmit(block);
	release(block);
}

// Notes: AudioEffectGranularExternal needs Initialise, Read and Write functions to talk to the SPI memory chip. What’s happening and how to get it out and mapped back into the granular function is not the easiest for me to work out.

//Also unsure what this line means in the delay code:
//uint32_t AudioEffectDelayExternal::allocated[2] = {0, 0};

//Granular function looks like it’s moving the samples between three different banks, sample_req (Record?), write_en and sample_loaded. Record, write and loaded I suspect it should be re-written after the “sample_bank == NULL” part whether is still needs wrapping in that section

void AudioEffectGranularExternal::initialize(AudioEffectDelayMemoryType_t type, uint32_t samples)
{
	uint32_t memsize, avail;

	activemask = 0;
	head_offset = 0;
	memory_type = type;

	SPI.setMOSI(SPIRAM_MOSI_PIN);
	SPI.setMISO(SPIRAM_MISO_PIN);
	SPI.setSCK(SPIRAM_SCK_PIN);

	SPI.begin();

#ifdef INTERNAL_TEST
		memsize = 8000;
#else
		memsize = 65536;
#endif
		pinMode(SPIRAM_CS_PIN, OUTPUT);
		digitalWriteFast(SPIRAM_CS_PIN, HIGH);

	avail = memsize - allocated[type];
	if (avail < AUDIO_BLOCK_SAMPLES*2+1) {
		memory_type = AUDIO_MEMORY_UNDEFINED;
		return;
	}
	if (samples > avail) samples = avail;
	memory_begin = allocated[type];
	allocated[type] += samples;
	memory_length = samples;

	zero(0, memory_length);
}


#ifdef INTERNAL_TEST
static int16_t testmem[8000]; // testing only
#endif

void AudioEffectGranularExternal::read(uint32_t offset, uint32_t count, int16_t *data)
{
	uint32_t addr = memory_begin + offset;

#ifdef INTERNAL_TEST
	while (count) { *data++ = testmem[addr++]; count--; } // testing only
#else

		addr *= 2;
		SPI.beginTransaction(SPISETTING);
		digitalWriteFast(SPIRAM_CS_PIN, LOW);
		SPI.transfer16((0x03 << 8) | (addr >> 16));
		SPI.transfer16(addr & 0xFFFF);
		while (count) {
			*data++ = (int16_t)(SPI.transfer16(0));
			count--;
		}
		digitalWriteFast(SPIRAM_CS_PIN, HIGH);
		SPI.endTransaction();
#endif
}

void AudioEffectGranularExternal::write(uint32_t offset, uint32_t count, const int16_t *data)
{
	uint32_t addr = memory_begin + offset;

#ifdef INTERNAL_TEST
	while (count) { testmem[addr++] = *data++; count--; } // testing only
#else
		addr *= 2;
		SPI.beginTransaction(SPISETTING);
		digitalWriteFast(SPIRAM_CS_PIN, LOW);
		SPI.transfer16((0x02 << 8) | (addr >> 16));
		SPI.transfer16(addr & 0xFFFF);
		while (count) {
			int16_t w = 0;
			if (data) w = *data++;
			SPI.transfer16(w);
			count--;
		}
		digitalWriteFast(SPIRAM_CS_PIN, HIGH);
		SPI.endTransaction();

}

here's the effect_granular_ext.h file as well:

Code:
/*
 * Copyright (c) 2018 John-Michael Reed
 * bleeplabs.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#ifndef effect_granular_ext_h_
#define effect_granular_ext_h_
#include "Arduino.h"
#include "spi_interrupt.h"
#include "AudioStream.h"

class AudioEffectGranularExt : public AudioStream
{
public:
	AudioEffectGranularExt(void): AudioStream(1,inputQueueArray) { }
	void begin(int16_t *sample_bank_def, int16_t max_len_def);
	void setSpeed(float ratio) {
		if (ratio < 0.125) ratio = 0.125;
		else if (ratio > 8.0) ratio = 8.0;
		playpack_rate = ratio * 65536.0 + 0.499;
	}
	void beginFreeze(float grain_length) {
		if (grain_length <= 0.0) return;
		beginFreeze_int(grain_length * (AUDIO_SAMPLE_RATE_EXACT * 0.001) + 0.5);
	}
	void beginPitchShift(float grain_length) {
		if (grain_length <= 0.0) return;
		beginPitchShift_int(grain_length * (AUDIO_SAMPLE_RATE_EXACT * 0.001) + 0.5); 
	}
	void stop();
	virtual void update(void);
private:
	void beginFreeze_int(int grain_samples);
	void beginPitchShift_int(int grain_samples);
	audio_block_t *inputQueueArray[1];
	int16_t *sample_bank;
	uint32_t playpack_rate;
	uint32_t accumulator;
	int16_t max_sample_len;
	int16_t write_head;
	int16_t read_head;
	int16_t grain_mode;
	int16_t freeze_len;
	int16_t prev_input;
	int16_t glitch_len;
	bool allow_len_change;
	bool sample_loaded;
	bool write_en;
	bool sample_req;
};
 
Paul,
What memory does your delay class use? I thought about trying to edit that since the sample time is long enough for what I am trying to do and it does not require external hardware.

Thanks
 
Status
Not open for further replies.
Back
Top