News:

SMF for DIYStompboxes.com!

Main Menu

Audio Effects DSP Board

Started by markseel, June 13, 2016, 11:53:46 AM

Previous topic - Next topic

markseel

#240
Nine combo boards have been assembled.  A combo board has the USB interface, XMOS processor (XUF216), audio ADC (AK5386), audio DAC (AK4420), input buffers, and I/O's for audio and additional I2S and I2C peripherals.

Email "flexfx@protonmail.com" if you want one of these combo units for $99 (shipped to anywhere in continental US).  Keep in mind that these are hand assembled beta units.  The SDK continues to mature - see https://github.com/markseel/flexfx_kit/blob/master/README.md

After getting these combo boards out and getting some feedback I'll (if there's demand) start a larger production run with a contract manufacturer.



A companion board is on the way.  It can accept a combo board and provides pots and jacks.  It's actually a two-layer version of a four-layer all-in-one board without the XMOS and audio components installed.

 

markseel

#241
Here's how to hook up the combo board.  This will work for all of the examples.



1) Digital power (bottom left) is connected to USB +5V (bottom left).
2) Analog power (top left) is connected to USB +5V (bottom left).
3) The analog ground (top left, top right) is connected to the digital ground (bottom right).
4) The left/right inputs (top right) are high impedance buffered inputs. Left is connected to ground and right is connected to the instrument.
5) The left/right outputs (top right) are low impedance line-level outputs (can be connected to other effects or directly to powered speakers).

Note that for #4 above the firmware should then subtract one from the other forming a pseudo differential input.  I've measured the input noise floor using this approach to -110dB or so.  Pretty quiet :-)

markseel

I'm going to work on another example.  Any requests?  Was thinking of doing a reverb demo but I'm open to other examples that would help get people started.

markseel

#243
Here's a stereo reverb.  It implements the Schroeder-Moorer approach to reverberation and this particular implementation is a port of the 'FreeVerb' algorithm that's used in a number of software packages. I haven't tested it yet but this should be pretty close.  I'll test it, fix the bugs, and add it to the FlexFX kit examples.

A nice improvement would be to use a potentiometer (sensed via an ADC attached via I2C in the 'control' function) to select the reverb type.  The potentiometer code would perhaps generate a value of 1 through 9 depending on the pot's rotation setting.  The value would then be used to select the reverb's wet/dry mix, stereo width, room size, and room reflection/damping.

Note that there's plenty of processing power left over to implement other algorithms along with this reverb such as graphic EQ's, flanger/chorus, etc.


#include "flexfx.h"
#include <string.h>

const char* product_name_string   = "FlexFX Example"; // Your company/product name
const int   audio_sample_rate     = 48000;     // Audio sampling frequency
const int   usb_output_chan_count = 2;         // 2 USB audio class 2.0 output channels
const int   usb_input_chan_count  = 2;         // 2 USB audio class 2.0 input channels
const int   i2s_channel_count     = 2;         // ADC/DAC channels per SDIN/SDOUT wire
const char  interface_string[]    = "No interface is specified";
const char  controller_string[]   = "No controller is available";

const int   i2s_sync_word[8] = { 0xFFFFFFFF,0x00000000,0,0,0,0,0,0 }; // I2S WCLK values per slot

void control( int rcv_prop[6], int usb_prop[6], int dsp_prop[6] )
{
    // If outgoing USB or DSP properties are still use then come back later ...
    if( usb_prop[0] != 0 || dsp_prop[0] != 0 ) return;
}

void mixer( const int* usb_output, int* usb_input,
            const int* i2s_output, int* i2s_input,
            const int* dsp_output, int* dsp_input, const int* property )
{
    // Convert the two ADC inputs into a single pseudo-differential mono input (mono = L - R).
    int guitar_in = i2s_output[0] - i2s_output[1];
    // Route instrument input to the left USB input and to the DSP input.
    dsp_input[0] = (usb_input[0] = guitar_in) / 8; // DSP samples need to be Q28 formatted.
    // Route DSP result to the right USB input and the audio DAC.
    usb_input[1] = i2s_input[0] = i2s_input[1] = dsp_output[0] * 8; // Q28 to Q31
}

