diff --git a/examples/AMY_Spresense_Test/AMY_Spresense_Test.ino b/examples/AMY_Spresense_Test/AMY_Spresense_Test.ino new file mode 100644 index 00000000..c9171852 --- /dev/null +++ b/examples/AMY_Spresense_Test/AMY_Spresense_Test.ino @@ -0,0 +1,99 @@ +#include + +// AMY_Spresense_Analog +// +// Minimal AMY example for Sony Spresense analog output. +// Spresense backend in src/spresense_support.cpp handles DAC output. + +void setup() { +#ifdef LED_BUILTIN + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, HIGH); +#endif + + Serial.begin(115200); + delay(1000); + Serial.println("AMY Spresense analog output"); + + amy_config_t amy_config = amy_default_config(); + amy_config.features.startup_bleep = 1; + amy_config.features.default_synths = 1; + + // Default synth set may require around 90+ oscillators; keep headroom. + amy_config.max_oscs = 128; + + // Use Spresense platform audio path (analog DAC output backend). + // To use I2S output instead, uncomment the next line. + // amy_config.spresense_output_device = SPRESENSE_OUTPUT_I2S; + amy_config.audio = AMY_AUDIO_IS_SPRESENSE; + amy_config.midi = AMY_MIDI_IS_NONE; + + amy_start(amy_config); + +} + +void loop() { + amy_update(); + + static uint32_t last_ms = 0; + uint32_t now = millis(); + if (now - last_ms > 600) { + last_ms = now; + static int root = 60; + static bool play_chord = false; + uint32_t t = amy_sysclock(); + + if (!play_chord) { + Serial.print("single note "); + Serial.print(root); + Serial.print(" at "); + Serial.println(t); + + amy_event on = amy_default_event(); + on.time = t; + on.synth = 1; + on.midi_note = root; + on.velocity = 0.8f; + amy_add_event(&on); + + amy_event off = amy_default_event(); + off.time = t + 220; + off.synth = 1; + off.midi_note = root; + off.velocity = 0.0f; + amy_add_event(&off); + } else { + const int chord[3] = {root, root + 4, root + 7}; + Serial.print("chord "); + Serial.print(chord[0]); + Serial.print(","); + Serial.print(chord[1]); + Serial.print(","); + Serial.print(chord[2]); + Serial.print(" at "); + Serial.println(t); + + for (int i = 0; i < 3; ++i) { + amy_event on = amy_default_event(); + on.time = t; + on.synth = 1; + on.midi_note = chord[i]; + on.velocity = 0.7f; + amy_add_event(&on); + + amy_event off = amy_default_event(); + off.time = t + 220; + off.synth = 1; + off.midi_note = chord[i]; + off.velocity = 0.0f; + amy_add_event(&off); + } + } + + play_chord = !play_chord; + root += 2; + if (root > 72) { + root = 60; + } + } +} diff --git a/library.properties b/library.properties index 58c90792..28f03dac 100644 --- a/library.properties +++ b/library.properties @@ -6,6 +6,6 @@ sentence=AMY, the Music Synthesizer Library paragraph=AMY supports many types of oscillators, filters, envelopes, analog, FM, PCM, Karplus-strong, reverb, chorus, echo category=Signal Input/Output url=http://github.com/shorepine/amy -architectures=esp32,stm32,rp2040,mbed_rp2040,rp2350,mbed_rp2350,teensy,STM32F1,STM32F4,STM32H7,nrf52,samd,avr +architectures=esp32,stm32,rp2040,mbed_rp2040,rp2350,mbed_rp2350,teensy,STM32F1,STM32F4,STM32H7,nrf52,samd,avr,spresense includes=AMY-Arduino.h depends= diff --git a/src/amy.c b/src/amy.c index f9ea2fe5..b304fecd 100644 --- a/src/amy.c +++ b/src/amy.c @@ -48,6 +48,9 @@ uint64_t profile_start_us = 0; int64_t amy_get_us() { return esp_timer_get_time(); } #elif defined PICO_ON_DEVICE int64_t amy_get_us() { return to_us_since_boot(get_absolute_time()); } +#elif defined(ARDUINO_ARCH_SPRESENSE) +extern unsigned long micros(void); +int64_t amy_get_us() { return (int64_t)micros(); } #elif defined(_WIN32) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN diff --git a/src/amy.h b/src/amy.h index 071a280b..fb3b1554 100644 --- a/src/amy.h +++ b/src/amy.h @@ -72,6 +72,8 @@ extern const uint32_t pcm_wavetable_len; #ifdef AMY_DAISY #define AMY_SAMPLE_RATE 48000 +#elif defined(ARDUINO_ARCH_SPRESENSE) +#define AMY_SAMPLE_RATE 48000 #elif defined __EMSCRIPTEN__ #define AMY_SAMPLE_RATE 48000 #else @@ -121,6 +123,8 @@ extern const uint32_t pcm_wavetable_len; #define AMY_HAS_REVERB (amy_global.config.features.reverb) #define AMY_HAS_AUDIO_IN (amy_global.config.features.audio_in) #define AMY_HAS_I2S (amy_global.config.audio == AMY_AUDIO_IS_I2S) +#define AMY_HAS_SPRESENSE_AUDIO (amy_global.config.audio == AMY_AUDIO_IS_SPRESENSE) +#define AMY_HAS_PLATFORM_AUDIO_OUT (AMY_HAS_I2S || AMY_HAS_SPRESENSE_AUDIO) #define AMY_HAS_DEFAULT_SYNTHS (amy_global.config.features.default_synths) #define AMY_HAS_CHORUS (amy_global.config.features.chorus) #define AMY_HAS_ECHO (amy_global.config.features.echo) @@ -313,7 +317,7 @@ typedef int amy_err_t; #include "amy_fixedpoint.h" -#if defined ARDUINO && !defined TLONG && !defined ESP_PLATFORM +#if defined ARDUINO && !defined TLONG && !defined ESP_PLATFORM && !defined(ARDUINO_ARCH_SPRESENSE) #include "avr/pgmspace.h" // for PROGMEM, DMAMEM, FASTRUN #else #define PROGMEM @@ -634,6 +638,13 @@ typedef struct delay_line { #define AMY_AUDIO_IS_I2S 0x01 #define AMY_AUDIO_IS_USB_GADGET 0x02 #define AMY_AUDIO_IS_MINIAUDIO 0x04 +#define AMY_AUDIO_IS_SPRESENSE 0x08 + +#if defined(ARDUINO_ARCH_SPRESENSE) +// Spresense audio output device selection +#define SPRESENSE_OUTPUT_ANALOG 0 +#define SPRESENSE_OUTPUT_I2S 1 +#endif #define AMY_MIDI_IS_NONE 0x0 #define AMY_MIDI_IS_UART 0x01 @@ -721,6 +732,11 @@ typedef struct { int8_t capture_device_id; int8_t playback_device_id; +#if defined(ARDUINO_ARCH_SPRESENSE) + // Spresense audio output device (SPRESENSE_OUTPUT_ANALOG or SPRESENSE_OUTPUT_I2S) + uint8_t spresense_output_device; +#endif + } amy_config_t; typedef struct reverb_state { diff --git a/src/amy_midi.c b/src/amy_midi.c index 18b13026..f94c42f5 100644 --- a/src/amy_midi.c +++ b/src/amy_midi.c @@ -619,6 +619,14 @@ void stop_midi() { } #endif +#if defined(ARDUINO_ARCH_SPRESENSE) +void run_midi() { +} + +void stop_midi() { +} +#endif + #ifdef __linux__ void stop_midi() { diff --git a/src/api.c b/src/api.c index 9aaa4ea9..1737c2ad 100644 --- a/src/api.c +++ b/src/api.c @@ -95,6 +95,11 @@ amy_config_t amy_default_config() { c.midi_uart = 1; // This is MIDI UART _number_, like index #endif + #if defined(ARDUINO_ARCH_SPRESENSE) + c.audio = AMY_AUDIO_IS_SPRESENSE; + c.spresense_output_device = SPRESENSE_OUTPUT_ANALOG; // Default to analog DAC output + #endif + return c; } @@ -407,7 +412,7 @@ int16_t *amy_update() { // Single function to update buffers. amy_update_tasks(); int16_t *block = amy_render_audio(); - if (AMY_HAS_I2S && !amy_global.i2s_is_in_background) { + if (AMY_HAS_PLATFORM_AUDIO_OUT && !amy_global.i2s_is_in_background) { amy_i2s_write( (uint8_t *)block, AMY_BLOCK_SIZE * AMY_NCHANS * sizeof(int16_t) ); diff --git a/src/i2s.c b/src/i2s.c index ac5797f7..1c4c074a 100644 --- a/src/i2s.c +++ b/src/i2s.c @@ -5,8 +5,8 @@ // rp2350, rp2040 // teensy 3.6, 4.0, 4.1 -// Only run this code on MCUs -#if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO) || defined(ARDUINO_ARCH_RP2350) +// Only run this code on MCU targets with platform backends in this file. +#if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_ARCH_RP2350) || defined(ARDUINO_ARCH_SPRESENSE) #include "amy.h" @@ -574,6 +574,39 @@ void amy_platform_deinit() { } +#elif defined(ARDUINO_ARCH_SPRESENSE) + +// Spresense analog output backend is implemented in C++. +extern void spresense_setup_audio(amy_config_t *config); +extern void spresense_teardown_audio(amy_config_t *config); +extern size_t spresense_audio_write(const uint8_t *buffer, size_t nbytes); + +void amy_platform_init() { + if (AMY_HAS_SPRESENSE_AUDIO) { + spresense_setup_audio(&amy_global.config); + } +} + +void amy_platform_deinit() { + if (AMY_HAS_SPRESENSE_AUDIO) { + spresense_teardown_audio(&amy_global.config); + } +} + +void amy_update_tasks() { + amy_execute_deltas(); +} + +int16_t *amy_render_audio() { + amy_render(0, AMY_OSCS, 0); + return amy_fill_buffer(); +} + +size_t amy_i2s_write(const uint8_t *buffer, size_t nbytes) { + return spresense_audio_write(buffer, nbytes); +} + + #elif defined __IMXRT1062__ diff --git a/src/spresense_support.cpp b/src/spresense_support.cpp new file mode 100644 index 00000000..b8b8d1cc --- /dev/null +++ b/src/spresense_support.cpp @@ -0,0 +1,188 @@ +#if defined(ARDUINO_ARCH_SPRESENSE) + +#include +#include +#include +#include + +#define AMY_PCM_FRAME_SIZE (256) // Samples per frame (match AMY block size) +#define AMY_BYTEWIDTH (2) // 16-bit +#define AMY_CHANNELS (2) // Stereo +#define AMY_READSIZE (AMY_PCM_FRAME_SIZE * AMY_BYTEWIDTH * AMY_CHANNELS) + +static OutputMixer *theMixer = nullptr; +static AsPcmDataParam g_pcm_frame; +static volatile bool g_frame_valid = false; + +// Forward declaration for callback +extern "C" void spresense_mixer_callback(int32_t identifier, bool is_end); + +extern "C" { + +static bool spresense_make_silence_frame(AsPcmDataParam *pcm_param) { + if (pcm_param == nullptr) { + return false; + } + if (pcm_param->mh.allocSeg(S0_REND_PCM_BUF_POOL, AMY_READSIZE) != ERR_OK) { + return false; + } + + uint8_t *pcm_buffer = (uint8_t *)pcm_param->mh.getPa(); + memset(pcm_buffer, 0, AMY_READSIZE); + + pcm_param->identifier = 0; + pcm_param->callback = 0; + pcm_param->bit_length = 16; + pcm_param->size = AMY_READSIZE; + pcm_param->sample = AMY_PCM_FRAME_SIZE; + pcm_param->is_end = false; + pcm_param->is_valid = true; + return true; +} + +/** + * @brief Mixer done callback procedure + */ +static void outputmixer_done_callback(MsgQueId requester_dtq, + MsgType reply_of, + AsOutputMixDoneParam *done_param) { + // Not used in this implementation + (void)requester_dtq; + (void)reply_of; + (void)done_param; +} + +/** + * @brief Mixer data send callback - called when mixer needs more PCM data + */ +void spresense_mixer_callback(int32_t identifier, bool is_end) { + (void)identifier; + + if (is_end) { + return; + } + + AsPcmDataParam pcm_param; + bool has_frame = g_frame_valid; + if (has_frame) { + pcm_param = g_pcm_frame; + g_frame_valid = false; + } + if (!has_frame) { + if (!spresense_make_silence_frame(&pcm_param)) { + return; + } + } + + (void)theMixer->sendData(OutputMixer0, spresense_mixer_callback, pcm_param); +} + +void spresense_setup_audio(amy_config_t *config) { + if (theMixer == nullptr && config != nullptr) { + // Initialize memory pools + initMemoryPools(); + createStaticPools(MEM_LAYOUT_PLAYER); + + // Get OutputMixer instance + theMixer = OutputMixer::getInstance(); + theMixer->activateBaseband(); + + // Create mixer object with done callback + theMixer->create(outputmixer_done_callback); + + // Set rendering clock mode (Normal = 48kHz) + theMixer->setRenderingClkMode(OUTPUTMIXER_RNDCLK_NORMAL); + + // Select output device based on config + int output_device = (config->spresense_output_device == SPRESENSE_OUTPUT_I2S) + ? I2SOutputDevice + : HPOutputDevice; + + // Activate mixer with selected output device + theMixer->activate(OutputMixer0, output_device, outputmixer_done_callback); + + // Set output gain to -6 dB (0.1 dB units). + theMixer->setVolume(-60, -60, -60); + + // Unmute amplifier + board_external_amp_mute_control(false); + + g_frame_valid = false; + + // Prime callback chain with 3 initial frames. + int err = OUTPUTMIXER_ECODE_OK; + for (int i = 0; i < 3; ++i) { + AsPcmDataParam pcm_param; + if (!spresense_make_silence_frame(&pcm_param)) { + err = -1; + break; + } + + err = theMixer->sendData(OutputMixer0, spresense_mixer_callback, pcm_param); + if (err != OUTPUTMIXER_ECODE_OK) { + break; + } + } + g_frame_valid = false; + } +} + +void spresense_teardown_audio(amy_config_t *config) { + (void)config; + if (theMixer != nullptr) { + g_frame_valid = false; + AsPcmDataParam pcm_param; + if (pcm_param.mh.allocSeg(S0_REND_PCM_BUF_POOL, AMY_READSIZE) == ERR_OK) { + pcm_param.identifier = 0; + pcm_param.callback = 0; + pcm_param.bit_length = 16; + pcm_param.size = 0; + pcm_param.sample = 0; + pcm_param.is_end = true; + pcm_param.is_valid = false; + theMixer->sendData(OutputMixer0, spresense_mixer_callback, pcm_param); + } + theMixer = nullptr; + } +} + +/** + * @brief Trigger audio stream - initiates mixer callback chain + */ +size_t spresense_audio_write(const uint8_t *buffer, size_t nbytes) { + // Wait until frame is consumed by callback + for (;;) { + if (!g_frame_valid) { break; } + delayMicroseconds(50); + } + + AsPcmDataParam pcm_param; + if (pcm_param.mh.allocSeg(S0_REND_PCM_BUF_POOL, AMY_READSIZE) != ERR_OK) { + return 0; + } + + uint8_t *pcm_buffer = (uint8_t *)pcm_param.mh.getPa(); + size_t copy_size = (nbytes < AMY_READSIZE) ? nbytes : AMY_READSIZE; + memset(pcm_buffer, 0, AMY_READSIZE); + if (buffer != nullptr && copy_size > 0) { + memcpy(pcm_buffer, buffer, copy_size); + } + + pcm_param.identifier = 0; + pcm_param.callback = 0; + pcm_param.bit_length = 16; + pcm_param.size = AMY_READSIZE; + pcm_param.sample = AMY_PCM_FRAME_SIZE; + pcm_param.is_end = false; + pcm_param.is_valid = true; + + g_pcm_frame = pcm_param; + __asm__ volatile("dmb" ::: "memory"); // Ensure frame data is visible before setting valid flag + g_frame_valid = true; + + return nbytes; +} + +} // extern "C" + +#endif