Audio Service
The ElenixOS audio subsystem uses a four-layer architecture. Each layer has a clear responsibility and is replaceable through its interface:
The service manages a single player instance internally, exposed through the service API. For advanced operations (e.g., querying current_sample for a progress bar), grab the pointer via eos_service_audio_get_player(). Prefer the service-layer functions for normal use.
Speaker: Borrow-Enqueue Zero-Copy
We designed the borrow-enqueue pattern to eliminate inter-layer copies. The platform maintains an internal buffer pool; the player writes decoded PCM directly into platform-owned memory:
enqueue has a critical side-effect: if the speaker is in DEV_STATE_READY (registered but not started), the first enqueue automatically starts the hardware, transitioning to PLAYING. You must replicate this behavior when porting.
ISR Safety: cqueue + Critical Section
The playback completion callback (or DMA interrupt) runs on the audio/interrupt thread. We use eos_cqueue (a lock-free circular queue) + eos_critical_enter/leave to hand buffers between the audio thread and the main LVGL thread:
When porting, you must ensure thread safety between borrow (called from the consumer thread) and the callback/interrupt that feeds buffers back.
Mute: Zero-Fill, Not Volume=0
A common misconception: mute does not call set_volume(0). eos_service_audio_set_mute only writes the flag to config and sets p->muted = true. Inside the fill loop:
if (p->muted) {
memset(buf, 0, cap);
spk->ops->enqueue(buf, cap);
return true;
}
This means the volume level is preserved while muted — you can adjust volume independently and it takes effect immediately on unmute.
Volume: Two-Phase Commit
We split volume setting into two steps to support fade-in/fade-out and batch updates:
eos_audio_player_set_volume(n)— writesp->volumeonly, no hardware accesseos_audio_player_apply_volume(p)— pushes the internal value to the speaker'sset_volumecallback
eos_service_audio_set_volume wraps both. If you need to ramp volume smoothly, call set_volume multiple times then apply_volume once.
Volume is persisted via eos_config to EOS_CONFIG_KEY_SPEAKER_VOLUME_NUMBER (default 50); mute state to EOS_CONFIG_KEY_MUTE_BOOL.
State Machine & Feed Loop
play() execution pipeline:
- Automatically
stops any active playback - Opens the decoder (iterates the probe chain)
- Opens the speaker device and applies volume
- Pre-fills 8 buffers (prevents underrun at start)
- Creates the feed timer (30ms period)
_feed_cb fills as many buffers as possible per tick using a while loop until borrow returns -1. resume() also pre-fills 8 buffers before resuming the hardware.
Decoder Pipeline
Decoders are maintained in an LVGL linked list, probed in registration order. eos_audio_decoder_open iterates the list calling each probe_cb; the first match wins.
Built-in decoders:
| Decoder | Formats | Source |
|---|---|---|
| WAV | 16-bit PCM RIFF/WAVE | src/services/audio/decoders/eos_audio_decoder_wav.c |
probe_cb inspects without holding resources — the WAV decoder opens the file, reads the RIFF header, scans for the fmt chunk, parses format, then closes. Real resource allocation happens in open_cb.
Adding a new decoder:
eos_audio_decoder_t *dec = eos_audio_decoder_create();
eos_audio_decoder_set_probe_cb(dec, my_probe);
eos_audio_decoder_set_open_cb(dec, my_open);
eos_audio_decoder_set_read_cb(dec, my_read);
eos_audio_decoder_set_close_cb(dec, my_close);
eos_audio_decoder_set_seek_cb(dec, my_seek); // optional
dec->name = "MyDecoder";
Seek uses a two-phase fallback: if the decoder provides seek_cb, call it directly; otherwise close + reopen the decoder and read-and-discard sample * bytes_per_frame bytes in 512-frame-aligned chunks (minimum 1024 bytes). This ensures streaming decoders without random access can still seek.
Recording: Double Ring Buffer
Two independent pieces of code handle ring-buffer wrap-around on the recording path:
Recording parameters are fixed at 16kHz, mono, 16-bit. Output is standard RIFF/WAV: a 44-byte header is written at start (size fields set to zero), patched with correct values on stop_recording.
Device Abstraction
Speaker
typedef struct {
int (*init)(void);
int (*deinit)(void);
int (*open)(uint32_t sample_rate, uint8_t channels, uint8_t bits_per_sample);
int (*borrow)(void **p_buf, uint32_t *p_capacity);
int (*enqueue)(void *buf, uint32_t size);
int (*stop)(void);
int (*pause)(void);
int (*resume)(void);
int (*set_volume)(uint8_t volume);
bool (*is_available)(void);
} eos_dev_speaker_ops_t;
eos_dev_speaker_register validates only the six required pointers: open, borrow, enqueue, stop, set_volume, is_available; init/deinit/pause/resume are optional. Registration is one-shot; duplicates return EOS_ERR_ALREADY_EXISTS.
Microphone
typedef struct {
int (*init)(void); // Optional
int (*deinit)(void); // Optional
int (*open)(uint32_t sample_rate, uint8_t channels,
uint8_t bits_per_sample); // Required
int (*close)(void); // Required
int (*start)(void); // Required
int (*stop)(void); // Required
int (*set_gain)(uint8_t gain); // Optional
bool (*is_available)(void); // Required
int (*set_buffer)(uint8_t *buf, uint32_t size); // Required
uint32_t (*get_write_offset)(void); // Required
} eos_dev_microphone_ops_t;
set_buffer registers a ring buffer that DMA writes into (power-of-2 size recommended). get_write_offset must be thread-safe, callable from any context.
enqueue Alignment
enqueue requires size to be frame-aligned — the platform layer should validate that size is a multiple of bytes_per_frame, and may silently truncate or return an error on misalignment. Each platform decides its own strategy.
Save/Restore (Interrupt-Resume Pattern)
For temporary interruptions like incoming calls:
// Interrupt — ownership transfers to caller
void *saved_src;
eos_audio_src_type_t saved_type;
uint32_t saved_pos;
eos_audio_player_save_state(&player, &saved_src, &saved_type, &saved_pos);
// player has forgotten saved_src; you must free() it later
// Restore — player must be in IDLE
eos_audio_player_restore_state(&player, saved_src, saved_type, saved_pos);
// internally calls play() then seek(position)
save_state transfers ownership of p->cached_src (a strdup'd string) to you. The implementation assumes cached_src comes from strdup — verify behavior if you use other source types.
Complete Playback Data Flow
Notes
- The service exposes a single player instance
_player_media.eos_service_audio_get_player()returns the internal pointer for advanced use (e.g., progress tracking), but the header recommends using the service-layer functions to avoid corrupting internal state. eos_service_audio_play_toneis stubbed, returnsEOS_ERR_DEV_OPS_NOT_SUPPORTED. A future implementation could synthesize PCM directly and push it through the player.- Recording parameters are hardcoded at 16kHz mono 16-bit, not configurable at runtime. Extending this requires adding parameter plumbing through the service layer.
save_state/restore_stateassumescached_srcoriginates fromstrdup. Verify correctness if you pass other source types.- Additional player APIs:
eos_audio_player_seek(p, sample)seeks during play/pause;eos_audio_player_get_position(p)/eos_audio_player_get_duration(p)track progress;eos_audio_player_get_sample_rate(p)returns sample rate;eos_audio_player_set_done_callback(p, cb, ud)registers a completion callback. Access these via the pointer fromeos_service_audio_get_player(). - Two file I/O interfaces exist: the WAV decoder uses
eos_fs_*(eos_fs_port.h— low-level FS port), while the recording code useseos_storage_*(eos_service_storage.h— storage service wrapper). Both eventually route toeos_fs_port, but the storage layer adds path validation and the Deferred File Writer (DFW) on top.