int _comb_bufferL   [8][2048], _comb_bufferR   [8][2048]; // Delay lines for comb filters
int _comb_stateL    [8],       _comb_stateR    [8];       // Comb filter state (previous value)
int _allpass_bufferL[4][2048], _allpass_bufferR[4][2048]; // Delay lines for allpass filters
int _allpass_feedbk    = FQ(0.5); // Reflection decay/dispersion
int _stereo_spread     = 23; // Buffer index spread for stereo separation
int _comb_delays   [8] = { 1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617 };
int _allpass_delays[8] = { 556, 441, 341, 225 };

int _wet_dry_blend  = FQ(0.2); // Parameter: Wet/dry mix setting (0.0=dry)
int _stereo_width   = FQ(0.2); // Parameter: Stereo width setting
int _comb_damping   = FQ(0.2); // Parameter: Reflection damping factor (aka 'reflectivity')
int _comb_feedbk    = FQ(0.2); // Parameter: Reflection feedback ratio (aka 'room size')

void dsp_initialize( void ) // Called once upon boot-up.
{
    memset( _comb_bufferL, 0, sizeof(_comb_bufferL) );
    memset( _comb_stateL,  0, sizeof(_comb_stateL) );
    memset( _comb_bufferR, 0, sizeof(_comb_bufferR) );
    memset( _comb_stateR,  0, sizeof(_comb_stateR) );
}

inline int _comb_filterL( int xx, int ii, int nn ) // yy[k] = xx[k] + g1*xx[k-M1] - g2*yy[k-M2]
{
    ii = (_comb_delays[nn] + ii) & 2047; // Index into sample delay FIFO
    int yy = _comb_bufferL[nn][ii];
    _comb_stateL[nn] = dsp_multiply( yy, FQ(1.0) - _comb_damping )
                     + dsp_multiply( _comb_stateL[nn], _comb_damping );
    _comb_bufferL[nn][ii] = xx + dsp_multiply( _comb_stateL[nn], _comb_feedbk );
    return yy;
}

inline int _comb_filterR( int xx, int ii, int nn ) // yy[k] = xx[k] + g1*xx[k-M1] - g2*yy[k-M2]
{
    ii = (_comb_delays[nn] + ii + _stereo_spread) & 2047; // Index into sample delay FIFO
    int yy = _comb_bufferR[nn][ii];
    _comb_stateR[nn] = dsp_multiply( yy, FQ(1.0) - _comb_damping )
                     + dsp_multiply( _comb_stateR[nn], _comb_damping );
    _comb_bufferR[nn][ii] = xx + dsp_multiply( _comb_stateR[nn], _comb_feedbk );
    return yy;
}

inline int _allpass_filterL( int xx, int ii, int nn ) // yy[k] = xx[k] + g * xx[k-M] - g * xx[k]
{
    int yy = _allpass_bufferL[nn][ii] - xx;
    _allpass_bufferL[nn][ii] = xx + dsp_multiply( _allpass_bufferL[nn][ii], _allpass_feedbk );
    return yy;
}

inline int _allpass_filterR( int xx, int ii, int nn ) // yy[k] = xx[k] + g * xx[k-M] - g * xx[k]
{
    int yy = _allpass_bufferR[nn][ii] - xx;
    _allpass_bufferR[nn][ii] = xx + dsp_multiply( _allpass_bufferR[nn][ii], _allpass_feedbk );
    return yy;
}

