getting started with library writing - samples vs blocks and AUDIO_BLOCK_SAMPLES

Status
Not open for further replies.

quarterturn

Well-known member
I'd like to translate my Arduino Due 'string ensemble chorus' effect to an Audio Library-compatible library, but I could use a few pointers.

First, my Due code deals with samples as they come in via a 44.1 KHz interrupt, but in the Audio Library it's the 'update' method getting called somehow. Does that happen at the samplerate? I'll need to know that also since I'm targeting a Teensy 3.6 and will probably calculate my LFO on the fly (should be fast enough) and that depends on samples per second, or however often 'update' gets called. I need three phases of LFO separated by 2/3*pi radians, each a sum of two sine functions, will that be too much math per update?

Second, on the Due I process a sample at a time, but in the Audio Library it's blocks. What size are blocks? Is it 'AUDIO_BLOCK_SAMPLES'? I need a certain amount of buffer space to wiggle around my delay tap pointers, and I suppose it should be calculated based on the samplerate used.

Finally, I'd like to make the effect stereo, so I need to figure out how that's handled in the Audio Library. I guess it'd be stereo based on a phase adjustment of the LFO for one of the channels, to give a fake stereo spread.

If anyone skilled in the writing on Audio Library libraries finds the trivial and wants to take it on, that'd be awesome. I'd frankly just love to get going on using it to code my 'string machine' synth.

My Due code is here: https://github.com/quarterturn/due_ensenble_chorus
The root of the repo has the python code to generate the LFO wavetable.
 
AUDIO_BLOCK_SAMPLES is 128, see here: https://github.com/PaulStoffregen/cores/blob/master/teensy4/AudioStream.h

Audio is processed in blocks, 44100 / 128 = 344 times per second, or about 2.9 milliseconds.

For stereo, you'll need to allocate and process one audio_block_t per channel. If you can do mono, stereo isn't really any harder to implement. Take a look at the stereo reverb implementation to see: https://github.com/PaulStoffregen/Audio/blob/master/effect_freeverb.cpp and look for AudioEffectFreeverbStereo::update.
 
AUDIO_BLOCK_SAMPLES is 128, see here: https://github.com/PaulStoffregen/cores/blob/master/teensy4/AudioStream.h

Audio is processed in blocks, 44100 / 128 = 344 times per second, or about 2.9 milliseconds.

For stereo, you'll need to allocate and process one audio_block_t per channel. If you can do mono, stereo isn't really any harder to implement. Take a look at the stereo reverb implementation to see: https://github.com/PaulStoffregen/Audio/blob/master/effect_freeverb.cpp and look for AudioEffectFreeverbStereo::update.

Thanks!

So looks like each block I just run 128 iterations of the per-interrupt code and then pass on the completed block. Got it. I was updating my wavetable pointer every 100 interrupts so that'll work OK inside that loop.

My first version will probably just be non-adjustable. There's pretty much a sweet spot to the effect anyway which gives the 'nailed it' sound.
 
Sounds good, unfortunately I don't have time to actually help write or debug the port, but good luck none the less :)
 
I'm looking over effect_freeverb.cpp in section void AudioEffectFreeverbStereo::update()

I'm confused that there's only one block pointer for the input:

Code:
#if defined(__ARM_ARCH_7EM__)
	const audio_block_t *block;
	audio_block_t *outblockL;
	audio_block_t *outblockR;
	int i;
	int16_t input, bufout, outputL, outputR;
	int32_t sum;

Does this example take two channels as its input? It doesn't appear to. How would I get the second channel for my input?
 
Where should my LFO table go? I was using something like this with the due in lfo.h:

Code:
 const PROGMEM int LFO_TABLE[]=
    {
        0,
        1,
        2,
        3,
        ...
    }

I've tried putting that in the private section of the class constructor for my class AudioEffectEnsemble in file effect_ensemble.h, but it gcc doesn't like it. I get the following:

Code:
In file included from /Applications/Arduino.app/Contents/Java/hardware/teensy/avr/libraries/Audio/Audio.h:82:0,
                 from /Applications/Arduino.app/Contents/Java/hardware/teensy/avr/libraries/Audio/examples/Synthesis/pulseWidth/pulseWidth.ino:4:
/Applications/Arduino.app/Contents/Java/hardware/teensy/avr/libraries/Audio/effect_ensemble.h:1552:5: error: expected ';' at end of member declaration
     }
     ^
/Applications/Arduino.app/Contents/Java/hardware/teensy/avr/libraries/Audio/effect_ensemble.h:1552:5: error: too many initializers for 'const int [0]'

Of course there is a ';' at the end:
Code:
};
#endif

My guess based on the last line of the errors is there's something it does not like about PROGMEM in either a header file or a class constructor.
 
Code:
const PROGMEM int LFO_TABLE[]=
I think the PROGMEM has to go at the end of the declaration, like this:
Code:
const int LFO_TABLE[] PROGMEM =
because it is defined as a storage attribute:
Code:
#define PROGMEM __attribute__((section(".progmem")))

Pete
 
