embedded-audio
no_std duty-modulated PWM audio for Cortex-M / RISC-V MCUs: effect banks, tiered DSP, multi-voice mixing, and a full desktop DAW & Chiptune Audio Studio.
Workspace Structure
crates/embedded-audio: Coreno_stdsynthesis, decoding (ADPCM, PCM8), envelopes, and PWM/ΣΔ output pipelines.crates/embedded-audio-codegen: Rustno_stdsong & bank generators, C/C++ headers, and.eafbinary format compilers.crates/embedded-audio-live: Hardware-in-the-loop streaming protocol over USB CDC / Serial UART.crates/embedded-audio-studio: Interactive desktop DAW with modern Piano-Roll & Step-Sequencer, Synth Lab, real-time Oscilloscope, PWM/ΣΔ pulse density stream visualizer, FFT spectrum analyzer, and MCU profiler.
Running the DAW Studio
Architecture
Flash (EAFX bank) → decode (Tier A/B/C) → 2 voices + ADSR → mix → ΣΔ → PWM duty
Call AudioEngine::tick() once per sample at AudioConfig::sample_rate_hz (default 16 kHz). Drive a fixed PWM carrier (default 32 kHz timer) by writing the returned duty compare value.
Quick start (firmware)
use *;
static BANK: & = include_bytes!;
let bank = parse?;
let mut engine = new;
engine.set_bank;
engine.play?;
// Timer ISR @ 16 kHz:
let duty = engine.tick;
pwm.set_duty;
Effect bank (EAFX v2)
| Offset | Field |
|---|---|
| 0..4 | Magic EAFX |
| 4 | Version 2 |
| 5..6 | Effect count (u16 LE) |
| 7..8 | Sample rate Hz (u16 LE) |
| 9 | Reserved |
| 10+ | Directory entries (16 bytes × N) |
| … | Payload bytes |
Effect kinds
| ID | Tier | Payload |
|---|---|---|
Tone |
A | empty — param0 = Hz, param1 = duration ms |
Wavetable |
A | 256-byte table |
Fm |
A | empty — param0 = carrier Hz, param1 = mod ratio ×100 |
Pcm8 |
B | raw 8-bit mono |
Adpcm |
B | 4-byte IMA header + nibbles |
SigmaDeltaBits |
C | MSB-first packed bits |
Host baking
Multi-effect bank:
Legacy single effect: eaf-bake --kind pcm8 --id 1 sound.raw -o bank.bin
Input for pcm8 / adpcm is unsigned 8-bit mono raw (center 128), band-limited in your DAW before export.
wavetable requires a 256-byte table file; param0 in the bank is playback Hz.
Preview (host)
Uses tick_pcm() — the same mix/envelope path as firmware, before PWM mapping.
Peripheral DMA & Format Buffering
embedded-audio supports arbitrary hardware peripherals (PWM timers, 8/12/16-bit DACs, I2S / SAI audio codecs) via peripheral-agnostic buffer fillers:
// Fill DMA buffers for different peripherals:
engine.fill_duty_buffer; // PWM timers (0..=period)
engine.fill_dac_u8_buffer; // 8-bit DACs (0..=255)
engine.fill_dac_u12_buffer; // 12-bit DACs (0..=4095, e.g. STM32 DAC1)
engine.fill_dac_u16_buffer; // 16-bit DACs (0..=65535)
engine.fill_pcm_i16_buffer; // Signed 16-bit PCM (-32768..=32767)
engine.fill_stereo_i16_buffer;// Interleaved stereo i16 for I2S/SAI
Embassy Async DMA Integration
Use DmaDoubleBuffer for zero-allocation ping-pong DMA streaming with Embassy async drivers:
use *;
// 256 samples per half-buffer
let mut dma_pump = new;
loop
See examples/embassy_stm32u585.rs for a complete Embassy STM32U585CIU6 hardware example.
Wavetables & Synthesizers
Play standard synthesized waveforms or custom 256-sample wavetables:
// Standard built-in wavetables: SINE_TABLE, TRIANGLE_TABLE, SAW_TABLE, SQUARE_TABLE, PULSE_25_TABLE
engine.play_wavetable?;
// Custom fixed-point wavetable generator:
let custom_table = generate_wavetable_fixed;
engine.play_wavetable?;
Defaults
| Constant | Value |
|---|---|
DEFAULT_SAMPLE_RATE_HZ |
16_000 |
DEFAULT_PWM_CARRIER_HZ |
32_000 |
DEFAULT_PWM_PERIOD |
1000 (set from your timer clock) |
| Output shaping | DutyMode::SigmaDelta |
Real-time DSP & Analysis (dsp feature)
Enable the optional dsp feature to integrate zero-allocation digital signal processing algorithms powered by embedded-dsp (using libm for #![no_std] targets):
[]
= { = "0.2.1", = ["dsp"] }
use *;
use Waveform;
let mut engine = from_sample_rate;
engine.play_tone;
// 1. Equalize or filter engine audio with a Biquad filter (Lowpass, Highpass, Bandpass, Notch)
let mut filter = lowpass;
let mut frame = ;
engine.fill_pcm_f32_buffer;
filter.process_buffer;
// 2. Measure audio signal statistics (RMS, Peak, Power, Mean, Variance)
let stats = measure;
// stats.rms, stats.peak, stats.power, etc.
// 3. FFT Spectrum Analysis & Pitch / Dominant Frequency Detection
let = find_peak_frequency;
Features
| Feature | Purpose |
|---|---|
std |
Host ADPCM encoder + eaf-bake binary |
fm |
Optional FM-buzzer backend (tick_fm, Markham profile) for bring-up only |
dsp |
Optional real-time DSP (BiquadAudioFilter, AudioSpectrumAnalyzer, AudioMeter, AudioLmsFilter) via embedded-dsp |
RAM budget (typical)
- 2 voices + ADSR + ΣΔ state: < 512 B
- No heap; bank parsed from flash by reference
License
The contents of this repository are dual-licensed under the MIT OR Apache 2.0
License. That means you can choose either the MIT license or the Apache 2.0
license when you re-use this code. See LICENSE-MIT or
LICENSE-APACHE for more information on each specific
license.