Skip to content

Implementing polyphony / multiple channels #58

@robinmayol

Description

@robinmayol

EDIT: OK I've just dramatically simplified my vult code and I can bring VOICE_COUNT up to 8 without shenanigans. So I guess I have indeed encountered a hardware limit. Unless I can make the I2S callback leaner by moving the Processor_process(processors[instance]); into the main function...


I am using Vult on a PicoADK. I have posted an issue there as well as it's kind of relevant to both sides :)

I am trying to implement a multi midi channel instrument. Following the recommendations at https://vult-dsp.github.io/vult/tutorials/patterns/ I have modified the firmware template to define a VOICE_COUNT make all occurrences of Processor_process_type arrays of size VOICE_COUNT. I am using the channel in the callbacks as an array index to call the relevant instance. I am not bound checking but I know that I am not sending to channels > 3. In fact I am only sending to channel 0 and 1.

Then in the __not_in_flash_func callback I am iterating through the voices, calling process and adding the return divided by the total amount of voices.

// We are filling the buffer with 32-bit samples (2 channels)
		for (uint i = 0; i < buffer->max_sample_count; i++) {
			fix16_t left_out = 0;
			fix16_t right_out = 0;

			for (uint8_t instance = 0; instance < VOICES_COUNT; instance++) {
				// In case of the Vult Example, this is Processor_process(processors);
				Processor_process(processors[instance]);
				left_out += Processor_process_ret_0(processors[instance]) / VOICES_COUNT;
				right_out += Processor_process_ret_1(processors[instance]) / VOICES_COUNT;
			}

			samples[i * 2 + 0] = fix16_to_int32(left_out);  // LEFT
			samples[i * 2 + 1] = fix16_to_int32(right_out); // RIGHT
		}