Code:
const PROGMEM int LFO_TABLE[]=
I think the PROGMEM has to go at the end of the declaration, like this:
Code:
const int LFO_TABLE[] PROGMEM =
because it is defined as a storage attribute:
Code:
#define PROGMEM __attribute__((section(".progmem")))

Pete

Same error, unfortunately.
 
Welp here's my first attempt: https://github.com/quarterturn/teensy3-ensemble-chorus

It does not work. The freeverbs code checks that the input block got something from receiveReadOnly(), so I use that too but fail on both channels. If I comment out the test it just kills the envelope used in the .ino file. If I pass back the input blocks, there's nothing but silence, reinforcing the validity of the test in the freeverbs library. It's possible there's mistake in my code, but it's more or less the exact same code that worked on the Due.

I'm am probably complicating things for myself since I have a Blackaddr Audio interface vs the PJRC one. If I take the pulsewidth example and just swap the correct codec name in, it does work, so my hardware is good. I used the web tool to get code by hooking up an oscillator to the freeverb module and then to the i2s output. Who knows maybe I have my connections wrong somehow.

Finally, I got around the PROGMEM issue by just not storing the LFO in EEPROM. Instead, I just write it to an array in RAM using the orignal trig function in the constructor.

I'll need to sleep on this for a while.
 
Quick suggestion is I think you need to use receiveWritable, though I have not dug into the receiveReadOnly and receiveWritable code to know what's going on under the hood.

Actually, on second thought, I'm probably wrong. I will have to look into it more.
 
Why do you have three delay buffers? You store the same sample in each one and otherwise never change their content. Therefore all three always have the same content.
I think you can use just one buffer and use this to compute the left channel output:
Code:
            outputL = 16384 + (delayBuffer1L[offsetIndex1L] >> 2) + (delayBuffer1L[offsetIndex2L] >> 2) + (delayBuffer1L[offsetIndex3L] >> 2);
and similar code for the right channel.

Pete
 
Why do you have three delay buffers? You store the same sample in each one and otherwise never change their content. Therefore all three always have the same content.
I think you can use just one buffer and use this to compute the left channel output:
Code:
            outputL = 16384 + (delayBuffer1L[offsetIndex1L] >> 2) + (delayBuffer1L[offsetIndex2L] >> 2) + (delayBuffer1L[offsetIndex3L] >> 2);
and similar code for the right channel.

Pete

Yeah it's an artifact of initially doing it backwards ie. doing the modulation on the input pointer and keeping the output steady. Thanks for pointing it out, I will fix it.
 
I've re-written my code and it should be making sense for processing blocks in and out of a ring buffer. Anyhow, not working - I'm going out to lunch when I try to load the combined delay offset data from the buffer into the block. Code's on github but here it is as well:

Code:
void AudioEffectEnsemble::update(void)
{
	audio_block_t *block;
	uint16_t i;

	block = receiveWritable(0);

    // buffer the incoming block
    for (i=0; i < AUDIO_BLOCK_SAMPLES; i++)
    {

        // wrap the input index
        inIndex++;
        if (inIndex > (BUFFER_SIZE - 1))
            inIndex = 0;

        delayBuffer[inIndex] = block->data[i];

    }
    // re-load the block with the delayed data
    for (i=0; i < AUDIO_BLOCK_SAMPLES; i++)
    {
        // advance the wavetable indexes every COUNTS_PER_LFO
        // so the LFO modulates at the correct rate
        lfoCount++;
        if (lfoCount > COUNTS_PER_LFO)
        {
            // wrap the lfo index
            lfoIndex1++;
            if (lfoIndex1 > (LFO_SIZE - 1))
                lfoIndex1 = 0;
            lfoIndex2++;
            if (lfoIndex2 > (LFO_SIZE - 1))
                lfoIndex2 = 0;
            lfoIndex3++;
            if (lfoIndex3 > (LFO_SIZE - 1))
                lfoIndex3 = 0;

            // reset the counter
            lfoCount = 0;
        }

        // wrap the output index
        outIndex1++;
        if (outIndex1 > (BUFFER_SIZE - 1))
            outIndex1 = 0;

        outIndex2++;
        if (outIndex2 > (BUFFER_SIZE - 1))
            outIndex2 = 0;

        outIndex3++;
        if (outIndex3 > (BUFFER_SIZE - 1))
            outIndex3 = 0;

        // get the delay from the wavetable
        offset1 = lfoTable[lfoIndex1];
        offset2 = lfoTable[lfoIndex2];
        offset3 = lfoTable[lfoIndex3];

        // add the delay to the buffer index to get the delay index
        offsetIndex1 = outIndex1 + offset1;
        offsetIndex2 = outIndex2 + offset2;
        offsetIndex3 = outIndex3 + offset3;


        // wrap the index if it goes past the end of the buffer
        if (offsetIndex1 > (BUFFER_SIZE - 1))
            offsetIndex1 = offsetIndex1 - BUFFER_SIZE;
        if (offsetIndex2 > (BUFFER_SIZE - 1))
            offsetIndex2 = offsetIndex2 - BUFFER_SIZE;
        if (offsetIndex3 > (BUFFER_SIZE - 1))
            offsetIndex3 = offsetIndex3 - BUFFER_SIZE;

        // wrap the index if it goes past the buffer the other way
        if (offsetIndex1 < 0)
            offsetIndex1 = BUFFER_SIZE + offsetIndex1;
        if (offsetIndex2 < 0)
            offsetIndex2 = BUFFER_SIZE + offsetIndex2;
        if (offsetIndex3 < 0)
            offsetIndex3 = BUFFER_SIZE + offsetIndex3;

        // combine delayed samples into output
        // add the delayed and scaled samples
        block->data[i] = 16384 + (delayBuffer[offsetIndex1] >> 2) + (delayBuffer[offsetIndex2] >> 2) + (delayBuffer[offsetIndex3] >> 2);

    }
    Serial.println("next is transmit");
    transmit(block, 0);
    Serial.println("transmit done");
	release(block);
    
    return;

}

