Compare commits

...

6 commits

Author SHA1 Message Date
Solomon Peachy
ea570c5728 pcm: Make more of the low-level PCM API private
* pcm_play_data
 * pcm_play_stop
 * pcm_play_stop_int
 * pcm_is_playing
 * pcm_set_frequency
 * pcm_get_frequency
 * pcm_apply_settings

Now, the only user of these functions are the mixer and recording layers
that provide a higher-level API to plugins and the main [playback]
application.

Outside of the PCM core, pcm_apply_settings() was only used immediately
following a call to mixer_set_frequency(), so the latter function
now always calls the former.

Change-Id: I61c3144dc156b9de9b7963160b525c6d10c6ad4b
2026-03-03 13:09:21 -05:00
Solomon Peachy
be1e074800 doom: minor code cleanup (replace magic constant with an existing define)
Change-Id: I39dd9accb0bb116c0c23035b75b7be141a236c01
2026-03-03 12:42:09 -05:00
Aidan MacDonald
84fa538979 echoplayer: add PCM debug menu
Display DMA error counters and the SAI underrun counter
in the debug menu.

Change-Id: I235bfcb0fa965d87c30deeb300535141eda5f974
2026-03-03 09:24:01 -05:00
Aidan MacDonald
5c1ae51193 echoplayer: implement audio playback
Playback is implemented using a target-specific PCM layer,
using the STM32H7 SAI & DMA registers directly. There are
a number of pop/click issues:

1. Slight click when powering up the amplifiers
2. Click when starting and stopping playback
3. Popping when changing playback frequency
4. Popping when shutting down

It should be possible to eliminate or at least mitigate
(2) to (4) in software, but (1) happens as a result of
powering on the amplifiers while everything is muted so
might be unavoidable.

Change-Id: I398b66596176fb2341beb7deba7bf6f4f3fb82b3
2026-03-03 09:23:23 -05:00
Vencislav Atanasov
b782f15eaa Update Bulgarian translation
Change-Id: Iac4ef4b9ae6e5778dc37fb423297336480f9b8a0
2026-03-03 14:20:49 +02:00
Solomon Peachy
c9417e24c1 FS#13807 - Updated German translation (Wilfried Winkler)
Change-Id: I01f4ba343de72386cf07efe9afdf1535659cb19f
2026-03-03 06:59:40 -05:00
25 changed files with 681 additions and 219 deletions

View file

@ -12507,20 +12507,6 @@
*: "Средна побитова скорост"
</voice>
</phrase>
<phrase>
id: LANG_PLAYTIME_ERROR
desc: playing time screen
user: core
<source>
*: "Error while gathering info"
</source>
<dest>
*: "Грешка при събирането на информация"
</dest>
<voice>
*: "Грешка при събирането на информация"
</voice>
</phrase>
<phrase>
id: LANG_PLAYING_TIME
desc: onplay menu
@ -14686,7 +14672,7 @@
user: core
<source>
*: "Press LEFT to cancel."
android,hifietma*,zenvision: "Press BACK to cancel."
android,hifietma*: "Press BACK to cancel."
cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Press POWER to cancel."
ihifi760,ihifi960: "Double tap RETURN to cancel."
ihifi770,ihifi770c,ihifi800: "Press HOME to cancel."
@ -14701,7 +14687,7 @@
</source>
<dest>
*: "Натиснете LEFT за отказ."
android,hifietma*,zenvision: "Натиснете BACK за отказ."
android,hifietma*: "Натиснете BACK за отказ."
cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Натиснете POWER за отказ."
ihifi760,ihifi960: "Натиснете двукратно RETURN за отказ."
ihifi770,ihifi770c,ihifi800: "Натиснете HOME за отказ."
@ -14716,7 +14702,7 @@
</dest>
<voice>
*: "Натиснете LEFT за отказ."
android,hifietma*,zenvision: "Натиснете BACK за отказ."
android,hifietma*: "Натиснете BACK за отказ."
cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Натиснете POWER за отказ."
ihifi760,ihifi960: "Натиснете двукратно RETURN за отказ."
ihifi770,ihifi770c,ihifi800: "Натиснете HOME за отказ."
@ -15211,13 +15197,13 @@
</phrase>
<phrase>
id: LANG_VOICED_DATE_FORMAT
desc: format string for how dates will be read back. Y == 4-digit year, A == month name, m == numeric month, d == numeric day. For example, "AdY" will read "January 21 2021"
desc: format string for how dates will be read back. Y == 4-digit year (grouped), y == 4-digit year (numeric), A == month name, m == numeric month, d == numeric day. For example, for 2021-01-05, "AdY" will be voiced as "January 5 twenty twenty-one" and "dmy" will be voiced as "5 1 two thousand twenty one
user: core
<source>
*: "dAY"
</source>
<dest>
*: "~dAY"
*: "dAy"
</dest>
<voice>
*: ""
@ -15832,20 +15818,6 @@
*: "Браузър по подразбиране"
</voice>
</phrase>
<phrase>
id: LANG_AMAZE_MENU
desc: Amaze game
user: core
<source>
*: "Amaze Main Menu"
</source>
<dest>
*: "Главно меню на Amaze"
</dest>
<voice>
*: "Главно меню на Amaze"
</voice>
</phrase>
<phrase>
id: LANG_SET_MAZE_SIZE
desc: Maze size in Amaze game
@ -16098,34 +16070,6 @@
*: "Настройки на Mik mod"
</voice>
</phrase>
<phrase>
id: LANG_MIKMOD_MENU
desc: mikmod plugin
user: core
<source>
*: "Mikmod Menu"
</source>
<dest>
*: "Меню на Mikmod"
</dest>
<voice>
*: "Меню на Mik mod"
</voice>
</phrase>
<phrase>
id: LANG_CHESSBOX_MENU
desc: chessbox plugin
user: core
<source>
*: "Chessbox Menu"
</source>
<dest>
*: "Меню на Chessbox"
</dest>
<voice>
*: "Меню на Chess box"
</voice>
</phrase>
<phrase>
id: VOICE_INVALID_VOICE_FILE
desc: played if the voice file fails to load
@ -16987,3 +16931,48 @@
usbdac: "USB ЦАП е активен"
</voice>
</phrase>
<phrase>
id: LANG_ANNOUNCE_STATUS
desc: announnnce_status plugin
user: core
<source>
*: "Announce Status"
</source>
<dest>
*: "Съобщаване на състоянието"
</dest>
<voice>
*: "Съобщаване на състоянието"
</voice>
</phrase>
<phrase>
id: LANG_USE_LED_INDICATORS
desc: LED indicators setting
user: core
<source>
*: none
general_purpose_led: "Use LED indicators"
</source>
<dest>
*: none
general_purpose_led: "Използване на светодиодни индикатори"
</dest>
<voice>
*: none
general_purpose_led: "Използване на светодиодни индикатори"
</voice>
</phrase>
<phrase>
id: LANG_KEEP_DIRECTORY
desc: file browser setting
user: core
<source>
*: "Always remember last folder"
</source>
<dest>
*: "Винаги помни последната папка"
</dest>
<voice>
*: "Винаги помни последната папка"
</voice>
</phrase>

View file

@ -14690,7 +14690,7 @@
user: core
<source>
*: "Press LEFT to cancel."
android,hifietma*,zenvision: "Press BACK to cancel."
android,hifietma*: "Press BACK to cancel."
cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Press POWER to cancel."
ihifi760,ihifi960: "Double tap RETURN to cancel."
ihifi770,ihifi770c,ihifi800: "Press HOME to cancel."
@ -14705,7 +14705,7 @@
</source>
<dest>
*: "Zum Abbrechen LINKS drücken."
android,hifietma*,vibe500,zenvision: "Zum Abbrechen ZURÜCK drücken."
android,hifietma*,vibe500: "Zum Abbrechen ZURÜCK drücken."
cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Zum Abbrechen POWER drücken."
ihifi760,ihifi960: "Zum Abbrechen ZURÜCK doppeldrücken."
ihifi770,ihifi770c,ihifi800: "Zum Abbrechen STARTMENÜ drücken."
@ -14719,7 +14719,7 @@
</dest>
<voice>
*: "Zum Abbrechen LINKS drücken."
android,hifietma*,vibe500,zenvision: "Zum Abbrechen ZURÜCK drücken."
android,hifietma*,vibe500: "Zum Abbrechen ZURÜCK drücken."
cowond2,creativezenxfi2,ibassodx50,ibassodx90,mrobe500,ondavx747: "Zum Abbrechen POWER drücken."
ihifi760,ihifi960: "Zum Abbrechen ZURÜCK doppeldrücken."
ihifi770,ihifi770c,ihifi800: "Zum Abbrechen STARTMENÜ drücken."
@ -15213,7 +15213,7 @@
</phrase>
<phrase>
id: LANG_VOICED_DATE_FORMAT
desc: format string for how dates will be read back. Y == 4-digit year, A == month name, m == numeric month, d == numeric day. For example, "AdY" will read "January 21 2021"
desc: format string for how dates will be read back. Y == 4-digit year (grouped), y == 4-digit year (numeric), A == month name, m == numeric month, d == numeric day. For example, for 2021-01-05, "AdY" will be voiced as "January 5 twenty twenty-one" and "dmy" will be voiced as "5 1 two thousand twenty one
user: core
<source>
*: "dAY"
@ -16978,3 +16978,17 @@
*: "Status mitteilen"
</voice>
</phrase>
<phrase>
id: LANG_KEEP_DIRECTORY
desc: file browser setting
user: core
<source>
*: "Always remember last folder"
</source>
<dest>
*: "Zuletzt verwendeten Ordner immer merken"
</dest>
<voice>
*: "Zuletzt verwendeten Ordner immer merken"
</voice>
</phrase>

View file

@ -644,7 +644,6 @@ static const struct plugin_api rockbox_api = {
#endif
&audio_master_sampr_list[0],
&hw_freq_sampr[0],
pcm_apply_settings,
pcm_play_lock,
pcm_play_unlock,
pcm_current_sink_caps,

View file

@ -179,7 +179,7 @@ int plugin_open(const char *plugin, const char *parameter);
* when this happens please take the opportunity to sort in
* any new functions "waiting" at the end of the list.
*/
#define PLUGIN_API_VERSION 279
#define PLUGIN_API_VERSION 280
/* 239 Marks the removal of ARCHOS HWCODEC and CHARCELL */
@ -743,7 +743,6 @@ struct plugin_api {
#endif
const unsigned long *audio_master_sampr_list;
const unsigned long *hw_freq_sampr;
void (*pcm_apply_settings)(void);
void (*pcm_play_lock)(void);
void (*pcm_play_unlock)(void);
const struct pcm_sink_caps* (*pcm_current_sink_caps)(void);

View file

@ -462,7 +462,7 @@ void get_more(const void** start, size_t* size)
I_UpdateSound(); // Force sound update
*start = mixbuffer;
*size = SAMPLECOUNT*2*sizeof(short);
*size = MIXBUFFERSIZE*sizeof(short);
}

View file

@ -24,6 +24,5 @@
-- [[ conversion to old style pcm_ functions ]]
if not rb.pcm then rb.splash(rb.HZ, "No Support!") return nil end
rb.pcm_apply_settings = function() rb.pcm("apply_settings") end
rb.pcm_play_lock = function() rb.pcm("play_lock") end
rb.pcm_play_unlock = function() rb.pcm("play_unlock") end

View file

@ -549,10 +549,10 @@ RB_WRAP(sound)
RB_WRAP(pcm)
{
enum e_pcm {PCM_APPLYSETTINGS = 0, PCM_PLAYLOCK, PCM_PLAYUNLOCK,
enum e_pcm {PCM_PLAYLOCK = 0, PCM_PLAYUNLOCK,
PCM_ECOUNT};
const char *pcm_option[] = {"apply_settings", "play_lock", "play_unlock",
const char *pcm_option[] = {"play_lock", "play_unlock",
NULL};
lua_pushnil(L); /*push nil so options w/o return have something to return */
@ -560,9 +560,6 @@ RB_WRAP(pcm)
int option = luaL_checkoption (L, 1, NULL, pcm_option);
switch(option)
{
case PCM_APPLYSETTINGS:
rb->pcm_apply_settings();
break;
case PCM_PLAYLOCK:
rb->pcm_play_lock();
break;

View file

@ -64,7 +64,6 @@ void rockbox_open_audio(int rate)
/* Set sample rate of the audio buffer. */
rb->mixer_set_frequency(rate);
rb->pcm_apply_settings();
/* Initialize output buffer. */
for(i = 0; i < OUTBUFSIZE; i++)
@ -86,7 +85,6 @@ void rockbox_close_audio(void)
/* Restore default sampling rate. */
rb->mixer_set_frequency(HW_SAMPR_DEFAULT);
rb->pcm_apply_settings();
}
/* Rockbox audio callback. */

View file

@ -168,7 +168,6 @@ static void set_frequency(int index)
update_gen_step();
rb->mixer_set_frequency(hw_sampr);
rb->pcm_apply_settings();
}
#ifndef HAVE_VOLUME_IN_LIST

View file

@ -253,7 +253,6 @@ bool syssnd_init(void)
#endif
rb->mixer_set_frequency(HW_FREQ_44);
rb->pcm_apply_settings();
rb->memset(channels, 0, sizeof(channels));
rb->memset(mixBuffers, 0, sizeof(mixBuffers));
@ -286,7 +285,6 @@ void syssnd_shutdown(void)
/* Restore default sampling rate. */
rb->mixer_set_frequency(HW_SAMPR_DEFAULT);
rb->pcm_apply_settings();
rb->talk_disable(false);

View file

@ -616,6 +616,10 @@ static void init_state(void)
record_status = RECORD_STOPPED;
}
/* To avoid having to pull in all of pcm-internal.h */
void pcm_set_frequency(unsigned int samplerate);
void pcm_apply_settings(void);
/* Set hardware samplerate and save it */
static void update_samplerate_config(unsigned long sampr)
{
@ -876,7 +880,7 @@ copy_buffer_mono_lr(void *dst, const void *src, size_t src_size)
int16_t *d = (int16_t*) dst;
int16_t const *s = (int16_t const*) src;
ssize_t copy_size = src_size;
/* mono = (L + R) / 2 */
while(copy_size > 0) {
*d++ = ((int32_t)s[0] + (int32_t)s[1] + 1) >> 1;

View file

@ -1754,10 +1754,6 @@ void pcmbuf_set_low_latency(bool state)
\param state
\description
void pcm_apply_settings(void)
\group sound
\description
void pcm_calculate_rec_peaks(int *left, int *right)
\group sound
\conditions (defined(HAVE_RECORDING))

View file

@ -1942,7 +1942,6 @@ target/arm/stm32/debug-stm32h7.c
#endif
target/arm/stm32/gpio-stm32h7.c
target/arm/stm32/i2c-stm32h7.c
target/arm/stm32/pcm-stm32h7.c
target/arm/stm32/sdmmc-stm32h7.c
target/arm/stm32/spi-stm32h7.c
target/arm/stm32/system-stm32h7.c
@ -1959,6 +1958,9 @@ target/arm/stm32/echoplayer/button-echoplayer.c
target/arm/stm32/echoplayer/clock-echoplayer.c
target/arm/stm32/echoplayer/i2c-echoplayer.c
target/arm/stm32/echoplayer/lcd-echoplayer.c
#ifndef BOOTLOADER
target/arm/stm32/echoplayer/pcm-echoplayer.c
#endif
target/arm/stm32/echoplayer/power-echoplayer.c
target/arm/stm32/echoplayer/sdmmc-echoplayer.c
target/arm/stm32/echoplayer/system-echoplayer.c

View file

@ -51,9 +51,14 @@
/* Codec / audio hardware defines */
#define HW_SAMPR_CAPS SAMPR_CAP_ALL_96 // FIXME: check this section
#define HAVE_ECHOPLAYER_CODEC
#define HAVE_AIC310X
#define HAVE_SW_TONE_CONTROLS
#define HAVE_SW_VOLUME_CONTROL
#ifndef SIMULATOR
#define PCM_NATIVE_BITDEPTH 32
#endif
/* Button defines */
#define CONFIG_KEYPAD ECHO_R1_PAD
#define HAVE_HEADPHONE_DETECTION

View file

@ -21,9 +21,13 @@
#ifndef __ECHOPLAYER_CODEC_H__
#define __ECHOPLAYER_CODEC_H__
#include <stdbool.h>
/* -79 to 0 dB in 0.5 dB steps; software volume control
* is used because the hardware volume controls "click"
* when changing the volume */
AUDIOHW_SETTING(VOLUME, "dB", 1, 5, -790, 0, -200);
void audiohw_mute(bool mute);
#endif /* __ECHOPLAYER_CODEC_H__ */

View file

@ -71,6 +71,20 @@ void pcm_sync_pcm_factors(void);
({ (start) = (void *)(((uintptr_t)(start) + 3) & ~3); \
(size) &= ~3; })
/* Internal PCM API calls for playback */
void pcm_play_data(pcm_play_callback_type get_more,
pcm_status_callback_type status_cb,
const void *start, size_t size);
void pcm_play_stop(void);
void pcm_play_stop_int(void); /* requires PCM lock held */
bool pcm_is_playing(void);
void pcm_set_frequency(unsigned int samplerate);
unsigned int pcm_get_frequency(void);
/* apply settings to hardware immediately */
void pcm_apply_settings(void);
void pcm_do_peak_calculation(struct pcm_peaks *peaks, bool active,
const void *addr, int count);
@ -125,7 +139,6 @@ pcm_play_dma_status_callback(enum pcm_dma_status status)
#if defined(HAVE_SW_VOLUME_CONTROL) && !defined(PCM_SW_VOLUME_UNBUFFERED)
void pcm_play_dma_start_int(const void *addr, size_t size);
void pcm_play_dma_stop_int(void);
void pcm_play_stop_int(void);
#endif /* HAVE_SW_VOLUME_CONTROL && !PCM_SW_VOLUME_UNBUFFERED */
/* Called by the bottom layer ISR when more data is needed. Returns true

View file

@ -44,7 +44,7 @@ typedef void (*pcm_play_callback_type)(const void **start, size_t *size);
/* Typedef for registered status callback */
typedef enum pcm_dma_status (*pcm_status_callback_type)(enum pcm_dma_status status);
/* set the pcm frequency - use values in hw_sampr_list
/* set the pcm frequency - use values in hw_sampr_list
* when CONFIG_SAMPR_TYPES is #defined, or-in SAMPR_TYPE_* fields with
* frequency value. SAMPR_TYPE_PLAY is 0 and the default if none is
* specified. */
@ -54,15 +54,6 @@ unsigned int pcm_sampr_type_rec_to_play(unsigned int samplerate);
#endif
#endif /* CONFIG_SAMPR_TYPES */
/* set next frequency to be used */
void pcm_set_frequency(unsigned int samplerate);
/* return last-set frequency */
unsigned int pcm_get_frequency(void);
/* apply settings to hardware immediately */
void pcm_apply_settings(void);
/** RAW PCM playback routines **/
/* Reenterable locks for locking and unlocking the playback interrupt */
void pcm_play_lock(void);
void pcm_play_unlock(void);
@ -77,11 +68,6 @@ const struct pcm_sink_caps* pcm_sink_caps(enum pcm_sink_ids sink);
/* shortcut for plugins */
const struct pcm_sink_caps* pcm_current_sink_caps(void);
/* This is for playing "raw" PCM data */
void pcm_play_data(pcm_play_callback_type get_more,
pcm_status_callback_type status_cb,
const void *start, size_t size);
/* Kept internally for global PCM and used by mixer's verion of peak
calculation */
struct pcm_peaks
@ -92,9 +78,6 @@ struct pcm_peaks
long tick; /* Last tick called */
};
void pcm_play_stop(void);
bool pcm_is_playing(void);
#ifdef HAVE_RECORDING
/** RAW PCM recording routines **/

View file

@ -94,8 +94,6 @@ volatile pcm_status_callback_type
/* PCM playback state */
volatile bool pcm_playing SHAREDBSS_ATTR = false;
void pcm_play_stop_int(void);
struct pcm_sink* pcm_get_current_sink(void)
{
return sinks[cur_sink];

View file

@ -475,6 +475,9 @@ void mixer_set_frequency(unsigned int samplerate)
mix_frame_size = 1;
mix_frame_size *= MIX_FRAME_SAMPLES * 4;
if (pcm_is_initialized())
pcm_apply_settings();
}
/* Get output samplerate */

View file

@ -86,11 +86,68 @@ static bool swd_menu(void)
return simplelist_show_list(&info);
}
#if defined(ECHO_R1)
extern volatile int pcm_sai_xrun_count;
extern volatile int pcm_dma_teif_count;
extern volatile int pcm_dma_dmeif_count;
extern volatile int pcm_dma_feif_count;
struct pcmdebug_counter
{
const char *name;
volatile int *value;
};
static const struct pcmdebug_counter pcmdebug_counters[] =
{
{"SAI xrun count", &pcm_sai_xrun_count},
{"DMA TEIF count", &pcm_dma_teif_count},
{"DMA DMEIF count", &pcm_dma_dmeif_count},
{"DMA FEIF count", &pcm_dma_feif_count},
};
static int pcmdebug_menu_action_cb(int action, struct gui_synclist *lists)
{
if (action == ACTION_STD_OK)
{
int item = gui_synclist_get_sel_pos(lists);
const struct pcmdebug_counter *counter = &pcmdebug_counters[item];
*counter->value = 0;
action = ACTION_REDRAW;
}
if (action == ACTION_NONE)
action = ACTION_REDRAW;
return action;
}
static const char *pcmdebug_menu_get_name(int item, void *data, char *buf, size_t bufsz)
{
const struct pcmdebug_counter *counter = &pcmdebug_counters[item];
(void)data;
snprintf(buf, bufsz, "%s: %d", counter->name, *counter->value);
return buf;
}
static bool pcm_debug_menu(void)
{
struct simplelist_info info;
simplelist_info_init(&info, "PCM debug", ARRAYLEN(pcmdebug_counters), NULL);
info.action_callback = pcmdebug_menu_action_cb;
info.get_name = pcmdebug_menu_get_name;
return simplelist_show_list(&info);
}
#endif
/* Menu definition */
static const struct {
const char *name;
bool (*function) (void);
} menuitems[] = {
{"PCM debug", pcm_debug_menu},
{"SWD/JTAG", swd_menu},
};

View file

@ -19,21 +19,151 @@
*
****************************************************************************/
#include "audiohw.h"
#include "kernel.h"
#include "panic.h"
#include "aic310x.h"
#include "pcm_sw_volume.h"
#include "power-echoplayer.h"
#include "gpio-stm32h7.h"
#include "i2c-echoplayer.h"
static int tlv_read_multiple(uint8_t reg, uint8_t *data, size_t count)
{
return stm32_i2c_read_mem(&i2c1_ctl, AIC310X_I2C_ADDR, reg, data, count);
}
static int tlv_write_multiple(uint8_t reg, const uint8_t *data, size_t count)
{
return stm32_i2c_write_mem(&i2c1_ctl, AIC310X_I2C_ADDR, reg, data, count);
}
static struct aic310x tlv_codec = {
.read_multiple = tlv_read_multiple,
.write_multiple = tlv_write_multiple,
};
struct codec_reg_data
{
uint8_t reg;
uint8_t val;
};
static const struct codec_reg_data codec_init_seq[] = {
/* Set output driver configuration to stereo psuedo-differential */
{ .reg = AIC310X_HEADSET_DETECT_B, .val = 0x08 },
/* Clock input is CODEC_CLKIN (bypass codec PLL) */
{ .reg = AIC310X_CLOCK, .val = 0x01 },
/* Route left ch. -> left DAC, right ch -> right DAC */
{ .reg = AIC310X_DATA_PATH, .val = 0x0A },
/* Power on DACs, set HPLCOM = VCM, set HPRCOM = VCM,
* enable short circuit protection */
{ .reg = AIC310X_HPOUT_DRIVER_CTRL, .val = 0x0C },
{ .reg = AIC310X_DAC_POWER_CTRL, .val = 0xD0 },
/* Unmute the DACs */
{ .reg = AIC310X_LEFT_DAC_DIGITAL_VOLUME, .val = 0x00 },
{ .reg = AIC310X_RIGHT_DAC_DIGITAL_VOLUME, .val = 0x00 },
/* Route DACs to output drivers */
{ .reg = AIC310X_DAC_L1_TO_HPLOUT_VOLUME, .val = 0x80 },
{ .reg = AIC310X_DAC_R1_TO_HPROUT_VOLUME, .val = 0x80 },
/* Route DACs to line out port */
{ .reg = AIC310X_DAC_L1_TO_LEFT_LO_VOLUME, .val = 0x80 },
{ .reg = AIC310X_DAC_R1_TO_RIGHT_LO_VOLUME, .val = 0x80 },
/* Power on headphone output drivers */
{ .reg = AIC310X_HPLCOM_LEVEL_CONTROL, .val = 0x01 },
{ .reg = AIC310X_HPRCOM_LEVEL_CONTROL, .val = 0x01 },
{ .reg = AIC310X_HPLOUT_LEVEL_CONTROL, .val = 0x01 },
{ .reg = AIC310X_HPROUT_LEVEL_CONTROL, .val = 0x01 },
/* Power up line out drivers */
{ .reg = AIC310X_RIGHT_LO_LEVEL_CONTROL, .val = 0x01 },
{ .reg = AIC310X_LEFT_LO_LEVEL_CONTROL, .val = 0x01 },
};
static void write_aic310x_seq(const struct codec_reg_data *seq, size_t count)
{
while (count > 0)
{
if (aic310x_write(&tlv_codec, seq->reg, seq->val))
panicf("aic310x wr fail: %02x", (unsigned int)seq->reg);
seq++;
count--;
}
}
static void wait_aic310x_active(void)
{
long end_tick = current_tick + HZ;
while (TIME_BEFORE(current_tick, end_tick))
{
uint8_t tmp;
int err = tlv_read_multiple(AIC310X_PAGE_SELECT, &tmp, 1);
if (err == 0)
return;
sleep(1);
}
panicf("aic310x init timeout");
}
void audiohw_init(void)
{
/* initialize driver */
aic310x_init(&tlv_codec);
/* bring 1.8v regulator up */
echoplayer_enable_1v8_regulator(true);
/* apply AVDD/DRVDD and DVDD */
gpio_set_level(GPIO_CODEC_AVDD_EN, 0);
gpio_set_level(GPIO_CODEC_DVDD_EN, 0);
/* delay for power stabilization */
sleep(HZ/100);
/* bring codec out of reset */
gpio_set_level(GPIO_CODEC_RESET, 1);
}
void audiohw_postinit(void)
{
wait_aic310x_active();
write_aic310x_seq(codec_init_seq, ARRAYLEN(codec_init_seq));
}
void audiohw_close(void)
{
/* apply reset */
gpio_set_level(GPIO_CODEC_RESET, 0);
/* remove power */
gpio_set_level(GPIO_CODEC_AVDD_EN, 1);
gpio_set_level(GPIO_CODEC_DVDD_EN, 1);
/* disable regulator */
echoplayer_enable_1v8_regulator(false);
}
void audiohw_mute(bool mute)
{
uint8_t wr_val = (mute ? 0x01 : 0x09);
/* Mute and unmute at the output drivers */
aic310x_write(&tlv_codec, AIC310X_HPLOUT_LEVEL_CONTROL, wr_val);
aic310x_write(&tlv_codec, AIC310X_HPROUT_LEVEL_CONTROL, wr_val);
aic310x_write(&tlv_codec, AIC310X_LEFT_LO_LEVEL_CONTROL, wr_val);
aic310x_write(&tlv_codec, AIC310X_RIGHT_LO_LEVEL_CONTROL, wr_val);
}
void audiohw_set_volume(int vol_l, int vol_r)
{
(void)vol_l;
(void)vol_r;
pcm_set_master_volume(vol_l, vol_r);
}

View file

@ -52,6 +52,7 @@ INIT_ATTR static void init_pll(void)
/*
* Use HSE/4 input for PLL1
* Use HSE/16 input for PLL3
* PLL2 reserved for audio; configured in target PCM code
*/
reg_writef(RCC_PLLCKSELR,
PLLSRC_V(HSE),
@ -164,7 +165,7 @@ INIT_ATTR static void init_lse(void)
INIT_ATTR static void init_periph_clock(void)
{
reg_writef(RCC_D1CCIPR, SDMMCSEL_V(PLL1Q));
reg_writef(RCC_D2CCIP1R, SPI45SEL_V(HSE));
reg_writef(RCC_D2CCIP1R, SAI1SEL_V(PLL2P), SPI45SEL_V(HSE));
reg_writef(RCC_D2CCIP2R, I2C123SEL_V(HSI));
/* Enable AXI SRAM in sleep mode to allow DMA'ing out of it */

View file

@ -0,0 +1,381 @@
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2026 Aidan MacDonald
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#include "pcm.h"
#include "pcm_sampr.h"
#include "pcm-internal.h"
#include "audiohw.h"
#include "panic.h"
#include "nvic-arm.h"
#include "dmamux-stm32h743.h"
#include "regs/stm32h743/rcc.h"
#include "regs/stm32h743/sai.h"
#include "regs/stm32h743/dma.h"
#include "regs/stm32h743/dmamux.h"
#include <stdatomic.h>
/* Configurable bit depth */
#if PCM_NATIVE_BITDEPTH == 32
# define PCM_NATIVE_SAMPLE_SIZE 4
# define DMA_PSIZE_VAL BV_DMA_CHN_CR_PSIZE_32BIT
# define SAI_DSIZE_VAL BV_SAI_SUBBLOCK_CR1_DS_32BIT
# define SAI_FIFO_THRESH BV_SAI_SUBBLOCK_CR2_FTH_QUARTER
# define SAI_FRL_VAL 64
#elif PCM_NATIVE_BITDEPTH == 24
# define PCM_NATIVE_SAMPLE_SIZE 4
# define DMA_PSIZE_VAL BV_DMA_CHN_CR_PSIZE_32BIT
# define SAI_DSIZE_VAL BV_SAI_SUBBLOCK_CR1_DS_24BIT
# define SAI_FIFO_THRESH BV_SAI_SUBBLOCK_CR2_FTH_QUARTER
# define SAI_FRL_VAL 64
#elif PCM_NATIVE_BITDEPTH == 16
# define PCM_NATIVE_SAMPLE_SIZE 2
# define DMA_PSIZE_VAL BV_DMA_CHN_CR_PSIZE_16BIT
# define SAI_DSIZE_VAL BV_SAI_SUBBLOCK_CR1_DS_16BIT
# define SAI_FIFO_THRESH BV_SAI_SUBBLOCK_CR2_FTH_HALF
# define SAI_FRL_VAL 32
#else
# error "Unsupported PCM bit depth"
#endif
static const uintptr_t sai1 = ITA_SAI1;
static const uintptr_t sai1a = sai1 + ITO_SAI_A;
static const uintptr_t dmamux1 = ITA_DMAMUX1;
static const uintptr_t dma1 = ITA_DMA1;
static const uintptr_t dma1_ch0 = dma1 + ITO_DMA_CHN(0);
static atomic_size_t play_lock;
static volatile int play_active;
static int pcm_last_freq = -1;
volatile int pcm_sai_xrun_count;
volatile int pcm_dma_teif_count;
volatile int pcm_dma_dmeif_count;
volatile int pcm_dma_feif_count;
static void play_dma_start(const void *addr, size_t size)
{
commit_dcache_range(addr, size);
/*
* The number of transfers is defined by PSIZE, which may
* be 16-bit or 32-bit depending on PCM native sample size.
* Each FIFO write transfers 1 sample, so we want to set
* NDT = size / PCM_NATIVE_SAMPLE_SIZE.
*
* When MSIZE == 32-bit and PSIZE == 16-bit, the DMA will
* take each 32-bit word from memory and write the lower/upper
* halfwords separately to the FIFO. The number of reads from
* memory in this case is NDT/2, so we're still transferring
* the correct amount of data.
*/
reg_varl(dma1_ch0, DMA_CHN_NDTR) = size / PCM_NATIVE_SAMPLE_SIZE;
reg_varl(dma1_ch0, DMA_CHN_M0AR) = (uintptr_t)addr;
reg_writelf(dma1_ch0, DMA_CHN_CR, EN(1));
pcm_play_dma_status_callback(PCM_DMAST_STARTED);
}
static void sai_init(void)
{
/* Enable SAI1 and DMA1 */
reg_writef(RCC_APB2ENR, SAI1EN(1));
reg_writef(RCC_AHB1ENR, DMA1EN(1));
/* Link DMA1_CH0 to SAI1A */
reg_writelf(dmamux1, DMAMUX_CR(0), DMAREQ_ID(DMAMUX1_REQ_SAI1A_DMA));
/* Configure DMA1 channel 0 */
reg_writelf(dma1_ch0, DMA_CHN_CR,
MBURST_V(INCR4), /* 32-bit x 4 burst = 16 bytes */
PBURST_V(INCR4), /* (16|32)-bit x 4 burst = 8-16 bytes */
TRBUFF(0), /* bufferable mode not used for SAI */
DBM(0), /* double buffer mode off */
PL_V(VERYHIGH), /* highest priority */
MSIZE_V(32BIT), /* read 32-bit words from memory */
PSIZE(DMA_PSIZE_VAL), /* set according to PCM bit depth */
MINC(1), /* increment memory address */
PINC(0), /* don't increment peripheral address */
CIRC(0), /* circular mode off */
DIR_V(MEM_TO_PERIPH), /* read from memory, write to SAI */
PFCTRL_V(DMA), /* DMA is flow controller */
TCIE(1), /* transfer complete interrupt */
TEIE(1), /* transfer error interrupt */
DMEIE(1)); /* direct mode error interrupt */
reg_writelf(dma1_ch0, DMA_CHN_FCR,
FEIE(1), /* fifo error interrupt */
DMDIS(1), /* enable fifo mode */
FTH_V(FULL)); /* fifo threshold = 4 words */
/* Set peripheral address here since it's constant */
reg_varl(dma1_ch0, DMA_CHN_PAR) =
(uintptr_t)reg_ptrl(sai1a, SAI_SUBBLOCK_DR);
/* Configure SAI for playback */
reg_writelf(sai1a, SAI_SUBBLOCK_CR1,
OSR_V(256FS), /* MCLK is 256 x Fs */
NOMCK(0), /* MCLK enabled */
DMAEN(0), /* DMA request disabled */
SAIEN(0), /* SAI disabled */
OUTDRIV(0), /* drive outputs when SAIEN=1 */
MONO(0), /* stereo mode */
SYNCEN_V(ASYNC), /* no sync with other SAIs */
CKSTR_V(TX_FALLING_RX_RISING), /* clock edge for sampling */
LSBFIRST(0), /* transmit samples MSB first */
DS(SAI_DSIZE_VAL), /* sample size according to bit depth */
PRTCFG_V(FREE), /* free protocol */
MODE_V(MASTER_TX)); /* operate as bus master */
reg_writelf(sai1a, SAI_SUBBLOCK_CR2,
MUTEVAL_V(ZERO_SAMPLE), /* send zero sample on mute */
MUTE(1), /* mute output initially */
TRIS(0), /* don't tri-state outputs */
FTH(SAI_FIFO_THRESH)); /* fifo threshold (2 or 4 samples) */
reg_writelf(sai1a, SAI_SUBBLOCK_FRCR,
FSOFF(1), FSPOL(0), FSDEF(1), /* I2S format */
FSALL(SAI_FRL_VAL/2 - 1), /* FS active for half the frame */
FRL(SAI_FRL_VAL - 1)); /* set frame length */
reg_writelf(sai1a, SAI_SUBBLOCK_SLOTR,
SLOTEN(3), NBSLOT(2 - 1), /* enable first two slots */
SLOTSZ_V(DATASZ), /* slot size = data size */
FBOFF(0)); /* no bit offset in slot */
/* Enable xrun error interrupt */
reg_writelf(sai1a, SAI_SUBBLOCK_IM, OVRUDR(1));
/* Enable interrupts in NVIC */
nvic_enable_irq(NVIC_IRQN_DMA1_STR0);
nvic_enable_irq(NVIC_IRQN_SAI1);
}
struct div_settings
{
uint8_t pllm;
uint8_t plln;
uint8_t pllp;
uint8_t mckdiv;
};
_Static_assert(STM32_HSE_FREQ == 24000000,
"Audio clock settings only valid for 24MHz HSE");
static const struct div_settings div_settings[] = {
[HW_FREQ_96] = { .pllm = 5, .plln = 128 - 1, .pllp = 25 - 1, .mckdiv = 0 }, /* exact */
[HW_FREQ_48] = { .pllm = 5, .plln = 128 - 1, .pllp = 25 - 1, .mckdiv = 2 }, /* exact */
[HW_FREQ_24] = { .pllm = 5, .plln = 128 - 1, .pllp = 25 - 1, .mckdiv = 4 }, /* exact */
[HW_FREQ_12] = { .pllm = 5, .plln = 128 - 1, .pllp = 25 - 1, .mckdiv = 8 }, /* exact */
[HW_FREQ_88] = { .pllm = 4, .plln = 143 - 1, .pllp = 38 - 1, .mckdiv = 0 }, /* -11ppm error */
[HW_FREQ_44] = { .pllm = 4, .plln = 143 - 1, .pllp = 38 - 1, .mckdiv = 2 }, /* -11ppm error */
[HW_FREQ_22] = { .pllm = 5, .plln = 147 - 1, .pllp = 125 - 1, .mckdiv = 0 }, /* exact */
[HW_FREQ_11] = { .pllm = 5, .plln = 147 - 1, .pllp = 125 - 1, .mckdiv = 2 }, /* exact */
[HW_FREQ_64] = { .pllm = 15, .plln = 256 - 1, .pllp = 25 - 1, .mckdiv = 0 }, /* exact */
[HW_FREQ_32] = { .pllm = 15, .plln = 256 - 1, .pllp = 25 - 1, .mckdiv = 2 }, /* exact */
[HW_FREQ_16] = { .pllm = 15, .plln = 256 - 1, .pllp = 25 - 1, .mckdiv = 4 }, /* exact */
[HW_FREQ_8 ] = { .pllm = 15, .plln = 256 - 1, .pllp = 25 - 1, .mckdiv = 8 }, /* exact */
};
static void sai_set_pll(const struct div_settings *div)
{
/* Disable the PLL so it can be reconfigured */
if (reg_readf(RCC_CR, PLL2RDY))
{
reg_writef(RCC_CR, PLL2ON(0));
while (reg_readf(RCC_CR, PLL2RDY));
while (reg_readf(RCC_CR, PLL2ON));
}
/* Set the PLL configuration */
uint32_t pllcfgr = reg_var(RCC_PLLCFGR);
reg_writef(RCC_PLLCKSELR,
DIVM2(div->pllm));
reg_writef(RCC_PLL2DIVR,
DIVN(div->plln),
DIVP(div->pllp),
DIVQ(0), DIVR(0));
reg_vwritef(pllcfgr, RCC_PLLCFGR,
DIVP2EN(1),
DIVQ2EN(0),
DIVR2EN(0),
PLL2FRACEN(0),
PLL2VCOSEL_V(WIDE));
if (div->pllm <= 6)
reg_vwritef(pllcfgr, RCC_PLLCFGR, PLL2RGE_V(4_8MHZ));
else if (div->pllm <= 12)
reg_vwritef(pllcfgr, RCC_PLLCFGR, PLL2RGE_V(2_4MHZ));
else
reg_vwritef(pllcfgr, RCC_PLLCFGR, PLL2RGE_V(1_2MHZ));
reg_var(RCC_PLLCFGR) = pllcfgr;
/* Enable the PLL again */
reg_writef(RCC_CR, PLL2ON(1));
while (!reg_readf(RCC_CR, PLL2RDY));
}
static void sai_set_frequency(int freq)
{
/* Can't change frequency while the SAI is active */
if (reg_readlf(sai1a, SAI_SUBBLOCK_CR1, SAIEN))
panicf("%s while SAI active", __func__);
const struct div_settings *div = &div_settings[freq];
const struct div_settings *old_div = NULL;
if (pcm_last_freq >= 0)
old_div = &div_settings[pcm_last_freq];
if (old_div == NULL ||
div->pllm != old_div->pllm ||
div->plln != old_div->plln ||
div->pllp != old_div->pllp)
{
sai_set_pll(div);
}
/* Configure the MCK divider in SAI */
reg_writelf(sai1a, SAI_SUBBLOCK_CR1, MCKDIV(div->mckdiv));
}
static void sink_dma_init(void)
{
sai_init();
audiohw_init();
}
static void sink_set_freq(uint16_t freq)
{
/*
* Muting here doesn't seem to help clicks when switching
* tracks of different frequencies; the audio may need to
* be soft-muted in software before the switch.
*/
audiohw_mute(true);
sai_set_frequency(freq);
pcm_last_freq = freq;
audiohw_mute(false);
}
static void sink_dma_start(const void *addr, size_t size)
{
play_active = true;
play_dma_start(addr, size);
reg_writelf(sai1a, SAI_SUBBLOCK_CR1, SAIEN(1), DMAEN(1));
reg_writelf(sai1a, SAI_SUBBLOCK_CR2, MUTE(0));
}
static void sink_dma_stop(void)
{
play_active = false;
reg_writelf(sai1a, SAI_SUBBLOCK_CR2, MUTE(1));
reg_writelf(sai1a, SAI_SUBBLOCK_CR1, SAIEN(0), DMAEN(0));
reg_writelf(dma1_ch0, DMA_CHN_CR, EN(0));
/* Must wait for SAIEN bit to be cleared */
while (reg_readlf(sai1a, SAI_SUBBLOCK_CR1, SAIEN));
}
static void sink_lock(void)
{
/* Disable IRQ on first lock */
if (atomic_fetch_add_explicit(&play_lock, 1, memory_order_relaxed) == 0)
nvic_disable_irq_sync(NVIC_IRQN_DMA1_STR0);
}
static void sink_unlock(void)
{
/* Enable IRQ on release of last lock */
if (atomic_fetch_sub_explicit(&play_lock, 1, memory_order_relaxed) == 1)
nvic_enable_irq(NVIC_IRQN_DMA1_STR0);
}
struct pcm_sink builtin_pcm_sink = {
.caps = {
.samprs = hw_freq_sampr,
.num_samprs = HW_NUM_FREQ,
.default_freq = HW_FREQ_DEFAULT,
},
.ops = {
.init = sink_dma_init,
.postinit = audiohw_postinit,
.set_freq = sink_set_freq,
.lock = sink_lock,
.unlock = sink_unlock,
.play = sink_dma_start,
.stop = sink_dma_stop,
},
};
void dma1_ch0_irq_handler(void)
{
uint32_t lisr = reg_varl(dma1, DMA_LISR);
const void *addr;
size_t size;
if (reg_vreadf(lisr, DMA_LISR, TEIF0))
pcm_dma_teif_count++;
if (reg_vreadf(lisr, DMA_LISR, DMEIF0))
pcm_dma_dmeif_count++;
if (reg_vreadf(lisr, DMA_LISR, FEIF0))
pcm_dma_feif_count++;
if (reg_vreadf(lisr, DMA_LISR, TEIF0) ||
reg_vreadf(lisr, DMA_LISR, DMEIF0) ||
reg_vreadf(lisr, DMA_LISR, FEIF0))
{
reg_assignlf(dma1, DMA_LIFCR, TEIF0(1), DMEIF0(1), FEIF0(1));
reg_writelf(dma1_ch0, DMA_CHN_CR, EN(0));
pcm_play_dma_status_callback(PCM_DMAST_ERR_DMA);
}
else if (reg_vreadf(lisr, DMA_LISR, TCIF0))
{
reg_assignlf(dma1, DMA_LIFCR, TCIF0(1));
/* If we call the complete callback while not playing
* it can cause the PCM layer to get stuck... somehow */
if (!play_active)
return;
if (pcm_play_dma_complete_callback(PCM_DMAST_OK, &addr, &size))
play_dma_start(addr, size);
}
else
{
panicf("%s: %08lx", __func__, lisr);
}
}
void sai1_irq_handler(void)
{
if (reg_readlf(sai1a, SAI_SUBBLOCK_SR, OVRUDR))
{
reg_assignlf(sai1a, SAI_SUBBLOCK_CLRFR, OVRUDR(1));
pcm_sai_xrun_count++;
}
}

View file

@ -1,105 +0,0 @@
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2025 Aidan MacDonald
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#include "pcm.h"
#include "pcm-internal.h"
#include "pcm_sampr.h"
#include "pcm_sink.h"
static void sink_set_freq(uint16_t freq)
{
(void)freq;
}
static void sink_dma_init(void)
{
}
static void sink_dma_postinit(void)
{
}
static void sink_dma_start(const void *addr, size_t size)
{
(void)addr;
(void)size;
}
static void sink_dma_stop(void)
{
}
static void sink_lock(void)
{
}
static void sink_unlock(void)
{
}
struct pcm_sink builtin_pcm_sink = {
.caps = {
.samprs = hw_freq_sampr,
.num_samprs = HW_NUM_FREQ,
.default_freq = HW_FREQ_DEFAULT,
},
.ops = {
.init = sink_dma_init,
.postinit = sink_dma_postinit,
.set_freq = sink_set_freq,
.lock = sink_lock,
.unlock = sink_unlock,
.play = sink_dma_start,
.stop = sink_dma_stop,
},
};
#ifdef HAVE_RECORDING
void pcm_rec_dma_init(void)
{
}
void pcm_rec_dma_close(void)
{
}
void pcm_rec_dma_start(void *addr, size_t size)
{
(void)addr;
(void)size;
}
void pcm_rec_dma_stop(void)
{
}
void pcm_rec_lock(void)
{
}
void pcm_rec_unlock(void)
{
}
const void *pcm_rec_dma_get_peak_buffer(void)
{
return NULL;
}
#endif

View file

@ -493,7 +493,6 @@ static void set_playback_sampling_frequency(unsigned long f)
hw_freq_sampr[as_playback_freq_idx], f);
mixer_set_frequency(hw_freq_sampr[as_playback_freq_idx]);
pcm_apply_settings();
}
unsigned long usb_audio_get_playback_sampling_frequency(void)
@ -706,7 +705,6 @@ static void usb_audio_start_playback(void)
#endif
logf("usbaudio: start playback at %lu Hz", hw_freq_sampr[as_playback_freq_idx]);
mixer_set_frequency(hw_freq_sampr[as_playback_freq_idx]);
pcm_apply_settings();
mixer_channel_set_amplitude(PCM_MIXER_CHAN_USBAUDIO, MIX_AMP_UNITY);
usb_drv_recv_nonblocking(EP_ISO_OUT, rx_buffer, BUFFER_SIZE);
@ -787,7 +785,7 @@ int usb_audio_get_alt_intf(void)
{
return usb_as_playback_intf_alt;
}
int32_t usb_audio_get_samplesperframe(void)
{
return samples_fb;
@ -1348,7 +1346,7 @@ bool usb_audio_fast_transfer_complete(int ep, int dir, int status, int length)
{
retval = false;
}
// send feedback value every N frames!
// NOTE: important that we need to queue this up _the frame before_ it's needed - on MacOS especially!
if ((usb_drv_get_frame_number()+1) % FEEDBACK_UPDATE_RATE_FRAMES == 0 && send_fb)