Distorted sound from DAC on PicoADK

Thread Starter

Robyn

Joined May 1, 2013
31
Hello everyone,

I have spent most of the day on this, RTed all the FMs, Duckduckgoed like there's no tomorrow and even had a long conversation with my absolute last resort: ChatGPT. It is now time for me to bother real people so here I come:

Context:
I am working on a groovebox. the UI is handled by a Teensy and the sound is processed by a PicoADK which, for those who don't know is an overclocked RP2040 based board with a nice DAC on it. Vult is the recommended language. It gets transcompiled to C. I am using the Arduino command line interface and writing my code in Kate. The test rig is me sending MIDI messages from my laptop to the Teensy and the Teensy relaying these to the Pico (I know I could skip the Teensy but it will control the Pico this way in the end so I though I should set this up early).

The problem:
The sound is quite distorted. It's in tune but really ugly. I have a little handheld scope that tells me that part of the wave on the output of the DAC is missing:

IMG_20250705_185334.jpg



This is what the Vult code running in browser gives me:

20250705_18h56m18s_grim.png



It basically looks like there is a little pause in the processing but I don't feel like there's enough going on in that Vult program or anywhere else to cause it.


Edit: This is what the process function returns through a serial plotter. It looks fine so the issue happens in my conversion from float to int16...

20250705_19h41m17s_grim.png


The code:

Basically loop() in the Arduino sketch calls a process() function from the Vult code. There are also a couple of midi handles in the Arduino sketch that call the relevant Vult functions for MIDI note on / off events.

groovebox_audio.ino:
#include <Arduino.h>
#include <I2S.h>
#include <MIDI.h>
#include "vult.h"

//------
// MIDI
//------

MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, midiA);

//-----
// I2S
//-----

I2S i2s(OUTPUT);

#define pMute 25
#define pDemp 23

const int sampleRate = 48000;

//------
// VULT
//------

Vult_process_type ctx;

void setup() {

    //------------
    // SERIAL USB
    //------------

    Serial.begin(9600);

    //------
    // MIDI
    //------

    midiA.begin(MIDI_CHANNEL_OMNI);
    midiA.setHandleNoteOn(handleNoteOn);
    midiA.setHandleNoteOff(handleNoteOff);

    //-----
    // I2S
    //-----

    pinMode(pMute, OUTPUT);
    digitalWrite(pMute, HIGH); // unmute codec
    pinMode(pDemp, OUTPUT);
    digitalWrite(pDemp, LOW);

    i2s.setBCLK(PIN_I2S_BCLK);
    i2s.setDATA(PIN_I2S_DOUT);
    i2s.setBitsPerSample(16);
    i2s.setSysClk(sampleRate);

    if (!i2s.begin(sampleRate)) {
        Serial.println("Failed to initialize I2S!");
        while (1) Serial.println("Failed to initialize I2S!"); // do nothing
    }

    //------
    // VULT
    //------

    Vult_process_init(ctx);
    Vult_default_init(ctx);
    Vult_default(ctx);
}

void loop() {

    //------
    // MIDI
    //------

    midiA.read();

    float out = Vult_process(ctx, 0.0);
    int16_t pcm = (int16_t)(out * 32767.0f);
    i2s.write(pcm);
    i2s.write(pcm);
}

void handleNoteOn(byte channel, byte note, byte velocity) {
    float frequency = 0;
    Serial.print("channel: ");
    Serial.println(channel);
    Serial.print("note: ");
    Serial.println(note);
    Serial.print("velocity: ");
    Serial.println(velocity);
    Vult_noteOn(ctx, note, velocity, channel);
}

void handleNoteOff(byte channel, byte note, byte velocity) {
    Vult_noteOff(ctx, note, channel);
}
vult.vult (becomes vult.h, vult.cpp and vult.tables.h):
/* A simple synthesizer with one LFO */


// Used to soften the transitions of controls

fun smooth(input){

   mem x;

   x = x+(input-x)*0.005;

   return x;

}


// Returns true every time the input value changes

fun change(x):bool {

    mem pre_x;

    val v:bool = pre_x<>x;

    pre_x = x;

    return v;

}