void dsp_thread1( int* samples, const int* property )
{
    // ----- Left channel reverb
    static int index = 0; ++index; // Used to index into the sample FIFO delay buffer
    // Eight parallel comb filters ...
    samples[2] = _comb_filterL( samples[0]/8, index, 0 ) + _comb_filterL( samples[0]/8, index, 1 )
               + _comb_filterL( samples[0]/8, index, 2 ) + _comb_filterL( samples[0]/8, index, 3 )
               + _comb_filterL( samples[0]/8, index, 4 ) + _comb_filterL( samples[0]/8, index, 5 )
               + _comb_filterL( samples[0]/8, index, 6 ) + _comb_filterL( samples[0]/8, index, 7 );
    // Four series all-pass filters ...
    samples[2] = _allpass_filterL( samples[2], index, 0 );
    samples[2] = _allpass_filterL( samples[2], index, 1 );
    samples[2] = _allpass_filterL( samples[2], index, 2 );
    samples[2] = _allpass_filterL( samples[2], index, 3 );
}

void dsp_thread2( int* samples, const int* property )
{
    // ----- Right channel reverb
    static int index = 0; ++index; // Used to index into the sample FIFO delay buffer
    // Eight parallel comb filters ...
    samples[1] = _comb_filterR( samples[0]/8, index, 0 ) + _comb_filterR( samples[0]/8, index, 1 )
               + _comb_filterR( samples[0]/8, index, 2 ) + _comb_filterR( samples[0]/8, index, 3 )
               + _comb_filterR( samples[0]/8, index, 4 ) + _comb_filterR( samples[0]/8, index, 5 )
               + _comb_filterR( samples[0]/8, index, 6 ) + _comb_filterR( samples[0]/8, index, 7 );
    // Four series all-pass filters ...
    samples[3] = _allpass_filterR( samples[3], index, 0 );
    samples[3] = _allpass_filterR( samples[3], index, 1 );
    samples[3] = _allpass_filterR( samples[3], index, 2 );
    samples[3] = _allpass_filterR( samples[3], index, 3 );
}

void dsp_thread3( int* samples, const int* property )
{
    // Final mixing and stereo synthesis
    int dry = _wet_dry_blend, wet = FQ(1.0) - _wet_dry_blend;
    // Coefficients for stereo separation
    int wet1 = _stereo_width / 2 + 0.5;
    int wet2 = (FQ(1.0) - _stereo_width) / 2;
    // Final mixing and stereo separation for left channel
    samples[0] = dsp_multiply( dry, samples[0] )
               + dsp_multiply( wet, dsp_multiply( samples[2], wet1 ) +
                                    dsp_multiply( samples[3], wet2 ) );
    // Final mixing and stereo separation for right channel
    samples[1] = dsp_multiply( dry, samples[1] )
               + dsp_multiply( wet, dsp_multiply( samples[2], wet2 ) +
                                    dsp_multiply( samples[3], wet1 ) );
}

void dsp_thread4( int* samples, const int* property )
{
}

void dsp_thread5( int* samples, const int* property )
{
}

markseel

Hey Everyone ... am I posting too much or using this site wrong?  I don't want to abuse this forum website so let me know if something's not right :-)

The FlexFX github README was updated (see new sections at the end): https://github.com/markseel/flexfx_kit/blob/master/README.md

Add a section on how to use prebuilt effects. Some folks may not want to code so I'll include prebuilt optimized ones.  You don't even have to build them - you can just grab the *.bin firmware image file from the repo and burn it to the board via USB/MIDI.

Also added a section that introduces text info and javascript dump properties that allow software to get information on the target device (like its properties for control) and its javascript controller code ... if they exist that is.  Added these is easy - just create a TXT and/or JS file and the build script places this data in the firmware image allowing access by a host via USB.

EBK

Quote from: markseel on February 07, 2018, 03:21:35 PM
Hey Everyone ... am I posting too much or using this site wrong?  I don't want to abuse this forum website so let me know if something's not right :-)
I'm personally fascinated by all aspects of your project.  I've only been keeping a healthy distance so that I don't get sucked in before I clear out a sizable portion of my backlog.  I can understand how that can sound like a chirping cricket, but it is not.   :icon_wink:
  • SUPPORTER
Technical difficulties.  Please stand by.

Digital Larry