This works with a single voice, I have fiddled with the for loop and I can get one or the other playing individually. But as soon as I set VOICE_COUNT to more than 1 I lose sound (and serial so I'm guessing the PicoADK is crashing). I have got it working with i < buffer->max_sample_count / VOICE_COUNT; which I tried, assuming that loop was taking too long. But obviously I get decimated sound.

Have I just hit processing limits or am I doing this the wrong way?

Below is the whole main.cpp in case it's useful.

I am a bit ashamed to show my Vult code as it's quite messy. But it is functional and I am basically generating a kick with Kick.do and a simple gated dual sawtooth with Sawcore.process.

Thank you for your time.

#include <stdio.h>
#include "project_config.h"

#if __has_include("bsp/board_api.h")
#include "bsp/board_api.h"
#else
#include "bsp/board.h"
#endif

#include "midi_input.h"
#include "midi_input.cpp"
#include "midi_input_usb.h"
#include "audio_subsystem.h"
#include "vult.h"
#include "picoadk_hw.h"

#include "FreeRTOS.h"
#include <task.h>
#include <queue.h>

#include "arduino_compat.h"

#define USE_DIN_MIDI 1
#define DEBUG_MIDI

#define VOICES_COUNT 4

audio_buffer_pool_t *ap;
Processor_process_type processors[VOICES_COUNT];

MIDIInputUSB usbmidi;
MIDIInput* midi_input = nullptr;


#ifdef __cplusplus
extern "C" {
    //--------------------------------------------------------------------+
    // CDC Callbacks
    //--------------------------------------------------------------------+

    // Invoked when cdc when line state changed e.g connected/disconnected
    void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts) {
		(void) itf;
		(void) rts;
    }

    // Invoked when CDC interface received data from host
    void tud_cdc_rx_cb(uint8_t itf) {
		(void) itf;
    }
#endif

    volatile long dsp_start;
    volatile long dsp_end;

    // This task prints the statistics about the running FreeRTOS tasks
    // and how long it takes for the I2S callback to run.
    void print_task(void *p) {
        char ptrTaskList[2048];
        while (1) {
            vTaskList(ptrTaskList);
            printf("Task\t\tState\tPrio\tStack\tNum\n%s\n", ptrTaskList);
            printf("======================================================\n");
            printf("B = Blocked, R = Ready, D = Deleted, S = Suspended\n");
            printf("Milliseconds since boot: %d\n", xTaskGetTickCount() * portTICK_PERIOD_MS);
            printf("dsp task took %d uS\n", dsp_end - dsp_start);
            watchdog_update();
            vTaskDelay(pdMS_TO_TICKS(2000));
        }
    }

    // MIDI callbacks
    void note_on_callback(uint8_t note, uint8_t level, uint8_t channel) {
		printf("channel: %d\n", channel);
        if (level > 0) {
            Processor_noteOn(processors[channel], note, level, channel);

        }
        else {
            Processor_noteOff(processors[channel], note, channel);
        }
    }

    void note_off_callback(uint8_t note, uint8_t level, uint8_t channel) {
        Processor_noteOff(processors[channel], note, channel);
    }

    void cc_callback(uint8_t cc, uint8_t value, uint8_t channel) {
        Processor_controlChange(processors[channel], cc, value, channel);
    }

    // This task processes the USB MIDI input
    void usb_midi_task(void *pvParameters) {
        usbmidi.setCCCallback(cc_callback);
        usbmidi.setNoteOnCallback(note_on_callback);
        usbmidi.setNoteOffCallback(note_off_callback);

        while (1)
        {
            tud_task();
            usbmidi.process();
        }
    }

    // This task processes the UART MIDI input
    void uart_midi_task(void *pvParameters) {
        // printf("uart_midi_task started\n");
        if (midi_input) {
            // printf("midi_input object exists, setting callbacks\n");
            midi_input->setCCCallback(cc_callback);
            midi_input->setNoteOnCallback(note_on_callback);
            midi_input->setNoteOffCallback(note_off_callback);
        } else {
            // printf("ERROR: midi_input is NULL!\n");
        }

        while (1) {
            // Check if UART has data available
            if (midi_input) {
                if (uart_is_readable(uart0)) {
                    // printf("UART readable - processing MIDI\n");
                    midi_input->process();
                }
            }
            // Small delay to prevent excessive CPU usage
            vTaskDelay(pdMS_TO_TICKS(1));
        }
    }


    int main(void) {
        
        // initialize the hardware FIRST (sets clock to 402MHz)
        picoadk_init();
        // printf("Hardware initialized, creating MIDI input\n");

        // NOW initialize MIDI input with correct clock frequency
        midi_input = new MIDIInput(uart0);  // Changed to UART0 to test

		for (uint8_t instance = 0; instance < VOICES_COUNT; instance++) {
			// Initialize Vult DSP context.
			Processor_process_init(processors[instance]);
			Processor_default_init(processors[instance]);
			Processor_default(processors[instance]);
		}

        // Initialize the audio subsystem
        ap = init_audio();

        // Create FreeRTOS Tasks for USB MIDI and printing statistics
        xTaskCreate(usb_midi_task, "USBMIDI", 4096, NULL, configMAX_PRIORITIES, NULL);
        xTaskCreate(uart_midi_task, "UARTMIDI", 4096, NULL, configMAX_PRIORITIES, NULL);
        // xTaskCreate(print_task, "TASKLIST", 1024, NULL, configMAX_PRIORITIES - 1, NULL);

        // Start the scheduler.
        vTaskStartScheduler();


    }

    // This is the I2S callback function. It is called when the I2S subsystem
    // needs more audio data. It is called at a fixed rate of 48kHz.
    // The audio data is stored in the audio_buffer_t struct.
    void __not_in_flash_func(i2s_callback_func()) {
        audio_buffer_t *buffer = take_audio_buffer(ap, false);
        if (buffer == NULL) {
            return;
        }

        int32_t *samples = (int32_t *)buffer->buffer->bytes;

        dsp_start = to_us_since_boot(get_absolute_time());

		// We are filling the buffer with 32-bit samples (2 channels)
		for (uint i = 0; i < buffer->max_sample_count / VOICES_COUNT; i++) {
			fix16_t left_out = 0;
			fix16_t right_out = 0;

			for (uint8_t instance = 0; instance < VOICES_COUNT; instance++) {
				// In case of the Vult Example, this is Processor_process(processors);
				Processor_process(processors[instance]);
				left_out += Processor_process_ret_0(processors[instance]) / VOICES_COUNT;
				right_out += Processor_process_ret_1(processors[instance]) / VOICES_COUNT;
			}

			samples[i * 2 + 0] = fix16_to_int32(left_out);  // LEFT
			samples[i * 2 + 1] = fix16_to_int32(right_out); // RIGHT
		}

        dsp_end = to_us_since_boot(get_absolute_time());


		buffer->sample_count = buffer->max_sample_count;
		give_audio_buffer(ap, buffer);
        return;
    }

#ifdef __cplusplus
}
#endif

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions