audiocore: Integrate mp3 decoder
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@ hardware/tonberry-pico/tonberry-pico-backups/
|
||||
*.kicad_sch-bak
|
||||
*.kicad_sch.lck
|
||||
software/build
|
||||
compile_commands.json
|
||||
.dir-locals.el
|
||||
|
||||
@@ -1,27 +1,88 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
||||
// Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
#include "audiocore.h"
|
||||
#include "i2s.h"
|
||||
#include "mp3.h"
|
||||
|
||||
#include "py/mperrno.h"
|
||||
|
||||
void core1_main(void)
|
||||
void __time_critical_func(volume_adjust)(int16_t *buf, size_t samples, uint16_t scalef)
|
||||
{
|
||||
if (!i2s_init(shared_context.out_pin, shared_context.sideset_base, shared_context.samplerate)) {
|
||||
multicore_fifo_push_blocking(MP_EIO);
|
||||
return;
|
||||
for (size_t pos = 0; pos < samples; ++pos) {
|
||||
buf[pos] = ((int32_t)buf[pos] * scalef) >> 15;
|
||||
}
|
||||
|
||||
multicore_fifo_push_blocking(0);
|
||||
uint32_t cmd;
|
||||
while ((cmd = multicore_fifo_pop_blocking()) != AUDIOCORE_CMD_SHUTDOWN) {
|
||||
switch (cmd) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
i2s_deinit();
|
||||
multicore_fifo_push_blocking(0);
|
||||
}
|
||||
|
||||
void __time_critical_func(core1_main)(void)
|
||||
{
|
||||
uint32_t ret = 0;
|
||||
bool running = true, playing = false;
|
||||
if (!i2s_init(shared_context.out_pin, shared_context.sideset_base)) {
|
||||
ret = MP_EIO;
|
||||
goto out;
|
||||
}
|
||||
if (!mp3_init()) {
|
||||
ret = MP_ENOMEM;
|
||||
goto out_i2s;
|
||||
}
|
||||
|
||||
multicore_fifo_push_blocking(0);
|
||||
uint32_t current_volume = AUDIOCORE_MAX_VOLUME >> 4;
|
||||
bool flushing = false;
|
||||
while (running) {
|
||||
uint32_t cmd;
|
||||
uint32_t *buf;
|
||||
if (multicore_fifo_rvalid()) {
|
||||
cmd = multicore_fifo_pop_blocking();
|
||||
switch (cmd) {
|
||||
case AUDIOCORE_CMD_SHUTDOWN:
|
||||
running = false;
|
||||
break;
|
||||
case AUDIOCORE_CMD_SET_VOLUME: {
|
||||
const uint32_t new_volume = multicore_fifo_pop_blocking();
|
||||
if (new_volume > AUDIOCORE_MAX_VOLUME) {
|
||||
multicore_fifo_push_blocking(1);
|
||||
} else {
|
||||
current_volume = new_volume;
|
||||
multicore_fifo_push_blocking(0);
|
||||
}
|
||||
} break;
|
||||
case AUDIOCORE_CMD_FLUSH:
|
||||
flushing = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((buf = i2s_next_buf()) != NULL) {
|
||||
unsigned samplerate;
|
||||
// decode one frame
|
||||
if (mp3_decode(buf, &samplerate)) {
|
||||
if (!playing) {
|
||||
i2s_play(samplerate);
|
||||
playing = true;
|
||||
}
|
||||
volume_adjust((int16_t *)buf, 2304, current_volume);
|
||||
i2s_commit_buf(buf);
|
||||
continue;
|
||||
}
|
||||
/* mp3_decode returned false: not enough data in buffer */
|
||||
if (flushing) {
|
||||
mp3_reset();
|
||||
i2s_stop();
|
||||
playing = false;
|
||||
flushing = false;
|
||||
multicore_fifo_push_blocking(0);
|
||||
}
|
||||
}
|
||||
|
||||
__wfe();
|
||||
}
|
||||
|
||||
mp3_deinit();
|
||||
out_i2s:
|
||||
i2s_deinit();
|
||||
out:
|
||||
multicore_fifo_push_blocking(ret);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
||||
// Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
* communication between the cores.
|
||||
*/
|
||||
|
||||
#define AUDIO_BUFFER_SIZE 2048
|
||||
#define MP3_BUFFER_ALIGN (sizeof(uintptr_t))
|
||||
#define MP3_BUFFER_SIZE 4096
|
||||
#define MP3_BUFFER_PREAREA 1048
|
||||
|
||||
_Static_assert(MP3_BUFFER_PREAREA % MP3_BUFFER_ALIGN == 0,
|
||||
"Prearea must be a multiple of machine word size for alignment");
|
||||
|
||||
// Context shared between the micropython runtime on core0 and the audio task on
|
||||
// core1 All access must hold "lock" unless otherwise noted
|
||||
@@ -27,59 +32,59 @@ struct audiocore_shared_context {
|
||||
// Set by module.c before core1 is launched and then never changed, can be read without lock
|
||||
int out_pin, sideset_base, samplerate;
|
||||
|
||||
// Must hold lock
|
||||
uint32_t audio_buffer[AUDIO_BUFFER_SIZE];
|
||||
int audio_buffer_write, audio_buffer_read;
|
||||
// Must hold lock. The indices 0..MP3_BUFFER_PREAREA-1 may only be read and written on core1 (no
|
||||
// lock needed) The buffer is aligned to, and MP3_BUFFER_PREAREA is a multiple of, the machine
|
||||
// word size to prevent these accesses from crossing word boundaries.
|
||||
char mp3_buffer[MP3_BUFFER_PREAREA + MP3_BUFFER_SIZE] __attribute__((aligned(MP3_BUFFER_ALIGN)));
|
||||
int mp3_buffer_write, mp3_buffer_read;
|
||||
int underruns;
|
||||
};
|
||||
|
||||
extern struct audiocore_shared_context shared_context;
|
||||
|
||||
static inline unsigned audiocore_get_audio_buffer_space(void)
|
||||
/* Must hold audiocore_shared_context.lock */
|
||||
static inline unsigned audiocore_get_buffer_space(void)
|
||||
{
|
||||
if (shared_context.audio_buffer_write >= shared_context.audio_buffer_read)
|
||||
return AUDIO_BUFFER_SIZE - 1 - (shared_context.audio_buffer_write - shared_context.audio_buffer_read);
|
||||
if (shared_context.mp3_buffer_write >= shared_context.mp3_buffer_read)
|
||||
return MP3_BUFFER_SIZE - 1 - (shared_context.mp3_buffer_write - shared_context.mp3_buffer_read);
|
||||
else
|
||||
return shared_context.audio_buffer_read - shared_context.audio_buffer_write - 1;
|
||||
return shared_context.mp3_buffer_read - shared_context.mp3_buffer_write - 1;
|
||||
}
|
||||
|
||||
static inline unsigned audiocore_get_audio_buffer_avail(void)
|
||||
/* Must hold audiocore_shared_context.lock */
|
||||
static inline unsigned audiocore_get_buffer_avail(void)
|
||||
{
|
||||
if (shared_context.audio_buffer_write >= shared_context.audio_buffer_read)
|
||||
return shared_context.audio_buffer_write - shared_context.audio_buffer_read;
|
||||
if (shared_context.mp3_buffer_write >= shared_context.mp3_buffer_read)
|
||||
return shared_context.mp3_buffer_write - shared_context.mp3_buffer_read;
|
||||
else
|
||||
return AUDIO_BUFFER_SIZE - (shared_context.audio_buffer_read - shared_context.audio_buffer_write);
|
||||
return MP3_BUFFER_SIZE - (shared_context.mp3_buffer_read - shared_context.mp3_buffer_write);
|
||||
}
|
||||
|
||||
static inline void audiocore_audio_buffer_put(const uint32_t *restrict src, const size_t len)
|
||||
/* Must hold audiocore_shared_context.lock */
|
||||
static inline void audiocore_buffer_put(const char *restrict src, const size_t len)
|
||||
{
|
||||
const unsigned end_samples = AUDIO_BUFFER_SIZE - shared_context.audio_buffer_write;
|
||||
memcpy(shared_context.audio_buffer + shared_context.audio_buffer_write, src,
|
||||
4 * ((end_samples >= len) ? len : end_samples));
|
||||
if (end_samples < len) {
|
||||
memcpy(shared_context.audio_buffer, src + end_samples, 4 * (len - end_samples));
|
||||
shared_context.audio_buffer_write = len - end_samples;
|
||||
const unsigned end_bytes = MP3_BUFFER_SIZE - shared_context.mp3_buffer_write;
|
||||
memcpy(shared_context.mp3_buffer + MP3_BUFFER_PREAREA + shared_context.mp3_buffer_write, src,
|
||||
((end_bytes >= len) ? len : end_bytes));
|
||||
if (end_bytes < len) {
|
||||
memcpy(shared_context.mp3_buffer + MP3_BUFFER_PREAREA, src + end_bytes, len - end_bytes);
|
||||
shared_context.mp3_buffer_write = len - end_bytes;
|
||||
} else {
|
||||
shared_context.audio_buffer_write += len;
|
||||
shared_context.mp3_buffer_write += len;
|
||||
}
|
||||
shared_context.audio_buffer_write %= AUDIO_BUFFER_SIZE;
|
||||
shared_context.mp3_buffer_write %= MP3_BUFFER_SIZE;
|
||||
}
|
||||
|
||||
static inline void audiocore_audio_buffer_get(uint32_t *restrict dst, const size_t len)
|
||||
{
|
||||
const unsigned end_samples = AUDIO_BUFFER_SIZE - shared_context.audio_buffer_read;
|
||||
memcpy(dst, shared_context.audio_buffer + shared_context.audio_buffer_read,
|
||||
4 * ((end_samples >= len) ? len : end_samples));
|
||||
if (end_samples < len) {
|
||||
memcpy(dst + end_samples, shared_context.audio_buffer, 4 * (len - end_samples));
|
||||
shared_context.audio_buffer_read = len - end_samples;
|
||||
} else {
|
||||
shared_context.audio_buffer_read += len;
|
||||
}
|
||||
shared_context.audio_buffer_read %= AUDIO_BUFFER_SIZE;
|
||||
}
|
||||
void __time_critical_func(volume_adjust)(int16_t *buf, size_t samples, uint16_t scalef);
|
||||
|
||||
void core1_main(void);
|
||||
|
||||
// SHUTDOWN - no arguments - return 0
|
||||
#define AUDIOCORE_CMD_SHUTDOWN 0xdeadc0de
|
||||
|
||||
// FLUSH - signals end of file and stop decoding when buffer empty - no arguments - return 0 when decoding is finished
|
||||
#define AUDIOCORE_CMD_FLUSH 0xdeadc0dd
|
||||
|
||||
#define AUDIOCORE_MAX_VOLUME 0x8000u
|
||||
// SET VOLUME - one argument: uint32_t range 0 to AUDIOCORE_MAX_VOLUME - return 0 if volume changed, 1 on error
|
||||
#define AUDIOCORE_CMD_SET_VOLUME 0xdead0001
|
||||
|
||||
@@ -1,43 +1,53 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
||||
// Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
#include "i2s.h"
|
||||
#include "i2s_max98357.pio.h"
|
||||
|
||||
#include "audiocore.h"
|
||||
#include "i2s_max98357.pio.h"
|
||||
|
||||
#include <hardware/dma.h>
|
||||
#include <hardware/sync.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define audiocore_pio pio1
|
||||
|
||||
#define I2S_DMA_BUF_SIZE 256
|
||||
// Must be at least 2
|
||||
#define AUDIO_BUFS 2
|
||||
|
||||
struct i2s_context {
|
||||
unsigned pio_program_offset;
|
||||
int pio_sm;
|
||||
int dma_ch;
|
||||
dma_channel_config dma_config;
|
||||
uint32_t dma_buf[I2S_DMA_BUF_SIZE];
|
||||
uint32_t dma_buf[AUDIO_BUFS][I2S_DMA_BUF_SIZE];
|
||||
int cur_playing;
|
||||
int out_pin, sideset_base;
|
||||
bool has_data[AUDIO_BUFS];
|
||||
bool playback_active;
|
||||
};
|
||||
|
||||
static struct i2s_context i2s_context;
|
||||
|
||||
static void dma_isr(void)
|
||||
static void __time_critical_func(dma_isr)(void)
|
||||
{
|
||||
if (!dma_channel_get_irq1_status(i2s_context.dma_ch))
|
||||
if (get_core_num() != 1 || !dma_channel_get_irq1_status(i2s_context.dma_ch))
|
||||
return;
|
||||
dma_channel_acknowledge_irq1(i2s_context.dma_ch);
|
||||
const uint32_t flags = spin_lock_blocking(shared_context.lock);
|
||||
if (audiocore_get_audio_buffer_avail() >= I2S_DMA_BUF_SIZE) {
|
||||
audiocore_audio_buffer_get(i2s_context.dma_buf, I2S_DMA_BUF_SIZE);
|
||||
spin_unlock(shared_context.lock, flags);
|
||||
const int next_buf = (i2s_context.cur_playing + 1) % AUDIO_BUFS;
|
||||
if (i2s_context.playback_active && i2s_context.has_data[next_buf]) {
|
||||
i2s_context.cur_playing = next_buf;
|
||||
i2s_context.has_data[next_buf] = false;
|
||||
} else {
|
||||
++shared_context.underruns;
|
||||
spin_unlock(shared_context.lock, flags);
|
||||
memset(i2s_context.dma_buf, 0, sizeof(uint32_t) * I2S_DMA_BUF_SIZE);
|
||||
memset(i2s_context.dma_buf[i2s_context.cur_playing], 0, sizeof(uint32_t) * I2S_DMA_BUF_SIZE);
|
||||
if (i2s_context.playback_active) {
|
||||
++shared_context.underruns;
|
||||
}
|
||||
}
|
||||
dma_channel_transfer_from_buffer_now(i2s_context.dma_ch, i2s_context.dma_buf, I2S_DMA_BUF_SIZE);
|
||||
dma_channel_transfer_from_buffer_now(i2s_context.dma_ch, i2s_context.dma_buf[i2s_context.cur_playing],
|
||||
I2S_DMA_BUF_SIZE);
|
||||
}
|
||||
|
||||
static void setup_dma_config(void)
|
||||
@@ -46,41 +56,108 @@ static void setup_dma_config(void)
|
||||
channel_config_set_dreq(&i2s_context.dma_config, pio_get_dreq(pio1, i2s_context.pio_sm, true));
|
||||
}
|
||||
|
||||
bool i2s_init(int out_pin, int sideset_base, int samplerate)
|
||||
uint32_t *__time_critical_func(i2s_next_buf)(void)
|
||||
{
|
||||
memset(i2s_context.dma_buf, 0, sizeof(i2s_context.dma_buf[0]) * I2S_DMA_BUF_SIZE);
|
||||
if (!pio_can_add_program(audiocore_pio, &i2s_max98357_program))
|
||||
uint32_t *ret = NULL;
|
||||
const long flags = save_and_disable_interrupts();
|
||||
for (int i = 1; i < AUDIO_BUFS; ++i) {
|
||||
const int next_buf = (i2s_context.cur_playing + i) % AUDIO_BUFS;
|
||||
if (!i2s_context.has_data[next_buf]) {
|
||||
ret = i2s_context.dma_buf[next_buf];
|
||||
break;
|
||||
}
|
||||
}
|
||||
restore_interrupts(flags);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void __time_critical_func(i2s_commit_buf)(uint32_t *buf)
|
||||
{
|
||||
const long flags = save_and_disable_interrupts();
|
||||
for (int i = 1; i < AUDIO_BUFS; ++i) {
|
||||
const int next_buf = (i2s_context.cur_playing + i) % AUDIO_BUFS;
|
||||
if (i2s_context.dma_buf[next_buf] == buf) {
|
||||
i2s_context.has_data[next_buf] = true;
|
||||
if (i == AUDIO_BUFS - 1) {
|
||||
i2s_context.playback_active = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
restore_interrupts(flags);
|
||||
}
|
||||
|
||||
void i2s_stop(void)
|
||||
{
|
||||
if (!i2s_context.playback_active)
|
||||
return;
|
||||
bool have_data = false;
|
||||
do {
|
||||
const long flags = save_and_disable_interrupts();
|
||||
const int next_buf = (i2s_context.cur_playing + 1) % AUDIO_BUFS;
|
||||
have_data = i2s_context.has_data[next_buf];
|
||||
restore_interrupts(flags);
|
||||
if (have_data)
|
||||
__wfi();
|
||||
} while (have_data);
|
||||
const long flags = save_and_disable_interrupts();
|
||||
i2s_context.playback_active = false;
|
||||
restore_interrupts(flags);
|
||||
// Workaround rp2040 E13
|
||||
dma_channel_set_irq1_enabled(i2s_context.dma_ch, false);
|
||||
dma_channel_abort(i2s_context.dma_ch);
|
||||
dma_channel_acknowledge_irq1(i2s_context.dma_ch);
|
||||
dma_channel_set_irq1_enabled(i2s_context.dma_ch, true);
|
||||
pio_sm_set_enabled(audiocore_pio, i2s_context.pio_sm, false);
|
||||
pio_sm_clear_fifos(audiocore_pio, i2s_context.pio_sm);
|
||||
}
|
||||
|
||||
bool i2s_init(int out_pin, int sideset_base)
|
||||
{
|
||||
memset(i2s_context.dma_buf, 0, sizeof(i2s_context.dma_buf[0][0]) * AUDIO_BUFS * I2S_DMA_BUF_SIZE);
|
||||
if (!pio_can_add_program(audiocore_pio, (const pio_program_t *)&i2s_max98357_program))
|
||||
return false;
|
||||
i2s_context.pio_sm = pio_claim_unused_sm(audiocore_pio, false);
|
||||
i2s_context.out_pin = out_pin;
|
||||
i2s_context.sideset_base = sideset_base;
|
||||
if (i2s_context.pio_sm == -1)
|
||||
return false;
|
||||
i2s_context.pio_program_offset = pio_add_program(audiocore_pio, &i2s_max98357_program);
|
||||
i2s_max98357_program_init(audiocore_pio, i2s_context.pio_sm, i2s_context.pio_program_offset, out_pin, sideset_base,
|
||||
samplerate);
|
||||
i2s_context.pio_program_offset = pio_add_program(audiocore_pio, (const pio_program_t *)&i2s_max98357_program);
|
||||
|
||||
i2s_context.dma_ch = dma_claim_unused_channel(false);
|
||||
if (i2s_context.dma_ch == -1)
|
||||
goto out_dma_claim;
|
||||
i2s_context.playback_active = false;
|
||||
setup_dma_config();
|
||||
irq_set_exclusive_handler(DMA_IRQ_1, &dma_isr);
|
||||
irq_add_shared_handler(DMA_IRQ_1, &dma_isr, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY);
|
||||
dma_channel_set_irq1_enabled(i2s_context.dma_ch, true);
|
||||
irq_set_enabled(DMA_IRQ_1, true);
|
||||
dma_channel_configure(i2s_context.dma_ch, &i2s_context.dma_config, &audiocore_pio->txf[i2s_context.pio_sm],
|
||||
i2s_context.dma_buf, I2S_DMA_BUF_SIZE, true);
|
||||
pio_sm_set_enabled(audiocore_pio, i2s_context.pio_sm, true);
|
||||
|
||||
return true;
|
||||
|
||||
out_dma_claim:
|
||||
pio_remove_program(audiocore_pio, &i2s_max98357_program, i2s_context.pio_program_offset);
|
||||
pio_remove_program(audiocore_pio, (const pio_program_t *)&i2s_max98357_program, i2s_context.pio_program_offset);
|
||||
pio_sm_unclaim(audiocore_pio, i2s_context.pio_sm);
|
||||
return false;
|
||||
}
|
||||
|
||||
void i2s_play(int samplerate)
|
||||
{
|
||||
i2s_context.playback_active = false;
|
||||
i2s_context.cur_playing = 0;
|
||||
memset(i2s_context.dma_buf, 0, sizeof(i2s_context.dma_buf[0][0]) * AUDIO_BUFS * I2S_DMA_BUF_SIZE);
|
||||
i2s_max98357_program_init(audiocore_pio, i2s_context.pio_sm, i2s_context.pio_program_offset, i2s_context.out_pin,
|
||||
i2s_context.sideset_base, samplerate);
|
||||
dma_channel_configure(i2s_context.dma_ch, &i2s_context.dma_config, &audiocore_pio->txf[i2s_context.pio_sm],
|
||||
i2s_context.dma_buf, I2S_DMA_BUF_SIZE, true);
|
||||
pio_sm_set_enabled(audiocore_pio, i2s_context.pio_sm, true);
|
||||
}
|
||||
|
||||
void i2s_deinit(void)
|
||||
{
|
||||
pio_sm_set_enabled(audiocore_pio, i2s_context.pio_sm, false);
|
||||
dma_channel_set_irq1_enabled(i2s_context.dma_ch, false);
|
||||
dma_channel_abort(i2s_context.dma_ch);
|
||||
irq_remove_handler(DMA_IRQ_1, &dma_isr);
|
||||
dma_channel_unclaim(i2s_context.dma_ch);
|
||||
pio_remove_program(audiocore_pio, &i2s_max98357_program, i2s_context.pio_program_offset);
|
||||
pio_sm_unclaim(audiocore_pio, i2s_context.pio_sm);
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
||||
// Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
bool i2s_init(int out_pin, int sideset_base, int samplerate);
|
||||
#define I2S_DMA_BUF_SIZE (1152)
|
||||
|
||||
bool i2s_init(int out_pin, int sideset_base);
|
||||
void i2s_deinit(void);
|
||||
|
||||
void i2s_play(int samplerate);
|
||||
void i2s_stop(void);
|
||||
|
||||
uint32_t *i2s_next_buf(void);
|
||||
void i2s_commit_buf(uint32_t *buf);
|
||||
|
||||
@@ -8,8 +8,9 @@ pico_generate_pio_header(usermod_audiocore ${CMAKE_CURRENT_LIST_DIR}/i2s_max9835
|
||||
|
||||
target_sources(usermod_audiocore INTERFACE
|
||||
${CMAKE_CURRENT_LIST_DIR}/audiocore.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/module.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/i2s.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/module.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/mp3.c
|
||||
${CMAKE_CURRENT_BINARY_DIR}/i2s_max98357.pio.h
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
||||
// Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
#include "audiocore.h"
|
||||
|
||||
@@ -16,52 +16,54 @@
|
||||
struct audiocore_shared_context shared_context = {.lock = NULL};
|
||||
static bool initialized = false;
|
||||
|
||||
struct audiocore_Context_obj {
|
||||
struct audiocore_obj {
|
||||
mp_obj_base_t base;
|
||||
};
|
||||
const mp_obj_type_t audiocore_type;
|
||||
|
||||
/*
|
||||
* audiocore.Context.deinit(self)
|
||||
*
|
||||
* Deinitializes the audiocore.Context and shuts down processing on core1
|
||||
*/
|
||||
static mp_obj_t audiocore_Context_deinit(mp_obj_t self_in)
|
||||
static mp_obj_t audiocore_deinit(mp_obj_t self_in)
|
||||
{
|
||||
struct audiocore_Context_obj *self = MP_OBJ_TO_PTR(self_in);
|
||||
struct audiocore_obj *self = MP_OBJ_TO_PTR(self_in);
|
||||
multicore_fifo_push_blocking(AUDIOCORE_CMD_SHUTDOWN);
|
||||
multicore_fifo_pop_blocking();
|
||||
(void)self;
|
||||
initialized = false;
|
||||
return mp_const_none;
|
||||
}
|
||||
static MP_DEFINE_CONST_FUN_OBJ_1(audiocore_Context_deinit_obj, audiocore_Context_deinit);
|
||||
static MP_DEFINE_CONST_FUN_OBJ_1(audiocore_deinit_obj, audiocore_deinit);
|
||||
|
||||
/*
|
||||
* (copied, buf_space, undderuns) = audiocore.Context.put(self, buffer)
|
||||
* (copied, buf_space, undderuns) = audiocore.put(self, buffer)
|
||||
*
|
||||
* Copies as many integers as possible from the buffer to the audiocore ring buffer for playback.
|
||||
* 'buffer' must be any object supporting the buffer protocol with data in unsigned int (array typecode 'I')
|
||||
* format. The actual number of elements copied is returned in 'copied', the remaining free ring buffer space
|
||||
* is in 'buf_space', and the total number of buffer underruns since initialization of the audiocore Context
|
||||
* Copies as many bytes as possible from the buffer to the audiocore ring buffer for playback.
|
||||
* 'buffer' must be any object supporting the buffer protocol with data in byte (array typecode 'b' or 'B')
|
||||
* format. The actual number of elements copied is returned in 'copied', the remaining free buffer space
|
||||
* is in 'buf_space', and the total number of audio underruns since initialization of the audiocore Context
|
||||
* is in 'underruns'.
|
||||
*/
|
||||
static mp_obj_t audiocore_Context_put(mp_obj_t self_in, mp_obj_t buffer)
|
||||
static mp_obj_t audiocore_put(mp_obj_t self_in, mp_obj_t buffer)
|
||||
{
|
||||
struct audiocore_Context_obj *self = MP_OBJ_TO_PTR(self_in);
|
||||
struct audiocore_obj *self = MP_OBJ_TO_PTR(self_in);
|
||||
(void)self;
|
||||
mp_buffer_info_t bufinfo;
|
||||
if (!mp_get_buffer(buffer, &bufinfo, MP_BUFFER_READ))
|
||||
mp_raise_ValueError("not a read buffer");
|
||||
if (bufinfo.typecode != 'I')
|
||||
if (bufinfo.typecode != 'b' && bufinfo.typecode != 'B')
|
||||
mp_raise_ValueError("unsupported buffer type");
|
||||
unsigned to_copy = bufinfo.len / 4;
|
||||
unsigned to_copy = bufinfo.len;
|
||||
|
||||
const uint32_t flags = spin_lock_blocking(shared_context.lock);
|
||||
const unsigned buf_space = audiocore_get_audio_buffer_space();
|
||||
const unsigned buf_space = audiocore_get_buffer_space();
|
||||
if (to_copy > buf_space)
|
||||
to_copy = buf_space;
|
||||
if (to_copy > 0) {
|
||||
audiocore_audio_buffer_put(bufinfo.buf, to_copy);
|
||||
audiocore_buffer_put(bufinfo.buf, to_copy);
|
||||
__sev();
|
||||
}
|
||||
const unsigned underruns = shared_context.underruns;
|
||||
spin_unlock(shared_context.lock, flags);
|
||||
@@ -73,33 +75,68 @@ static mp_obj_t audiocore_Context_put(mp_obj_t self_in, mp_obj_t buffer)
|
||||
};
|
||||
return mp_obj_new_tuple(3, items);
|
||||
}
|
||||
static MP_DEFINE_CONST_FUN_OBJ_2(audiocore_Context_put_obj, audiocore_Context_put);
|
||||
|
||||
static uint32_t __scratch_y("core1_stack") core1_stack[1024];
|
||||
static const mp_rom_map_elem_t audiocore_Context_locals_dict_table[] = {
|
||||
{MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&audiocore_Context_deinit_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiocore_Context_deinit_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_put), MP_ROM_PTR(&audiocore_Context_put_obj)},
|
||||
};
|
||||
static MP_DEFINE_CONST_DICT(audiocore_Context_locals_dict, audiocore_Context_locals_dict_table);
|
||||
const mp_obj_type_t audiocore_Context_type;
|
||||
|
||||
MP_DEFINE_CONST_OBJ_TYPE(audiocore_Context_type, MP_QSTR_Context, MP_TYPE_FLAG_NONE, locals_dict,
|
||||
&audiocore_Context_locals_dict);
|
||||
static MP_DEFINE_CONST_FUN_OBJ_2(audiocore_put_obj, audiocore_put);
|
||||
|
||||
/*
|
||||
* audiocore.init(pin, sideset, samplerate)
|
||||
* Returns: audiocore.Context
|
||||
* audiocore.flush(self)
|
||||
*
|
||||
* Initialize the audiocore module, starting the audio processing on core1 and initializing the I2S driver.
|
||||
* Tells the audiocore to stop playback as soon as all MP3 frames still in the buffer have been decoded.
|
||||
* This function blocks until playback has ended.
|
||||
*/
|
||||
static mp_obj_t audiocore_flush(mp_obj_t self_in)
|
||||
{
|
||||
struct audiocore_obj *self = MP_OBJ_TO_PTR(self_in);
|
||||
(void)self;
|
||||
multicore_fifo_push_blocking(AUDIOCORE_CMD_FLUSH);
|
||||
__sev();
|
||||
multicore_fifo_pop_blocking();
|
||||
return mp_const_none;
|
||||
}
|
||||
static MP_DEFINE_CONST_FUN_OBJ_1(audiocore_flush_obj, audiocore_flush);
|
||||
|
||||
/*
|
||||
* audiocore.set_volume(self, volume)
|
||||
*
|
||||
* Set a volume between 0 (silence) and 255 (PCM values are output as they are decoded from the mp3 stream). Bewteen
|
||||
* 0 and 255 the volume is scaled linearly.
|
||||
* Raises a ValueError of the volume is not in the range [0, 255].
|
||||
* Raises an OSError if the audiocore rejects the volume setting.
|
||||
*/
|
||||
static mp_obj_t audiocore_set_volume(mp_obj_t self_in, mp_obj_t volume_obj)
|
||||
{
|
||||
struct audiocore_obj *self = MP_OBJ_TO_PTR(self_in);
|
||||
(void)self;
|
||||
const int volume = mp_obj_get_int(volume_obj);
|
||||
if (volume < 0 || volume > 255)
|
||||
mp_raise_ValueError("volume out of range");
|
||||
multicore_fifo_push_blocking(AUDIOCORE_CMD_SET_VOLUME);
|
||||
multicore_fifo_push_blocking(AUDIOCORE_MAX_VOLUME * volume / 255);
|
||||
__sev();
|
||||
const uint32_t ret = multicore_fifo_pop_blocking();
|
||||
if (ret != 0)
|
||||
mp_raise_OSError(MP_EINVAL);
|
||||
return mp_const_none;
|
||||
}
|
||||
static MP_DEFINE_CONST_FUN_OBJ_2(audiocore_set_volume_obj, audiocore_set_volume);
|
||||
|
||||
static uint32_t __scratch_y("core1_stack") core1_stack[1024];
|
||||
|
||||
/*
|
||||
* Audiocore(pin, sideset)
|
||||
*
|
||||
* Initialize the audiocore module, starting the audio processing on core1.
|
||||
* The pin parameter specifies the I2S data pin, the sideset parameter specifies the I2S LRCLK pin, the
|
||||
* BCLK must be on pin sideset+1.
|
||||
* Samplerate should be 44100 or 48000.
|
||||
* Only a single audiocore.Context may exist at a time.
|
||||
* Return a audiocore.Context object on success, raises a OSError or ValueError on failure.
|
||||
* Only a single Audiocore may exist at a time.
|
||||
* Constructs an Audiocore object on success, raises a OSError or ValueError on failure.
|
||||
*/
|
||||
static mp_obj_t audiocore_init(mp_obj_t pin_obj, mp_obj_t sideset_obj, mp_obj_t samplerate_obj)
|
||||
static void audiocore_init(struct audiocore_obj *obj, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args)
|
||||
{
|
||||
enum { ARG_pin, ARG_sideset };
|
||||
static const mp_arg_t allowed_args[] = {
|
||||
{MP_QSTR_pin, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
||||
{MP_QSTR_sideset, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
||||
};
|
||||
if (initialized)
|
||||
mp_raise_OSError(MP_EBUSY);
|
||||
if (!shared_context.lock) {
|
||||
@@ -109,21 +146,15 @@ static mp_obj_t audiocore_init(mp_obj_t pin_obj, mp_obj_t sideset_obj, mp_obj_t
|
||||
mp_raise_OSError(MP_ENOMEM);
|
||||
shared_context.lock = spin_lock_init(lock);
|
||||
}
|
||||
shared_context.audio_buffer_write = shared_context.audio_buffer_read = shared_context.underruns = 0;
|
||||
memset(shared_context.audio_buffer, 0, AUDIO_BUFFER_SIZE * 4);
|
||||
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
|
||||
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
|
||||
const mp_hal_pin_obj_t pin = mp_hal_get_pin_obj(args[ARG_pin].u_obj);
|
||||
const mp_hal_pin_obj_t sideset_pin = mp_hal_get_pin_obj(args[ARG_sideset].u_obj);
|
||||
shared_context.mp3_buffer_write = shared_context.mp3_buffer_read = shared_context.underruns = 0;
|
||||
memset(shared_context.mp3_buffer, 0, MP3_BUFFER_PREAREA + MP3_BUFFER_SIZE);
|
||||
multicore_reset_core1();
|
||||
struct audiocore_Context_obj *context = m_malloc_with_finaliser(sizeof(struct audiocore_Context_obj));
|
||||
context->base.type = &audiocore_Context_type;
|
||||
mp_hal_pin_obj_t pin = pin_obj == MP_OBJ_NULL ? -1 : mp_hal_get_pin_obj(pin_obj);
|
||||
if (pin == -1)
|
||||
mp_raise_ValueError("Invalid out pin");
|
||||
mp_hal_pin_obj_t sideset_pin = sideset_obj == MP_OBJ_NULL ? -1 : mp_hal_get_pin_obj(sideset_obj);
|
||||
if (sideset_pin == -1)
|
||||
mp_raise_ValueError("Invalid sideset base pin");
|
||||
int samplerate = mp_obj_get_int(samplerate_obj);
|
||||
shared_context.out_pin = pin;
|
||||
shared_context.sideset_base = sideset_pin;
|
||||
shared_context.samplerate = samplerate;
|
||||
initialized = true;
|
||||
multicore_launch_core1_with_stack(&core1_main, core1_stack, sizeof(core1_stack));
|
||||
uint32_t result = multicore_fifo_pop_blocking();
|
||||
@@ -132,15 +163,32 @@ static mp_obj_t audiocore_init(mp_obj_t pin_obj, mp_obj_t sideset_obj, mp_obj_t
|
||||
initialized = false;
|
||||
mp_raise_OSError(result);
|
||||
}
|
||||
|
||||
return MP_OBJ_FROM_PTR(context);
|
||||
}
|
||||
static MP_DEFINE_CONST_FUN_OBJ_3(audiocore_init_obj, audiocore_init);
|
||||
|
||||
static mp_obj_t audiocore_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args)
|
||||
{
|
||||
struct audiocore_obj *audiocore = mp_obj_malloc(struct audiocore_obj, &audiocore_type);
|
||||
mp_map_t kw_args;
|
||||
mp_map_init_fixed_table(&kw_args, n_kw, args + n_args);
|
||||
audiocore_init(audiocore, n_args, args, &kw_args);
|
||||
return MP_OBJ_FROM_PTR(audiocore);
|
||||
}
|
||||
|
||||
static const mp_rom_map_elem_t audiocore_locals_dict_table[] = {
|
||||
{MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&audiocore_deinit_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiocore_deinit_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_put), MP_ROM_PTR(&audiocore_put_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_flush), MP_ROM_PTR(&audiocore_flush_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_set_volume), MP_ROM_PTR(&audiocore_set_volume_obj)},
|
||||
};
|
||||
static MP_DEFINE_CONST_DICT(audiocore_locals_dict, audiocore_locals_dict_table);
|
||||
|
||||
MP_DEFINE_CONST_OBJ_TYPE(audiocore_type, MP_QSTR_Audiocore, MP_TYPE_FLAG_NONE, locals_dict, &audiocore_locals_dict,
|
||||
make_new, &audiocore_make_new);
|
||||
|
||||
static const mp_rom_map_elem_t audiocore_module_globals_table[] = {
|
||||
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiocore)},
|
||||
{MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&audiocore_init_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_Context), MP_ROM_PTR(&audiocore_Context_type)},
|
||||
{MP_ROM_QSTR(MP_QSTR_Audiocore), MP_ROM_PTR(&audiocore_type)},
|
||||
};
|
||||
static MP_DEFINE_CONST_DICT(audiocore_module_globals, audiocore_module_globals_table);
|
||||
|
||||
|
||||
146
software/src/audiocore/mp3.c
Normal file
146
software/src/audiocore/mp3.c
Normal file
@@ -0,0 +1,146 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
#include "mp3.h"
|
||||
#include "audiocore.h"
|
||||
#include "mp3dec.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static HMP3Decoder mp3dec;
|
||||
static bool synced = false;
|
||||
static size_t prearea_fill = 0;
|
||||
#ifdef MP3_DEBUG
|
||||
static first = true;
|
||||
#endif
|
||||
|
||||
bool mp3_init(void)
|
||||
{
|
||||
mp3dec = MP3InitDecoder();
|
||||
return (mp3dec != 0);
|
||||
}
|
||||
|
||||
void mp3_deinit(void)
|
||||
{
|
||||
if (mp3dec) {
|
||||
MP3FreeDecoder(mp3dec);
|
||||
mp3dec = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static size_t __time_critical_func(mp3_get_continuous_data)(unsigned char **readptr)
|
||||
{
|
||||
size_t bytes_left;
|
||||
const uint32_t flags = spin_lock_blocking(shared_context.lock);
|
||||
/* Check if remaining bytes at end of buffer should be moved to prearea */
|
||||
const unsigned end_bytes = MP3_BUFFER_SIZE - shared_context.mp3_buffer_read;
|
||||
if (shared_context.mp3_buffer_write < shared_context.mp3_buffer_read && end_bytes <= MP3_BUFFER_PREAREA) {
|
||||
assert(prearea_fill == 0);
|
||||
memcpy(shared_context.mp3_buffer + MP3_BUFFER_PREAREA - end_bytes,
|
||||
shared_context.mp3_buffer + MP3_BUFFER_PREAREA + shared_context.mp3_buffer_read, end_bytes);
|
||||
prearea_fill = end_bytes;
|
||||
shared_context.mp3_buffer_read = 0;
|
||||
}
|
||||
/* Get start and length of valid continuous data */
|
||||
if (shared_context.mp3_buffer_read == 0) {
|
||||
*readptr = (unsigned char *)(shared_context.mp3_buffer + MP3_BUFFER_PREAREA - prearea_fill);
|
||||
bytes_left = prearea_fill + shared_context.mp3_buffer_write;
|
||||
} else if (shared_context.mp3_buffer_write < shared_context.mp3_buffer_read) {
|
||||
*readptr = (unsigned char *)(shared_context.mp3_buffer + MP3_BUFFER_PREAREA + shared_context.mp3_buffer_read);
|
||||
bytes_left = end_bytes;
|
||||
} else {
|
||||
*readptr = (unsigned char *)(shared_context.mp3_buffer + MP3_BUFFER_PREAREA + shared_context.mp3_buffer_read);
|
||||
bytes_left = shared_context.mp3_buffer_write - shared_context.mp3_buffer_read;
|
||||
}
|
||||
spin_unlock(shared_context.lock, flags);
|
||||
return bytes_left;
|
||||
}
|
||||
|
||||
static void __time_critical_func(mp3_consume_data)(size_t bytes_used)
|
||||
{
|
||||
if (prearea_fill >= bytes_used) {
|
||||
prearea_fill -= bytes_used;
|
||||
return;
|
||||
} else {
|
||||
bytes_used -= prearea_fill;
|
||||
prearea_fill = 0;
|
||||
}
|
||||
const uint32_t flags = spin_lock_blocking(shared_context.lock);
|
||||
shared_context.mp3_buffer_read += bytes_used;
|
||||
shared_context.mp3_buffer_read %= MP3_BUFFER_SIZE;
|
||||
spin_unlock(shared_context.lock, flags);
|
||||
}
|
||||
|
||||
bool __time_critical_func(mp3_decode)(uint32_t pcm_buf[static 1152], unsigned *samplerate)
|
||||
{
|
||||
unsigned char *readptr;
|
||||
size_t bytes_avail = mp3_get_continuous_data(&readptr);
|
||||
if (!bytes_avail)
|
||||
return false;
|
||||
if (!synced) {
|
||||
const int ofs = MP3FindSyncWord((unsigned char *)readptr, bytes_avail);
|
||||
if (ofs == -1) {
|
||||
#ifdef MP3_DEBUG
|
||||
printf("MP3 sync word not found\n");
|
||||
#endif
|
||||
mp3_consume_data(bytes_avail);
|
||||
return false;
|
||||
}
|
||||
|
||||
readptr += ofs;
|
||||
bytes_avail -= ofs;
|
||||
mp3_consume_data(ofs);
|
||||
#ifdef MP3_DEBUG
|
||||
printf("MP3 sync word found after %zu bytes\n", ofs);
|
||||
#endif
|
||||
synced = true;
|
||||
}
|
||||
if (!bytes_avail)
|
||||
return false;
|
||||
|
||||
// Decode one frame
|
||||
int int_bytes_avail = bytes_avail;
|
||||
const int status = MP3Decode(mp3dec, &readptr, &int_bytes_avail, (short *)pcm_buf, 0);
|
||||
if (status) {
|
||||
if (status == ERR_MP3_INDATA_UNDERFLOW) {
|
||||
return false;
|
||||
} else /*if (status== ERR_MP3_MAINDATA_UNDERFLOW)*/ {
|
||||
mp3_consume_data(1);
|
||||
synced = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
mp3_consume_data(bytes_avail - int_bytes_avail);
|
||||
MP3FrameInfo info;
|
||||
MP3GetLastFrameInfo(mp3dec, &info);
|
||||
#ifdef MP3_DEBUG
|
||||
if (first) {
|
||||
printf("bitrate %d, nChans %d, samprate %d, bitsPerSample %d, outputSamps %d, layer %d, version %d\n",
|
||||
info.bitrate, info.nChans, info.samprate, info.bitsPerSample, info.outputSamps, info.layer,
|
||||
info.version);
|
||||
first = false;
|
||||
}
|
||||
#endif
|
||||
*samplerate = info.samprate;
|
||||
|
||||
if (info.outputSamps != 2304) {
|
||||
#ifdef MP3_DEBUG
|
||||
printf("Unexpected number of output samples: %d\n", info.outputSamps);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void mp3_reset(void)
|
||||
{
|
||||
synced = false;
|
||||
prearea_fill = 0;
|
||||
#ifdef MP3_DEBUG
|
||||
first = true;
|
||||
#endif
|
||||
const uint32_t flags = spin_lock_blocking(shared_context.lock);
|
||||
shared_context.mp3_buffer_read = shared_context.mp3_buffer_write = 0;
|
||||
spin_unlock(shared_context.lock, flags);
|
||||
}
|
||||
14
software/src/audiocore/mp3.h
Normal file
14
software/src/audiocore/mp3.h
Normal file
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
bool mp3_init(void);
|
||||
void mp3_deinit(void);
|
||||
|
||||
bool mp3_decode(uint32_t pcm_buf[static 1152], unsigned *samplerate);
|
||||
|
||||
void mp3_reset(void);
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
||||
// Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
#include "audiocore.h"
|
||||
#include "i2s.h"
|
||||
@@ -15,7 +15,7 @@ static unsigned multicore_fifo_push_last;
|
||||
|
||||
static unsigned (*multicore_fifo_pop_blocking_cb)(void);
|
||||
|
||||
bool i2s_init(int out_pin, int sideset_base, int samplerate)
|
||||
bool i2s_init(int out_pin, int sideset_base)
|
||||
{
|
||||
TEST_ASSERT_FALSE(i2s_initialized);
|
||||
if (i2s_init_return)
|
||||
@@ -23,12 +23,28 @@ bool i2s_init(int out_pin, int sideset_base, int samplerate)
|
||||
return i2s_init_return;
|
||||
}
|
||||
|
||||
uint32_t *i2s_next_buf(void) { return NULL; }
|
||||
|
||||
void i2s_commit_buf(uint32_t *buf) {}
|
||||
|
||||
void i2s_play(int samplerate) {}
|
||||
|
||||
void i2s_stop(void) {}
|
||||
|
||||
void i2s_deinit(void)
|
||||
{
|
||||
TEST_ASSERT_TRUE(i2s_initialized);
|
||||
i2s_initialized = false;
|
||||
}
|
||||
|
||||
bool mp3_init(void) { return true; }
|
||||
|
||||
void mp3_deinit(void) {}
|
||||
|
||||
bool mp3_decode(uint32_t pcm_buf[static 1152], unsigned *samplerate) { return false; }
|
||||
|
||||
void mp3_reset(void) {}
|
||||
|
||||
void multicore_fifo_push_blocking(unsigned val) { multicore_fifo_push_last = val; }
|
||||
|
||||
unsigned multicore_fifo_pop_blocking(void)
|
||||
@@ -38,6 +54,8 @@ unsigned multicore_fifo_pop_blocking(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool multicore_fifo_rvalid(void) { return multicore_fifo_pop_blocking_cb; }
|
||||
|
||||
void test_audiocore_handles_i2sinit_failure(void)
|
||||
{
|
||||
i2s_init_return = false;
|
||||
@@ -65,78 +83,78 @@ void test_audiocore_init_deinit(void)
|
||||
void test_audiocore_buffer_space(void)
|
||||
{
|
||||
// empty ring buffer
|
||||
shared_context.audio_buffer_read = shared_context.audio_buffer_write = 0;
|
||||
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, audiocore_get_audio_buffer_space());
|
||||
shared_context.mp3_buffer_read = shared_context.mp3_buffer_write = 0;
|
||||
TEST_ASSERT_EQUAL(MP3_BUFFER_SIZE - 1, audiocore_get_buffer_space());
|
||||
|
||||
shared_context.audio_buffer_read = shared_context.audio_buffer_write = 23;
|
||||
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, audiocore_get_audio_buffer_space());
|
||||
shared_context.mp3_buffer_read = shared_context.mp3_buffer_write = 23;
|
||||
TEST_ASSERT_EQUAL(MP3_BUFFER_SIZE - 1, audiocore_get_buffer_space());
|
||||
|
||||
shared_context.audio_buffer_read = shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 1;
|
||||
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, audiocore_get_audio_buffer_space());
|
||||
shared_context.mp3_buffer_read = shared_context.mp3_buffer_write = MP3_BUFFER_SIZE - 1;
|
||||
TEST_ASSERT_EQUAL(MP3_BUFFER_SIZE - 1, audiocore_get_buffer_space());
|
||||
|
||||
// full ring buffer
|
||||
shared_context.audio_buffer_write = 0;
|
||||
shared_context.audio_buffer_read = 1;
|
||||
TEST_ASSERT_EQUAL(0, audiocore_get_audio_buffer_space());
|
||||
shared_context.mp3_buffer_write = 0;
|
||||
shared_context.mp3_buffer_read = 1;
|
||||
TEST_ASSERT_EQUAL(0, audiocore_get_buffer_space());
|
||||
|
||||
shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 1;
|
||||
shared_context.audio_buffer_read = 0;
|
||||
TEST_ASSERT_EQUAL(0, audiocore_get_audio_buffer_space());
|
||||
shared_context.mp3_buffer_write = MP3_BUFFER_SIZE - 1;
|
||||
shared_context.mp3_buffer_read = 0;
|
||||
TEST_ASSERT_EQUAL(0, audiocore_get_buffer_space());
|
||||
|
||||
// write > read
|
||||
shared_context.audio_buffer_write = 10;
|
||||
shared_context.audio_buffer_read = 0;
|
||||
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1 - 10, audiocore_get_audio_buffer_space());
|
||||
shared_context.mp3_buffer_write = 10;
|
||||
shared_context.mp3_buffer_read = 0;
|
||||
TEST_ASSERT_EQUAL(MP3_BUFFER_SIZE - 1 - 10, audiocore_get_buffer_space());
|
||||
|
||||
// write < read
|
||||
shared_context.audio_buffer_write = 0;
|
||||
shared_context.audio_buffer_read = 10;
|
||||
TEST_ASSERT_EQUAL(9, audiocore_get_audio_buffer_space());
|
||||
shared_context.mp3_buffer_write = 0;
|
||||
shared_context.mp3_buffer_read = 10;
|
||||
TEST_ASSERT_EQUAL(9, audiocore_get_buffer_space());
|
||||
}
|
||||
|
||||
void test_audiocore_buffer_avail(void)
|
||||
{
|
||||
// empty ring buffer
|
||||
shared_context.audio_buffer_read = shared_context.audio_buffer_write = 0;
|
||||
TEST_ASSERT_EQUAL(0, audiocore_get_audio_buffer_avail());
|
||||
shared_context.mp3_buffer_read = shared_context.mp3_buffer_write = 0;
|
||||
TEST_ASSERT_EQUAL(0, audiocore_get_buffer_avail());
|
||||
|
||||
shared_context.audio_buffer_read = shared_context.audio_buffer_write = 23;
|
||||
TEST_ASSERT_EQUAL(0, audiocore_get_audio_buffer_avail());
|
||||
shared_context.mp3_buffer_read = shared_context.mp3_buffer_write = 23;
|
||||
TEST_ASSERT_EQUAL(0, audiocore_get_buffer_avail());
|
||||
|
||||
shared_context.audio_buffer_read = shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 1;
|
||||
TEST_ASSERT_EQUAL(0, audiocore_get_audio_buffer_avail());
|
||||
shared_context.mp3_buffer_read = shared_context.mp3_buffer_write = MP3_BUFFER_SIZE - 1;
|
||||
TEST_ASSERT_EQUAL(0, audiocore_get_buffer_avail());
|
||||
|
||||
// full ring buffer
|
||||
shared_context.audio_buffer_write = 0;
|
||||
shared_context.audio_buffer_read = 1;
|
||||
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, audiocore_get_audio_buffer_avail());
|
||||
shared_context.mp3_buffer_write = 0;
|
||||
shared_context.mp3_buffer_read = 1;
|
||||
TEST_ASSERT_EQUAL(MP3_BUFFER_SIZE - 1, audiocore_get_buffer_avail());
|
||||
|
||||
shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 1;
|
||||
shared_context.audio_buffer_read = 0;
|
||||
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, audiocore_get_audio_buffer_avail());
|
||||
shared_context.mp3_buffer_write = MP3_BUFFER_SIZE - 1;
|
||||
shared_context.mp3_buffer_read = 0;
|
||||
TEST_ASSERT_EQUAL(MP3_BUFFER_SIZE - 1, audiocore_get_buffer_avail());
|
||||
|
||||
// write > read
|
||||
shared_context.audio_buffer_write = 10;
|
||||
shared_context.audio_buffer_read = 0;
|
||||
TEST_ASSERT_EQUAL(10, audiocore_get_audio_buffer_avail());
|
||||
shared_context.mp3_buffer_write = 10;
|
||||
shared_context.mp3_buffer_read = 0;
|
||||
TEST_ASSERT_EQUAL(10, audiocore_get_buffer_avail());
|
||||
|
||||
// write < read
|
||||
shared_context.audio_buffer_write = 0;
|
||||
shared_context.audio_buffer_read = 10;
|
||||
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 10, audiocore_get_audio_buffer_avail());
|
||||
shared_context.mp3_buffer_write = 0;
|
||||
shared_context.mp3_buffer_read = 10;
|
||||
TEST_ASSERT_EQUAL(MP3_BUFFER_SIZE - 10, audiocore_get_buffer_avail());
|
||||
}
|
||||
|
||||
static unsigned fill_buffer_helper(void)
|
||||
{
|
||||
uint32_t test_data[100];
|
||||
char test_data[100];
|
||||
uint32_t ctr = 0;
|
||||
|
||||
unsigned avail, filled = 0;
|
||||
while ((avail = audiocore_get_audio_buffer_space()) > 0) {
|
||||
while ((avail = audiocore_get_buffer_space()) > 0) {
|
||||
const unsigned todo = avail > 100 ? 100 : avail;
|
||||
for (unsigned i = 0; i < todo; ++i)
|
||||
test_data[i] = ctr++;
|
||||
audiocore_audio_buffer_put(test_data, todo);
|
||||
audiocore_buffer_put(test_data, todo);
|
||||
filled += todo;
|
||||
}
|
||||
return filled;
|
||||
@@ -144,49 +162,39 @@ static unsigned fill_buffer_helper(void)
|
||||
|
||||
void test_audiocore_buffer_put(void)
|
||||
{
|
||||
shared_context.audio_buffer_read = shared_context.audio_buffer_write = 0;
|
||||
shared_context.mp3_buffer_read = shared_context.mp3_buffer_write = 0;
|
||||
|
||||
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, fill_buffer_helper());
|
||||
for (unsigned i = 0; i < AUDIO_BUFFER_SIZE - 1; ++i) {
|
||||
TEST_ASSERT_EQUAL(i, shared_context.audio_buffer[i]);
|
||||
TEST_ASSERT_EQUAL(MP3_BUFFER_SIZE - 1, fill_buffer_helper());
|
||||
for (unsigned i = 0; i < MP3_BUFFER_SIZE - 1; ++i) {
|
||||
TEST_ASSERT_EQUAL((char)(i & 0xFF), shared_context.mp3_buffer[MP3_BUFFER_PREAREA + i]);
|
||||
}
|
||||
|
||||
// test wraparound fill
|
||||
shared_context.audio_buffer_read = shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 10;
|
||||
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, fill_buffer_helper());
|
||||
for (unsigned i = 0; i < AUDIO_BUFFER_SIZE - 1; ++i) {
|
||||
TEST_ASSERT_EQUAL(i, shared_context.audio_buffer[(i + AUDIO_BUFFER_SIZE - 10) % AUDIO_BUFFER_SIZE]);
|
||||
shared_context.mp3_buffer_read = shared_context.mp3_buffer_write = MP3_BUFFER_SIZE - 10;
|
||||
TEST_ASSERT_EQUAL(MP3_BUFFER_SIZE - 1, fill_buffer_helper());
|
||||
for (unsigned i = 0; i < MP3_BUFFER_SIZE - 1; ++i) {
|
||||
TEST_ASSERT_EQUAL(
|
||||
(char)(i & 0xFF),
|
||||
shared_context.mp3_buffer[MP3_BUFFER_PREAREA + ((i + MP3_BUFFER_SIZE - 10) % MP3_BUFFER_SIZE)]);
|
||||
}
|
||||
}
|
||||
|
||||
static unsigned get_buffer_helper(void)
|
||||
void test_audiocore_volume_adjust(void)
|
||||
{
|
||||
uint32_t test_data[100];
|
||||
uint32_t ctr = 0;
|
||||
const int16_t samples[] = {0, 1, -1, INT16_MIN, INT16_MAX, 100, -100, 99, -99};
|
||||
#define SAMPLE_COUNT (sizeof(samples) / sizeof(samples[0]))
|
||||
const struct {
|
||||
uint16_t scalefactor;
|
||||
int16_t expected[SAMPLE_COUNT];
|
||||
} expected[] = {
|
||||
{.scalefactor = AUDIOCORE_MAX_VOLUME, .expected = {0, 1, -1, INT16_MIN, INT16_MAX, 100, -100, 99, -99}},
|
||||
{.scalefactor = 0, .expected = {0}},
|
||||
{.scalefactor = AUDIOCORE_MAX_VOLUME / 2, .expected = {0, 0, -1, -16384, 16383, 50, -50, 49, -50}}};
|
||||
|
||||
unsigned avail, gotten = 0;
|
||||
while ((avail = audiocore_get_audio_buffer_avail()) > 0) {
|
||||
const unsigned todo = avail > 100 ? 100 : avail;
|
||||
audiocore_audio_buffer_get(test_data, todo);
|
||||
for (unsigned i = 0; i < todo; ++i)
|
||||
TEST_ASSERT_EQUAL(ctr++, test_data[i]);
|
||||
gotten += todo;
|
||||
for (int test = 0; test < sizeof(expected) / sizeof(expected[0]); ++test) {
|
||||
int16_t buf[sizeof(samples) / sizeof(samples[0])];
|
||||
memcpy(buf, samples, sizeof(samples));
|
||||
volume_adjust(buf, SAMPLE_COUNT, expected[test].scalefactor);
|
||||
TEST_ASSERT_EQUAL_HEX16_ARRAY(expected[test].expected, buf, SAMPLE_COUNT);
|
||||
}
|
||||
return gotten;
|
||||
}
|
||||
|
||||
void test_audiocore_buffer_get(void)
|
||||
{
|
||||
shared_context.audio_buffer_read = 0;
|
||||
shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 1;
|
||||
for (unsigned i = 0; i < AUDIO_BUFFER_SIZE - 1; ++i)
|
||||
shared_context.audio_buffer[i] = i;
|
||||
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, get_buffer_helper());
|
||||
|
||||
// test wraparound read
|
||||
shared_context.audio_buffer_read = 10;
|
||||
shared_context.audio_buffer_write = 9;
|
||||
for (unsigned i = 0; i < AUDIO_BUFFER_SIZE - 1; ++i)
|
||||
shared_context.audio_buffer[(i + 10) % AUDIO_BUFFER_SIZE] = i;
|
||||
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, get_buffer_helper());
|
||||
}
|
||||
|
||||
@@ -3,22 +3,21 @@
|
||||
|
||||
import aiorepl
|
||||
import asyncio
|
||||
import audiocore
|
||||
import machine
|
||||
import micropython
|
||||
import os
|
||||
import time
|
||||
from array import array
|
||||
from machine import Pin
|
||||
from math import pi, sin, pow
|
||||
from micropython import const
|
||||
|
||||
# Own modules
|
||||
from audiocore import Audiocore
|
||||
from rp2_neopixel import NeoPixel
|
||||
from rp2_sd import SDCard
|
||||
|
||||
micropython.alloc_emergency_exception_buf(100)
|
||||
|
||||
asyncio.create_task(aiorepl.task())
|
||||
|
||||
leds = const(10)
|
||||
brightness = 0.5
|
||||
|
||||
@@ -43,27 +42,35 @@ async def rainbow(np, period=10):
|
||||
await asyncio.sleep_ms(20 - (now - before))
|
||||
|
||||
|
||||
samplerate = 44100
|
||||
hz = 441
|
||||
count = 100
|
||||
amplitude = 0x1fff
|
||||
buf = array('I', range(count))
|
||||
for i in range(len(buf)):
|
||||
val = int(sin(i * hz / samplerate * 2 * pi)*amplitude) & 0xffff
|
||||
buf[i] = (val << 16) | val
|
||||
|
||||
|
||||
async def output_sound(audioctx):
|
||||
pos = 0
|
||||
async def play_mp3(audiocore, mp3file):
|
||||
_, avail, _ = audioctx.put(b'')
|
||||
known_underruns = 0
|
||||
while True:
|
||||
pushed, avail, underruns = audioctx.put(buf[pos:])
|
||||
pos = (pos + pushed) % len(buf)
|
||||
# print(f"pushed {pushed}, pos {pos}, avail {avail}")
|
||||
data = mp3file.read(avail)
|
||||
if avail > 0 and len(data) == 0:
|
||||
# End of file
|
||||
break
|
||||
pos = 0
|
||||
while pos < len(data):
|
||||
pushed, avail, underruns = audioctx.put(data[pos:])
|
||||
if pushed == 0:
|
||||
await asyncio.sleep_ms(0)
|
||||
else:
|
||||
await asyncio.sleep_ms(0)
|
||||
pos += pushed
|
||||
if underruns > known_underruns:
|
||||
print(f"{underruns:x}")
|
||||
known_underruns = underruns
|
||||
await asyncio.sleep(0)
|
||||
audioctx.flush()
|
||||
print("Decoding ended")
|
||||
|
||||
|
||||
async def play_mp3s(audiocore, mp3files):
|
||||
for name in mp3files:
|
||||
print(b'Playing ' + name)
|
||||
with open(name, "rb") as testfile:
|
||||
await play_mp3(audiocore, testfile)
|
||||
await asyncio.sleep_ms(1000)
|
||||
|
||||
|
||||
# Set 8 mA drive strength and fast slew rate
|
||||
@@ -120,9 +127,14 @@ list_sd()
|
||||
asyncio.create_task(rainbow(np))
|
||||
|
||||
# Test audio
|
||||
audioctx = audiocore.init(Pin(8), Pin(6), samplerate)
|
||||
asyncio.create_task(output_sound(audioctx))
|
||||
audioctx = Audiocore(Pin(8), Pin(6))
|
||||
|
||||
asyncio.create_task(latency_test())
|
||||
# high prio for proc 1
|
||||
machine.mem32[0x40030000 + 0x00] = 0x10
|
||||
|
||||
testfiles = [b'/sd/' + name for name in os.listdir(b'/sd') if name.endswith(b'mp3')]
|
||||
|
||||
asyncio.create_task(play_mp3s(audioctx, testfiles))
|
||||
|
||||
asyncio.create_task(aiorepl.task())
|
||||
asyncio.get_event_loop().run_forever()
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
||||
// Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
struct spin_lock;
|
||||
typedef struct spin_lock spin_lock_t;
|
||||
|
||||
void multicore_fifo_push_blocking(unsigned val);
|
||||
unsigned multicore_fifo_pop_blocking(void);
|
||||
bool multicore_fifo_rvalid(void);
|
||||
|
||||
#define __time_critical_func(x) x
|
||||
#define __wfe()
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
#include <errno.h>
|
||||
|
||||
#define MP_EIO EIO
|
||||
#define MP_ENOMEM ENOMEM
|
||||
|
||||
Reference in New Issue
Block a user