// Returns true if the value changes from 0 to anything

fun edge(x):bool {

    mem pre_x;

    val v:bool = (pre_x<>x) && (pre_x==0);

    pre_x = x;

    return v;

}


// Returns true every 'n' calls

fun each(n){

   mem count;

   val ret = (count == 0);

   count = (count + 1) % n;

   return ret;

}


// Converts the MIDI note to increment rate at a 48000 sample rate

fun pitchToRate(d) return 8.1758*exp(0.0577623*d)/48000.0;


fun phasor(pitch,reset){

    mem rate,phase;

    if(change(pitch))

        rate = pitchToRate(pitch);

    phase = if reset then 0.0 else (phase + rate) % 1.0;

    return phase;

}


fun lfo(f,gate){

    mem phase;

    val rate = f * 10.0/48000.0;

    if(edge(gate)) phase = 0.0;

    phase = phase + rate;

    if(phase>1.0) phase = phase-1.0;

    return sin(phase*2.0*3.141592653589793)-0.5;

}


// Main processing function

fun process(input:real){

   mem volume,detune; // values set in 'controlChange'

   mem n1,n2,n3,n4; // used to handle the notes

   mem count,pitch,gate;

   mem pre_phase1;

   mem lfo_rate,lfo_amt;

   val lfo_val = lfo(lfo_rate,gate)*lfo_amt;

   // Implements the resonant filter simulation as shown in

   // http://en.wikipedia.org/wiki/Phase_distortion_synthesis

   val phase1 = phasor(pitch,false);

   val comp   = 1.0 - phase1;

   val reset  = (pre_phase1 - phase1) > 0.5;

   pre_phase1 = phase1;

   val phase2 = phasor(pitch+smooth(detune+lfo_val)*32.0,reset);

   val sine  = sin(2.0*3.14159265359*phase2);

   val gate_value = if gate > 0 then 1.0 else 0.0;

   return smooth(volume)*(sine*comp)*smooth(gate_value);

}


// Called when a note On is received

and noteOn(note:int,velocity:int,channel:int){

   mem n1,n2,n3,n4;

   mem count,pitch,gate;

   // written this way because Vult does not have array support yet.

   if(count == 0) { n1 = note; pitch = real(note); } else

   if(count == 1) { n2 = note; pitch = real(note); } else

   if(count == 2) { n3 = note; pitch = real(note); } else

   if(count == 3) { n4 = note; pitch = real(note); }

   if(count <= 4) count = count + 1;

   gate = if count>0 then 1 else 0;

}


// Called when a note Off is received

and noteOff(note,channel:int){

    mem n1,n2,n3,n4;

    mem count,pitch,gate;

    val found = false;


    // finds the voice and removes it

    if(note == n1) { n1,n2,n3 = n2,n3,n4; found = true; } else

    if(note == n2) { n2,n3    = n3,n4;    found = true; } else

    if(note == n3) { n3       = n4;       found = true; } else

    if(note == n4) {                      found = true; }


    // If found, decrease the number of active notes

    if(found && count>0) count = count - 1;


    gate = if count>0 then 1 else 0;


    if(count == 1) pitch = real(n1);

    if(count == 2) pitch = real(n2);

    if(count == 3) pitch = real(n3);

    if(count == 4) pitch = real(n4);

}


// Called when a control changes

and controlChange(control,value,channel:int){

   mem volume;

   mem detune;

   mem lfo_rate;

   mem lfo_amt;

   // Control 30 defines the volume

   if(control==30) volume = value/127.0;

   if(control==31) detune = value/127.0;

   if(control==32) lfo_rate = value/127.0;

   if(control==33) lfo_amt = 2.0*((real(value)/127.)-0.5);

}


// Called on initialization to define initial values

and default(){

   mem volume = 1.0;

   mem pitch = 45.0;

   mem detune = 0.0;

   mem lfo_rate = 0.07;

   mem lfo_amt = 0.0;

}
I know from the datasheet that the DAC (PCM5100A) accepts 16, 24 and 32 bits and the expected format is PCM I2S left justified.

Thanks for reading!
 
Last edited:
Top