Mark I'm impressed with all your work here.  I'm also keeping my distance because if I get pulled into something else, everything will grind to a complete halt.
Digital Larry
Want to quickly design your own effects patches for the Spin FV-1 DSP chip?
https://github.com/HolyCityAudio/SpinCAD-Designer

vigilante397

I agree with Eric, I'm super interested and I want in, but not before I clear some things off my plate. This seems like the sort of thing I'm going to want to spend some time playing with, so I want to make sure I have other stuff out of the way so I can actually spend said time ;D
  • SUPPORTER
"Some people love music the way other people love chocolate. Some of us love music the way other people love oxygen."

www.sushiboxfx.com

cliffsp8

This is great news - I'm looking forward to getting one when the companion board is available.

Mark, can you clarify what power supply is needed when it operates stand alone, eg what voltage range is permissible and whether a 9v battery or 9v stomp box PSU is suitable - with or without a regulator.

Also can you confirm that this will be compatible with the ADAT adapter you have in development .

Thanks

Cliff

markseel

Thanks all for the feedback so far.

Quote from: cliffsp8 on February 07, 2018, 06:16:13 PM
This is great news - I'm looking forward to getting one when the companion board is available.
OK, that board is in the works.

Quote from: cliffsp8 on February 07, 2018, 06:16:13 PM
can you clarify what power supply is needed when it operates stand alone, eg what voltage range is permissible and whether a 9v battery or 9v stomp box PSU is suitable - with or without a regulator.

Well here goes ... a lot of guesswork ... The on-board buck regulators for 1.0V and 3.3V supplies are Semtech SC189 parts and have a max Vin of 5.5V.  The XMOS 1.0V core is where much of the power consumption goes and it's quite a lot.  Perhaps 500mA to 800mA for very 'busy' firmware.  At a conservative estimate of 80% conversion efficiency for the SC189's we're looking at maybe 150mA at 5V.  Add more power consumption for ADC's/DAC's.  So perhaps 200mA at 5V?  Going from 9V to 5V at 90% efficiency is still well over 100mA at 9V.  So probably not a good candidate for 9V battery power.

Quote from: cliffsp8 on February 07, 2018, 06:16:13 PM
Also can you confirm that this will be compatible with the ADAT adapter you have in development .

Yes these boards will be able to encode and decode ADAT bitstreams while I2S is active.  I2S uses the I/O pins labeled ADC0/1/2/3 and DAC0/1/2/3 (four DAC data wires and four ADC data wires).  ADAT TX/RX will use two of the IO1/2/3 pins. 

pruttelherrie

Quote from: markseel on February 07, 2018, 03:21:35 PM
Hey Everyone ... am I posting too much or using this site wrong?  I don't want to abuse this forum website so let me know if something's not right :-)

Please keep it coming!

But I must admit I must have missed something in the posts:

Quote
QuoteAlso can you confirm that this will be compatible with the ADAT adapter you have in development .
Yes these boards will be able to encode and decode ADAT bitstreams while I2S is active.  I2S uses the I/O pins labeled ADC0/1/2/3 and DAC0/1/2/3 (four DAC data wires and four ADC data wires).  ADAT TX/RX will use two of the IO1/2/3 pins.

Awesome :)
Especially the firmware support; I can handle the electronics but the low level protocol is too much.

markseel

I have code to do both ADAT and SPDIF.  Haven't prioritized that work as much as the effects stuff but as long as there's interest I'll have those working.

orbitbot

I've been keeping track, just kind of out of my depth here so I'm waiting for general availability outside of the US (also have a bunch of normal pedals, some DSP kits and an Axoloti and separate RPI-based thing in the works)  :)

Sergey

Hi Mark,
first of all thanks for your project, it is amazing!
I'm dsp beginner, and I'm very interested in any simple guitar effects examples, easy to understand. Could you please explain how cab-sim example works on multi-thread architecture? As I understand FIR state should change 1 time per sample, and dsp_convolve(...) do it, but it your example  dsp_convolve executes 5 times on the one sample and is use same buffers.
Sory for newbie question :)