I hope it's something obvious. I'm not great with C++ syntax.
 
When you say "going out to lunch" you mean the teensy hard locks? If so you might have gone out of bounds on an array, but it appears you're checking array bounds correctly.

What's the last serial print before it locks up?
 
It crashes right at the block->data = ... line. I had it surrounded with Serial.print statements and it only printed the one before.
 
Maybe as a sanity check, after you do "block = receiveWritable(0);" could you check that the block has been received (make sure it's not null)?

The teensy doesn't lock up if you set block->data to some static value (rather than access the arrays)?
 
Ah yep, it's null. I can take a working example like the one below, substitute 'ensemble' for 'reverb' and get a null

Code:
/*************************************************************************
 * This demo uses the BALibrary library to provide enhanced control of
 * the TGA Pro board.
 * 
 * The latest copy of the BA Guitar library can be obtained from
 * https://github.com/Blackaddr/BALibrary
 * 
 * This demo provides an example guitar tone consisting of some slap-back delay,
 * followed by a reverb and a low-pass cabinet filter.
 * 
 */
#include <Wire.h>
#include <Audio.h>
#include <MIDI.h>
#include "BALibrary.h"

using namespace BALibrary;

BAAudioControlWM8731      codecControl;

AudioInputI2S            i2sIn;
AudioOutputI2S           i2sOut;

AudioMixer4              gainModule; // This will be used simply to reduce the gain before the reverb
AudioEffectDelay         delayModule; // we'll add a little slapback echo
AudioEffectEnsemble      ensemble;
AudioEffectReverb        reverb; // Add a bit of 'verb to our tone
AudioMixer4              mixer; // Used to mix the original dry with the wet (effects) path.
AudioFilterBiquad        cabFilter; // We'll want something to cut out the highs and smooth the tone, just like a guitar cab.


// Audio Connections
AudioConnection      patchIn(i2sIn,0, delayModule, 0); // route the input to the delay

AudioConnection      patch2(delayModule,0, gainModule, 0); // send the delay to the gain module
// AudioConnection      patch2b(gainModule, 0, reverb, 0); // then to the reverb
AudioConnection      patch2b(gainModule, 0, ensemble, 0); // then to the reverb

//AudioConnection      patch1(i2sIn,0, mixer,0); // mixer input 0 is our original dry signal
// AudioConnection      patch3(reverb, 0, mixer, 1); // mixer input 1 is our wet
AudioConnection      patch3(ensemble, 0, mixer, 1); // mixer input 1 is our wet

AudioConnection      patch4(mixer, 0, cabFilter, 0); // mixer outpt to the cabinet filter


AudioConnection      patch5(cabFilter, 0, i2sOut, 0); // connect the cab filter to the output.
//AudioConnection      patch5b(cabFilter, 0, i2sOut, 1); // connect the cab filter to the output.

void setup() {

  delay(5); // wait a few ms to make sure the GTA Pro is fully powered up
  AudioMemory(48);

  // If the codec was already powered up (due to reboot) power itd own first
  codecControl.disable();
  delay(100);
  codecControl.enable();
  delay(100);

  // Configure our effects
  delayModule.delay(0, 50.0f); // 50 ms slapback delay
  gainModule.gain(0, 0.25); // the reverb unit clips easily if the input is too high
  mixer.gain(0, 1.0f); // unity gain on the dry
  mixer.gain(1, 1.0f); // unity gain on the wet

  // Setup 2-stages of LPF, cutoff 4500 Hz, Q-factor 0.7071 (a 'normal' Q-factor)
  cabFilter.setLowpass(0, 4500, .7071);
  cabFilter.setLowpass(1, 4500, .7071);
  
  
}

void loop() {  

  // The audio flows automatically through the Teensy Audio Library

}
 
Well, I am left grasping at straws. Instead of receiveWritable, you could try receiveReadOnly, and then do all of your write operations into another block: "outBlock = allocate();" and then you'd actually transmit outBlock... making sure to check both blocks for not null :)

It seems like the issue is a lack of my/our understanding of what's going on with receiveWritable and receiveReadOnly.
 
Status
Not open for further replies.
Back
Top