diff --git a/examples/pico_audio/CMakeLists.txt b/examples/pico_audio/CMakeLists.txt index 0ee57c06..cd4705ff 100644 --- a/examples/pico_audio/CMakeLists.txt +++ b/examples/pico_audio/CMakeLists.txt @@ -6,6 +6,7 @@ if (TARGET pico_audio_i2s) add_executable( audio demo.cpp + synth.cpp ) # Pull in pico libraries that we need diff --git a/examples/pico_audio/audio.hpp b/examples/pico_audio/audio.hpp new file mode 100644 index 00000000..af0a9c0a --- /dev/null +++ b/examples/pico_audio/audio.hpp @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +#pragma once +#include "pico/audio_i2s.h" + +#define SAMPLES_PER_BUFFER 256 + + +typedef int16_t (*buffer_callback)(void); + +struct audio_buffer_pool *init_audio(uint32_t sample_rate, uint8_t pin_data, uint8_t pin_bclk, uint8_t pio_sm=0, uint8_t dma_ch=0) { + static audio_format_t audio_format = { + .sample_freq = sample_rate, + .format = AUDIO_BUFFER_FORMAT_PCM_S16, + .channel_count = 1, + }; + + static struct audio_buffer_format producer_format = { + .format = &audio_format, + .sample_stride = 2 + }; + + struct audio_buffer_pool *producer_pool = audio_new_producer_pool( + &producer_format, + 3, + SAMPLES_PER_BUFFER + ); + + const struct audio_format *output_format; + + struct audio_i2s_config config = { + .data_pin = pin_data, + .clock_pin_base = pin_bclk, + .dma_channel = dma_ch, + .pio_sm = pio_sm, + }; + + output_format = audio_i2s_setup(&audio_format, &config); + if (!output_format) { + panic("PicoAudio: Unable to open audio device.\n"); + } + + bool status = audio_i2s_connect(producer_pool); + if (!status) { + panic("PicoAudio: Unable to connect to audio device.\n"); + } + + audio_i2s_set_enabled(true); + + return producer_pool; +} + +void update_buffer(struct audio_buffer_pool *ap, buffer_callback cb) { + struct audio_buffer *buffer = take_audio_buffer(ap, true); + int16_t *samples = (int16_t *) buffer->buffer->bytes; + for (uint i = 0; i < buffer->max_sample_count; i++) { + samples[i] = cb(); + } + buffer->sample_count = buffer->max_sample_count; + give_audio_buffer(ap, buffer); +} \ No newline at end of file diff --git a/examples/pico_audio/demo.cpp b/examples/pico_audio/demo.cpp index effb312c..701bc40b 100644 --- a/examples/pico_audio/demo.cpp +++ b/examples/pico_audio/demo.cpp @@ -1,97 +1,125 @@ -/** - * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - #include #include -#if PICO_ON_DEVICE - -#include "hardware/clocks.h" -#include "hardware/structs/clocks.h" - -#endif - #include "pico/stdlib.h" -#include "pico/audio_i2s.h" -#define PICO_AUDIO_PAC_I2S_DATA 9 -#define PICO_AUDIO_PAC_I2S_BITCLOCK 10 -#define SINE_WAVE_TABLE_LEN 2048 -#define SAMPLES_PER_BUFFER 256 +#include "synth.hpp" +#include "audio.hpp" -static int16_t sine_wave_table[SINE_WAVE_TABLE_LEN]; +#define PICO_AUDIO_PACK_I2S_DATA 9 +#define PICO_AUDIO_PACK_I2S_BCLK 10 -struct audio_buffer_pool *init_audio() { +#define SONG_LENGTH 384 +#define HAT 20000 +#define BASS 500 +#define SNARE 6000 +#define SUB 50 - static audio_format_t audio_format = { - .sample_freq = 24000, - .format = AUDIO_BUFFER_FORMAT_PCM_S16, - .channel_count = 1, - }; +using namespace synth; - static struct audio_buffer_format producer_format = { - .format = &audio_format, - .sample_stride = 2 - }; +synth::AudioChannel synth::channels[CHANNEL_COUNT]; - struct audio_buffer_pool *producer_pool = audio_new_producer_pool(&producer_format, 3, - SAMPLES_PER_BUFFER); // todo correct size - bool __unused ok; - const struct audio_format *output_format; - struct audio_i2s_config config = { - .data_pin = PICO_AUDIO_PAC_I2S_DATA, - .clock_pin_base = PICO_AUDIO_PAC_I2S_BITCLOCK, - .dma_channel = 0, - .pio_sm = 0, - }; +// Gadgetoid's amazing masterpiece! +const int16_t notes[5][SONG_LENGTH] = { + { // melody notes + 147, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 247, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 330, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 349, 0, 0, 0, 0, 0, 0, 0, 349, 0, 330, 0, 294, 0, 220, 0, 262, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 247, 0, 0, 0, 0, 0, 0, 0, 247, 0, 220, 0, 196, 0, 147, 0, 175, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, + 147, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 247, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 330, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 349, 0, 0, 0, 0, 0, 0, 0, 349, 0, 330, 0, 294, 0, 220, 0, 262, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 247, 0, 0, 0, 0, 0, 0, 0, 247, 0, 220, 0, 196, 0, 147, 0, 175, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, + 147, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 247, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 175, 0, 0, 0, 0, 0, 0, 0, 175, 0, 196, 0, 220, 0, 262, 0, 330, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 349, 0, 0, 0, 0, 0, 0, 0, 349, 0, 330, 0, 294, 0, 220, 0, 262, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 247, 0, 0, 0, 0, 0, 0, 0, 247, 0, 262, 0, 294, 0, 392, 0, 440, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }, + { // rhythm notes + 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 392, 0, 523, 0, 659, 0, 523, 0, 392, 0, 523, 0, 659, 0, 523, 0, 698, 0, 587, 0, 440, 0, 587, 0, 698, 0, 587, 0, 440, 0, 587, 0, 523, 0, 440, 0, 330, 0, 440, 0, 523, 0, 440, 0, 330, 0, 440, 0, 349, 0, 294, 0, 220, 0, 294, 0, 349, 0, 294, 0, 220, 0, 294, 0, 262, 0, 247, 0, 220, 0, 175, 0, 165, 0, 147, 0, 131, 0, 98, 0, + 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 392, 0, 523, 0, 659, 0, 523, 0, 392, 0, 523, 0, 659, 0, 523, 0, 698, 0, 587, 0, 440, 0, 587, 0, 698, 0, 587, 0, 440, 0, 587, 0, 523, 0, 440, 0, 330, 0, 440, 0, 523, 0, 440, 0, 330, 0, 440, 0, 349, 0, 294, 0, 220, 0, 294, 0, 349, 0, 294, 0, 220, 0, 294, 0, 262, 0, 247, 0, 220, 0, 175, 0, 165, 0, 147, 0, 131, 0, 98, 0, + 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 294, 0, 440, 0, 587, 0, 440, 0, 392, 0, 523, 0, 659, 0, 523, 0, 392, 0, 523, 0, 659, 0, 523, 0, 698, 0, 587, 0, 440, 0, 587, 0, 698, 0, 587, 0, 440, 0, 587, 0, 523, 0, 440, 0, 330, 0, 440, 0, 523, 0, 440, 0, 330, 0, 440, 0, 349, 0, 294, 0, 220, 0, 294, 0, 349, 0, 294, 0, 220, 0, 294, 0, 262, 0, 247, 0, 220, 0, 175, 0, 165, 0, 147, 0, 131, 0, 98, 0, + }, + { // drum beats + BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, + BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, + BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, BASS, -1, BASS, -1, 0, 0, 0, 0, 0, 0, SNARE, 0, -1, 0, 0, 0, 0, 0 + }, + { // hi-hat + HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, + HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, + HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1, HAT, -1 + }, + { // bass notes under bass drum + SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, + SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, + SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, SUB, -1, SUB, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0 + }, +}; - output_format = audio_i2s_setup(&audio_format, &config); - if (!output_format) { - panic("PicoAudio: Unable to open audio device.\n"); +void update_playback(void) { + static uint16_t prev_beat = 1; + static uint16_t beat = 0; + + absolute_time_t at = get_absolute_time(); + uint64_t tick_ms = to_us_since_boot(at) / 1000; + + beat = (tick_ms / 100) % SONG_LENGTH; // 100ms per beat + + if (beat == prev_beat) return; + prev_beat = beat; + + for(uint8_t i = 0; i < 5; i++) { + if(notes[i][beat] > 0) { + channels[i].frequency = notes[i][beat]; + channels[i].trigger_attack(); + } else if (notes[i][beat] == -1) { + channels[i].trigger_release(); } - - ok = audio_i2s_connect(producer_pool); - assert(ok); - audio_i2s_set_enabled(true); - - return producer_pool; + } } int main() { - stdio_init_all(); + stdio_init_all(); + struct audio_buffer_pool *ap = init_audio(synth::sample_rate, PICO_AUDIO_PACK_I2S_DATA, PICO_AUDIO_PACK_I2S_BCLK); - for (int i = 0; i < SINE_WAVE_TABLE_LEN; i++) { - sine_wave_table[i] = 32767 * cosf(i * 2 * (float) (M_PI / SINE_WAVE_TABLE_LEN)); - } + // configure voices - struct audio_buffer_pool *ap = init_audio(); - uint32_t step = 0x200000; - uint32_t pos = 0; - uint32_t pos_max = 0x10000 * SINE_WAVE_TABLE_LEN; - uint vol = 128; - while (true) { - int c = getchar_timeout_us(0); - if (c >= 0) { - if (c == '-' && vol) vol -= 4; - if ((c == '=' || c == '+') && vol < 255) vol += 4; - if (c == '[' && step > 0x10000) step -= 0x10000; - if (c == ']' && step < (SINE_WAVE_TABLE_LEN / 16) * 0x20000) step += 0x10000; - if (c == 'q') break; - printf("vol = %d, step = %d \r", vol, step >> 16); - } - struct audio_buffer *buffer = take_audio_buffer(ap, true); - int16_t *samples = (int16_t *) buffer->buffer->bytes; - for (uint i = 0; i < buffer->max_sample_count; i++) { - samples[i] = (vol * sine_wave_table[pos >> 16u]) >> 8u; - pos += step; - if (pos >= pos_max) pos -= pos_max; - } - buffer->sample_count = buffer->max_sample_count; - give_audio_buffer(ap, buffer); - } - puts("\n"); - return 0; + // melody track + channels[0].waveforms = Waveform::TRIANGLE | Waveform::SQUARE; + channels[0].attack_ms = 16; + channels[0].decay_ms = 168; + channels[0].sustain = 0xafff; + channels[0].release_ms = 168; + channels[0].volume = 10000; + + // rhythm track + channels[1].waveforms = Waveform::SINE | Waveform::SQUARE; + channels[1].attack_ms = 38; + channels[1].decay_ms = 300; + channels[1].sustain = 0; + channels[1].release_ms = 0; + channels[1].volume = 12000; + + // drum track + channels[2].waveforms = Waveform::NOISE; + channels[2].attack_ms = 5; + channels[2].decay_ms = 10; + channels[2].sustain = 16000; + channels[2].release_ms = 100; + channels[2].volume = 18000; + + // hi-hat track + channels[3].waveforms = Waveform::NOISE; + channels[3].attack_ms = 5; + channels[3].decay_ms = 5; + channels[3].sustain = 8000; + channels[3].release_ms = 40; + channels[3].volume = 8000; + + // bass track + channels[4].waveforms = Waveform::SQUARE; + channels[4].attack_ms = 10; + channels[4].decay_ms = 100; + channels[4].sustain = 0; + channels[4].release_ms = 500; + channels[4].volume = 12000; + + while (true) { + update_playback(); + update_buffer(ap, get_audio_frame); + } + + return 0; } diff --git a/examples/pico_audio/synth.cpp b/examples/pico_audio/synth.cpp new file mode 100644 index 00000000..df8fa9b8 --- /dev/null +++ b/examples/pico_audio/synth.cpp @@ -0,0 +1,160 @@ +#include "synth.hpp" + +namespace synth { + + uint32_t prng_xorshift_state = 0x32B71700; + + uint32_t prng_xorshift_next() { + uint32_t x = prng_xorshift_state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + prng_xorshift_state = x; + return x; + } + + int32_t prng_normal() { + // rough approximation of a normal distribution + uint32_t r0 = prng_xorshift_next(); + uint32_t r1 = prng_xorshift_next(); + uint32_t n = ((r0 & 0xffff) + (r1 & 0xffff) + (r0 >> 16) + (r1 >> 16)) / 2; + return n - 0xffff; + } + + uint16_t volume = 0xffff; + const int16_t sine_waveform[256] = {-32768,-32758,-32729,-32679,-32610,-32522,-32413,-32286,-32138,-31972,-31786,-31581,-31357,-31114,-30853,-30572,-30274,-29957,-29622,-29269,-28899,-28511,-28106,-27684,-27246,-26791,-26320,-25833,-25330,-24812,-24279,-23732,-23170,-22595,-22006,-21403,-20788,-20160,-19520,-18868,-18205,-17531,-16846,-16151,-15447,-14733,-14010,-13279,-12540,-11793,-11039,-10279,-9512,-8740,-7962,-7180,-6393,-5602,-4808,-4011,-3212,-2411,-1608,-804,0,804,1608,2411,3212,4011,4808,5602,6393,7180,7962,8740,9512,10279,11039,11793,12540,13279,14010,14733,15447,16151,16846,17531,18205,18868,19520,20160,20788,21403,22006,22595,23170,23732,24279,24812,25330,25833,26320,26791,27246,27684,28106,28511,28899,29269,29622,29957,30274,30572,30853,31114,31357,31581,31786,31972,32138,32286,32413,32522,32610,32679,32729,32758,32767,32758,32729,32679,32610,32522,32413,32286,32138,31972,31786,31581,31357,31114,30853,30572,30274,29957,29622,29269,28899,28511,28106,27684,27246,26791,26320,25833,25330,24812,24279,23732,23170,22595,22006,21403,20788,20160,19520,18868,18205,17531,16846,16151,15447,14733,14010,13279,12540,11793,11039,10279,9512,8740,7962,7180,6393,5602,4808,4011,3212,2411,1608,804,0,-804,-1608,-2411,-3212,-4011,-4808,-5602,-6393,-7180,-7962,-8740,-9512,-10279,-11039,-11793,-12540,-13279,-14010,-14733,-15447,-16151,-16846,-17531,-18205,-18868,-19520,-20160,-20788,-21403,-22006,-22595,-23170,-23732,-24279,-24812,-25330,-25833,-26320,-26791,-27246,-27684,-28106,-28511,-28899,-29269,-29622,-29957,-30274,-30572,-30853,-31114,-31357,-31581,-31786,-31972,-32138,-32286,-32413,-32522,-32610,-32679,-32729,-32758}; + + bool is_audio_playing() { + if(volume == 0) { + return false; + } + + bool any_channel_playing = false; + for(int c = 0; c < CHANNEL_COUNT; c++) { + if(channels[c].volume > 0 && channels[c].adsr_phase != ADSRPhase::OFF) { + any_channel_playing = true; + } + } + + return any_channel_playing; + } + + int16_t get_audio_frame() { + int32_t sample = 0; // used to combine channel output + + for(int c = 0; c < CHANNEL_COUNT; c++) { + + auto &channel = channels[c]; + + // increment the waveform position counter. this provides an + // Q16 fixed point value representing how far through + // the current waveform we are + channel.waveform_offset += ((channel.frequency * 256) << 8) / sample_rate; + + if(channel.adsr_phase == ADSRPhase::OFF) { + continue; + } + + if ((channel.adsr_frame >= channel.adsr_end_frame) && (channel.adsr_phase != ADSRPhase::SUSTAIN)) { + switch (channel.adsr_phase) { + case ADSRPhase::ATTACK: + channel.trigger_decay(); + break; + case ADSRPhase::DECAY: + channel.trigger_sustain(); + break; + case ADSRPhase::RELEASE: + channel.off(); + break; + default: + break; + } + } + + channel.adsr += channel.adsr_step; + channel.adsr_frame++; + + if(channel.waveform_offset & 0x10000) { + // if the waveform offset overflows then generate a new + // random noise sample + channel.noise = prng_normal(); + } + + channel.waveform_offset &= 0xffff; + + // check if any waveforms are active for this channel + if(channel.waveforms) { + uint8_t waveform_count = 0; + int32_t channel_sample = 0; + + if(channel.waveforms & Waveform::NOISE) { + channel_sample += channel.noise; + waveform_count++; + } + + if(channel.waveforms & Waveform::SAW) { + channel_sample += (int32_t)channel.waveform_offset - 0x7fff; + waveform_count++; + } + + // creates a triangle wave of ^ + if (channel.waveforms & Waveform::TRIANGLE) { + if (channel.waveform_offset < 0x7fff) { // initial quarter up slope + channel_sample += int32_t(channel.waveform_offset * 2) - int32_t(0x7fff); + } + else { // final quarter up slope + channel_sample += int32_t(0x7fff) - ((int32_t(channel.waveform_offset) - int32_t(0x7fff)) * 2); + } + waveform_count++; + } + + if (channel.waveforms & Waveform::SQUARE) { + channel_sample += (channel.waveform_offset < channel.pulse_width) ? 0x7fff : -0x7fff; + waveform_count++; + } + + if(channel.waveforms & Waveform::SINE) { + // the sine_waveform sample contains 256 samples in + // total so we'll just use the most significant bits + // of the current waveform position to index into it + channel_sample += sine_waveform[channel.waveform_offset >> 8]; + waveform_count++; + } + + if(channel.waveforms & Waveform::WAVE) { + channel_sample += channel.wave_buffer[channel.wave_buf_pos]; + if (++channel.wave_buf_pos == 64) { + channel.wave_buf_pos = 0; + if(channel.wave_buffer_callback) + channel.wave_buffer_callback(channel); + } + waveform_count++; + } + + channel_sample = channel_sample / waveform_count; + + channel_sample = (int64_t(channel_sample) * int32_t(channel.adsr >> 8)) >> 16; + + // apply channel volume + channel_sample = (int64_t(channel_sample) * int32_t(channel.volume)) >> 16; + + // apply channel filter + //if (channel.filter_enable) { + //float filter_epow = 1 - expf(-(1.0f / 22050.0f) * 2.0f * pi * int32_t(channel.filter_cutoff_frequency)); + //channel_sample += (channel_sample - channel.filter_last_sample) * filter_epow; + //} + + //channel.filter_last_sample = channel_sample; + + // combine channel sample into the final sample + sample += channel_sample; + } + } + + sample = (int64_t(sample) * int32_t(volume)) >> 16; + + // clip result to 16-bit + sample = sample <= -0x8000 ? -0x8000 : (sample > 0x7fff ? 0x7fff : sample); + return sample; + } +} \ No newline at end of file diff --git a/examples/pico_audio/synth.hpp b/examples/pico_audio/synth.hpp new file mode 100644 index 00000000..741e2dab --- /dev/null +++ b/examples/pico_audio/synth.hpp @@ -0,0 +1,132 @@ +#pragma once + +#include + +namespace synth { + + // The duration a note is played is determined by the amount of attack, + // decay, and release, combined with the length of the note as defined by + // the user. + // + // - Attack: number of milliseconds it takes for a note to hit full volume + // - Decay: number of milliseconds it takes for a note to settle to sustain volume + // - Sustain: percentage of full volume that the note sustains at (duration implied by other factors) + // - Release: number of milliseconds it takes for a note to reduce to zero volume after it has ended + // + // Attack (750ms) - Decay (500ms) -------- Sustain ----- Release (250ms) + // + // + + + + + // | | | | + // | | | | + // | | | | + // v v v v + // 0ms 1000ms 2000ms 3000ms 4000ms + // + // | XXXX | | | | + // | X X|XX | | | + // | X | XXX | | | + // | X | XXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX| | + // | X | | |X | + // | X | | |X | + // | X | | | X | + // | X | | | X | + // | X | | | X | + // | X | | | X | + // | X | | | X | + // | X | | | X | + // | X + + + | + + + | + + + | + + + | + + // | X | | | | | | | | | | | | | | | | | + // |X | | | | | | | | | | | | | | | | | + // +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+---> + + #define CHANNEL_COUNT 8 + + constexpr float pi = 3.14159265358979323846f; + + const uint32_t sample_rate = 44100; + extern uint16_t volume; + + enum Waveform { + NOISE = 128, + SQUARE = 64, + SAW = 32, + TRIANGLE = 16, + SINE = 8, + WAVE = 1 + }; + + enum class ADSRPhase : uint8_t { + ATTACK, + DECAY, + SUSTAIN, + RELEASE, + OFF + }; + + struct AudioChannel { + uint8_t waveforms = 0; // bitmask for enabled waveforms (see AudioWaveform enum for values) + uint16_t frequency = 660; // frequency of the voice (Hz) + uint16_t volume = 0xffff; // channel volume (default 50%) + + uint16_t attack_ms = 2; // attack period + uint16_t decay_ms = 6; // decay period + uint16_t sustain = 0xffff; // sustain volume + uint16_t release_ms = 1; // release period + uint16_t pulse_width = 0x7fff; // duty cycle of square wave (default 50%) + int16_t noise = 0; // current noise value + + uint32_t waveform_offset = 0; // voice offset (Q8) + + int32_t filter_last_sample = 0; + bool filter_enable = false; + uint16_t filter_cutoff_frequency = 0; + + uint32_t adsr_frame = 0; // number of frames into the current ADSR phase + uint32_t adsr_end_frame = 0; // frame target at which the ADSR changes to the next phase + uint32_t adsr = 0; + int32_t adsr_step = 0; + ADSRPhase adsr_phase = ADSRPhase::OFF; + + uint8_t wave_buf_pos = 0; // + int16_t wave_buffer[64]; // buffer for arbitrary waveforms. small as it's filled by user callback + + void *user_data = nullptr; + void (*wave_buffer_callback)(AudioChannel &channel); + + void trigger_attack() { + adsr_frame = 0; + adsr_phase = ADSRPhase::ATTACK; + adsr_end_frame = (attack_ms * sample_rate) / 1000; + adsr_step = (int32_t(0xffffff) - int32_t(adsr)) / int32_t(adsr_end_frame); + } + void trigger_decay() { + adsr_frame = 0; + adsr_phase = ADSRPhase::DECAY; + adsr_end_frame = (decay_ms * sample_rate) / 1000; + adsr_step = (int32_t(sustain << 8) - int32_t(adsr)) / int32_t(adsr_end_frame); + } + void trigger_sustain() { + adsr_frame = 0; + adsr_phase = ADSRPhase::SUSTAIN; + adsr_end_frame = 0; + adsr_step = 0; + } + void trigger_release() { + adsr_frame = 0; + adsr_phase = ADSRPhase::RELEASE; + adsr_end_frame = (release_ms * sample_rate) / 1000; + adsr_step = (int32_t(0) - int32_t(adsr)) / int32_t(adsr_end_frame); + } + void off() { + adsr_frame = 0; + adsr_phase = ADSRPhase::OFF; + adsr_step = 0; + } + }; + + extern AudioChannel channels[CHANNEL_COUNT]; + + int16_t get_audio_frame(); + bool is_audio_playing(); + +}