markseel

#254
Hi Sergey.  I can see why that's confusing.  You're right, the goal is to perform convolution across the entire FIR just once per audio cycle.  And if we were executing the convolution function in just one thread then that's exactly how the code would look.  But ... I distributed the convolution across five threads.  In order for that to work the convolution in each thread operates on 1/5 of the FIR and state data -- look at the arguments to the convolution function and you'll see that the FIR coefficients and state data are passed with offsets such that each thread operates on its own block (five blocks total).  The convolution function returns the accumulator so that it can be passed into the next block's convolution therefore creating a daisy-chained convolution engine.

Like this pseudocode for 5 daisy-chained convolutions ...

L = FIR length // Total convolution length
N = L / 5 // N taps per convolution block, 5 blocks for complete L-length convolution
accumulator = 0 // 64-bit accumulator
sample = next_audio_sample // Incoming 32-bit sample

accumulator, sample = convolve( sample, fir_data + N*0, state_data + N*0, accumulator )
accumulator, sample = convolve( sample, fir_data + N*1, state_data + N*1, accumulator )
accumulator, sample = convolve( sample, fir_data + N*2, state_data + N*2, accumulator )
accumulator, sample = convolve( sample, fir_data + N*3, state_data + N*3, accumulator )
accumulator, sample = convolve( sample, fir_data + N*4, state_data + N*4, accumulator )

resulting_sample = upper_32bits( accumulator )


Does that make more sense? :-)

Sergey

Thanks for response, Mark, it is much clearer now :)
But it is not the same as serial connection of  5 FIR filters, yes? Input sample should be passed between blocks, as I understand.
Here is the example: if I have 8 tap FIR coefficients like this {0,0,0,0,0,0,0,1} and input is single 1 and others 0. If convolution will be performed as usual, I get 1 on output after 8 input samples. But if I split FIR on two: FIR1{0,0,0,0}, FIR2{0,0,0,1} and calculate FIR1 and then pass result of FIR1 to input of FIR2, I never get 1 on output of FIR2, as result of FIR1 convolution is always 0. Am I right?

markseel

Quote from: Sergey on February 26, 2018, 04:06:49 PM
=But it is not the same as serial connection of  5 FIR filters, yes?
Not quite.  Serializing FIR's is just cascading five filters who (1) do not maintain the accumulation of the coefficient and state data products from previous FIR's and (2) do not propagate state data (old samples) across FIR's.

For a 4-tap FIR:
x0 = new sample
ah:al = b0*x0+b1*x1+b2*x2+b3*x3, discard x3, x3=x2, x2=x1, x1=x0
result = ah

Cascading them as two 2-tap FIRs:
x0 = new sample
ah:al = b0*x0+b1*x1, discard x1, x1=x0
result = ah
x2 = result
ah:al = b2*x2+b3*x3, discard x3, x3=x2
result = ah

Cascading prevents x1 from being carried through to the next FIR's state data and also the lower 32-bits of the accumulator is dropped prematurely.

But splitting them as in the example source code looks like this:
x0 = new sample
ah:al = b0*x0+b1*x1, carry=x1, x1=x0
x2 = carry
ah:al = b2*x2+b3*x3, discard x3, x3=x2
result = ah

Sergey

Clear now, thanks a lot, Mark!

markseel

The demo board and the module will fit in side this custom machined aluminum case.  There's a recess for an adhesive label in case you want to create your own product.  The label would have your pedal graphics/text.  Need one more round of case CAD updates to clear up some dimensional issues.  The complete kit with options to get the XMOS/USB module, a demo board that holds the pots, jacks, and audio interface (ADC, DAC, buffers), and the aluminum case will be offered during the Kickstarter campaign.  I plan to have some generic adhesive labels also.



cliffsp8

That looks very smart.

How do you get to install the boards into the case with those jack sockets and pots in place? There doesn't seem to be enough "wiggle" room at first glance.

Cliff