AudioManager
MicroPythonOS provides a centralized audio service called AudioManager, inspired by Android's architecture. It manages audio playback and recording across different hardware outputs with priority-based audio focus control.
Overview
AudioManager provides:
- Priority-based audio focus - Higher priority streams interrupt lower priority ones
- Multiple audio devices - I2S digital audio, PWM buzzer, or both
- Background playback/recording - Runs in separate thread
- WAV file support - 8/16/24/32-bit PCM, mono/stereo, auto-upsampling
- WAV recording - 16-bit mono PCM from I2S microphone
- RTTTL ringtone support - Full Ring Tone Text Transfer Language parser
- Thread-safe - Safe for concurrent access
- Hardware-agnostic - Apps work across all platforms without changes
Supported Audio Devices
- I2S Output: Digital audio output for WAV file playback (Fri3d badge, Waveshare board)
- I2S Input: Microphone recording (Fri3d badge only)
- Buzzer: PWM-based tone/ringtone playback (Fri3d badge only)
- Both: Simultaneous I2S and buzzer support
- Null: No audio (desktop/Linux - simulated for testing)
Quick Start
Initialization
AudioManager uses a singleton pattern. Initialize it once at startup with your hardware configuration:
from mpos import AudioManager
# Initialize with I2S and buzzer (typical Fri3d badge setup)
i2s_pins = {'sck': 2, 'ws': 47, 'sd': 16, 'sd_in': 15}
buzzer = machine.PWM(machine.Pin(46))
AudioManager(i2s_pins=i2s_pins, buzzer_instance=buzzer)
# After initialization, use class methods directly (no .get() needed)
AudioManager.play_wav("music.wav")
AudioManager.set_volume(70)
Playing WAV Files
from mpos import Activity, AudioManager
class MusicPlayerActivity(Activity):
def onCreate(self):
# Play a music file (class method - no .get() needed)
success = AudioManager.play_wav(
"M:/sdcard/music/song.wav",
stream_type=AudioManager.STREAM_MUSIC,
volume=80,
on_complete=lambda msg: print(msg)
)
if not success:
print("Playback rejected (higher priority stream active)")
Supported formats: - Encoding: PCM (8/16/24/32-bit) - Channels: Mono or stereo - Sample rate: Any rate (auto-upsampled to ≥22050 Hz)
Playing RTTTL Ringtones
from mpos import AudioManager
# Play notification sound via buzzer
rtttl = "Nokia:d=4,o=5,b=225:8e6,8d6,8f#,8g#,8c#6,8b,d,8p,8b,8a,8c#,8e"
AudioManager.play_rtttl(
rtttl,
stream_type=AudioManager.STREAM_NOTIFICATION
)
RTTTL format example:
name:settings:notes
Nokia:d=4,o=5,b=225:8e6,8d6,8f#,8g#
d=4- Default duration (quarter note)o=5- Default octaveb=225- Beats per minute- Notes:
8e6= 8th note, E in octave 6
Volume Control
from mpos import AudioManager
# Set volume (0-100)
AudioManager.set_volume(70)
# Get current volume
volume = AudioManager.get_volume()
print(f"Current volume: {volume}")
Stopping Playback
from mpos import AudioManager
# Stop currently playing audio
AudioManager.stop()
Recording Audio
AudioManager supports recording audio from an I2S microphone to WAV files.
Basic Recording
from mpos import AudioManager
# Check if microphone is available
if AudioManager.has_microphone():
# Record for 10 seconds
success = AudioManager.record_wav(
file_path="data/my_recording.wav",
duration_ms=10000,
sample_rate=16000,
on_complete=lambda msg: print(msg)
)
if success:
print("Recording started...")
else:
print("Recording failed to start")
Recording Parameters
- file_path: Path to save the WAV file
- duration_ms: Maximum recording duration in milliseconds (default: 60000 = 60 seconds)
- sample_rate: Sample rate in Hz (default: 16000 - good for voice)
- on_complete: Callback function called when recording finishes
Stopping Recording
# Stop recording early
AudioManager.stop()
Recording Constraints
- Cannot record while playing: I2S can only be TX (output) or RX (input) at one time
- Cannot start new recording while recording: Only one recording at a time
- Microphone required:
has_microphone()must returnTrue
# Check recording state
if AudioManager.is_recording():
print("Currently recording...")
# Check playback state
if AudioManager.is_playing():
print("Currently playing...")
Complete Recording Example
from mpos import Activity
from mpos.audio.audiomanager import AudioManager
import lvgl as lv
import time
class SoundRecorderActivity(Activity):
def onCreate(self):
self.screen = lv.obj()
self.is_recording = False
# Status label
self.status = lv.label(self.screen)
self.status.set_text("Ready")
self.status.align(lv.ALIGN.TOP_MID, 0, 20)
# Record button
self.record_btn = lv.button(self.screen)
self.record_btn.set_size(120, 50)
self.record_btn.align(lv.ALIGN.CENTER, 0, 0)
self.record_label = lv.label(self.record_btn)
self.record_label.set_text("Record")
self.record_label.center()
self.record_btn.add_event_cb(self.toggle_recording, lv.EVENT.CLICKED, None)
# Check microphone availability
if not AudioManager.has_microphone():
self.status.set_text("No microphone")
self.record_btn.add_flag(lv.obj.FLAG.HIDDEN)
self.setContentView(self.screen)
def toggle_recording(self, event):
if self.is_recording:
AudioManager.stop()
self.is_recording = False
self.record_label.set_text("Record")
self.status.set_text("Stopped")
else:
# Generate timestamped filename
t = time.localtime()
filename = f"data/recording_{t[0]}{t[1]:02d}{t[2]:02d}_{t[3]:02d}{t[4]:02d}.wav"
success = AudioManager.record_wav(
file_path=filename,
duration_ms=60000, # 60 seconds max
sample_rate=16000,
on_complete=self.on_recording_complete
)
if success:
self.is_recording = True
self.record_label.set_text("Stop")
self.status.set_text("Recording...")
else:
self.status.set_text("Failed to start")
def on_recording_complete(self, message):
# Called from recording thread - update UI safely
self.update_ui_threadsafe_if_foreground(self._update_ui_after_recording, message)
def _update_ui_after_recording(self, message):
self.is_recording = False
self.record_label.set_text("Record")
self.status.set_text(message)
Audio Focus Priority
AudioManager implements a 3-tier priority-based audio focus system inspired by Android:
| Stream Type | Priority | Use Case | Behavior |
|---|---|---|---|
| STREAM_ALARM | 2 (Highest) | Alarms, alerts | Interrupts all other streams |
| STREAM_NOTIFICATION | 1 (Medium) | Notifications, UI sounds | Interrupts music, rejected by alarms |
| STREAM_MUSIC | 0 (Lowest) | Music, podcasts | Interrupted by everything |
Priority Rules
- Higher priority interrupts lower priority: ALARM > NOTIFICATION > MUSIC
- Equal priority is rejected: Can't play two alarms simultaneously
- Lower priority is rejected: Can't start music while alarm is playing
Example: Priority in Action
from mpos import AudioManager
# Start playing music (priority 0)
AudioManager.play_wav("music.wav", stream_type=AudioManager.STREAM_MUSIC)
# Notification sound (priority 1) interrupts music
AudioManager.play_rtttl("beep:d=4:8c", stream_type=AudioManager.STREAM_NOTIFICATION)
# Music stops, notification plays
# Try to play another notification while first is playing
success = AudioManager.play_rtttl("beep:d=4:8d", stream_type=AudioManager.STREAM_NOTIFICATION)
# Returns False - equal priority rejected
# Alarm (priority 2) interrupts notification
AudioManager.play_wav("alarm.wav", stream_type=AudioManager.STREAM_ALARM)
# Notification stops, alarm plays
Hardware Support Matrix
| Board | I2S Output | I2S Microphone | Buzzer | Notes |
|---|---|---|---|---|
| Fri3d 2024 Badge | ✅ | ✅ | ✅ | Full audio support |
| Waveshare ESP32-S3 | ✅ | ❌ | ❌ | I2S output only |
| Linux/macOS | ❌ | ✅ (simulated) | ❌ | Simulated recording for testing |
I2S Output Pins (DAC/Speaker): - BCK (Bit Clock): GPIO 2 - WS (Word Select): GPIO 47 - DOUT (Data Out): GPIO 16
I2S Input Pins (Microphone): - SCLK (Serial Clock): GPIO 17 - WS (Word Select): GPIO 47 (shared with output) - DIN (Data In): GPIO 15
Buzzer Pin: - PWM: GPIO 46 (Fri3d badge only)
!!! note "I2S Limitation" The ESP32 I2S peripheral can only be in TX (output) or RX (input) mode at one time. You cannot play and record simultaneously.
Configuration
Audio device preference is configured in the Settings app under "Advanced Settings":
- Auto-detect: Use available hardware (default)
- I2S (Digital Audio): Digital audio only
- Buzzer (PWM Tones): Tones/ringtones only
- Both I2S and Buzzer: Use both devices
- Disabled: No audio
⚠️ Important: Changing the audio device requires a restart to take effect.
How Auto-Detect Works
- Checks if I2S hardware is available
- Checks if Buzzer hardware is available
- Selects the best available option:
- Both devices available → Use "Both"
- Only I2S → Use "I2S"
- Only Buzzer → Use "Buzzer"
- Neither → Use "Null" (no audio)
Complete Example: Music Player with Controls
from mpos import Activity, AudioManager
import lvgl as lv
class SimpleMusicPlayerActivity(Activity):
def onCreate(self):
self.screen = lv.obj()
# Play button
play_btn = lv.button(self.screen)
play_btn.set_size(100, 50)
play_btn.set_pos(10, 50)
play_label = lv.label(play_btn)
play_label.set_text("Play")
play_label.center()
play_btn.add_event_cb(lambda e: self.play_music(), lv.EVENT.CLICKED, None)
# Stop button
stop_btn = lv.button(self.screen)
stop_btn.set_size(100, 50)
stop_btn.set_pos(120, 50)
stop_label = lv.label(stop_btn)
stop_label.set_text("Stop")
stop_label.center()
stop_btn.add_event_cb(lambda e: AudioManager.stop(), lv.EVENT.CLICKED, None)
# Volume slider
volume_label = lv.label(self.screen)
volume_label.set_text("Volume:")
volume_label.set_pos(10, 120)
volume_slider = lv.slider(self.screen)
volume_slider.set_size(200, 10)
volume_slider.set_pos(10, 150)
volume_slider.set_range(0, 100)
volume_slider.set_value(AudioManager.get_volume(), False)
volume_slider.add_event_cb(
lambda e: AudioManager.set_volume(volume_slider.get_value()),
lv.EVENT.VALUE_CHANGED,
None
)
self.setContentView(self.screen)
def play_music(self):
success = AudioManager.play_wav(
"M:/sdcard/music/song.wav",
stream_type=AudioManager.STREAM_MUSIC,
volume=AudioManager.get_volume(),
on_complete=lambda msg: print(f"Playback finished: {msg}")
)
if not success:
print("Playback rejected - higher priority audio active")
API Reference
Playback Functions
play_wav(path, stream_type=STREAM_MUSIC, volume=None, on_complete=None)
Play a WAV file.
- Parameters:
path(str): Path to WAV file (e.g.,"M:/sdcard/music/song.wav")stream_type(int): Stream type constant (STREAM_ALARM, STREAM_NOTIFICATION, STREAM_MUSIC)volume(int, optional): Volume 0-100. If None, uses current volume-
on_complete(callable, optional): Callback function called when playback completes -
Returns:
bool-Trueif playback started,Falseif rejected (higher/equal priority active)
play_rtttl(rtttl, stream_type=STREAM_NOTIFICATION)
Play an RTTTL ringtone.
- Parameters:
rtttl(str): RTTTL format string (e.g.,"Nokia:d=4,o=5,b=225:8e6,8d6")-
stream_type(int): Stream type constant -
Returns:
bool-Trueif playback started,Falseif rejected
Recording Functions
record_wav(file_path, duration_ms=None, on_complete=None, sample_rate=16000)
Record audio from I2S microphone to WAV file.
- Parameters:
file_path(str): Path to save WAV file (e.g.,"data/recording.wav")duration_ms(int, optional): Recording duration in milliseconds. Default: 60000 (60 seconds)on_complete(callable, optional): Callback function called when recording finishes-
sample_rate(int, optional): Sample rate in Hz. Default: 16000 (good for voice) -
Returns:
bool-Trueif recording started,Falseif rejected -
Rejection reasons:
- No microphone available (
has_microphone()returnsFalse) - Currently playing audio (I2S can only be TX or RX)
- Already recording
is_recording()
Check if audio is currently being recorded.
- Returns:
bool-Trueif recording active,Falseotherwise
has_microphone()
Check if I2S microphone is available for recording.
- Returns:
bool-Trueif microphone configured,Falseotherwise
Volume and Control Functions
set_volume(volume)
Set playback volume.
- Parameters:
volume(int): Volume level 0-100
get_volume()
Get current playback volume.
- Returns:
int- Current volume (0-100)
stop()
Stop currently playing audio or recording.
is_playing()
Check if audio is currently playing.
- Returns:
bool-Trueif playback active,Falseotherwise
Hardware Detection Functions
has_i2s()
Check if I2S audio output is available for WAV playback.
- Returns:
bool-Trueif I2S configured,Falseotherwise
has_buzzer()
Check if buzzer is available for RTTTL playback.
- Returns:
bool-Trueif buzzer configured,Falseotherwise
Stream Type Constants
AudioManager.STREAM_ALARM- Highest priority (value: 2)AudioManager.STREAM_NOTIFICATION- Medium priority (value: 1)AudioManager.STREAM_MUSIC- Lowest priority (value: 0)
Troubleshooting
Playback Rejected
Symptom: play_wav() or play_rtttl() returns False, no sound plays
Cause: Higher or equal priority stream is currently active
Solution:
1. Check if higher priority audio is playing
2. Wait for higher priority stream to complete
3. Use AudioManager.stop() to force stop current playback (use sparingly)
4. Use equal or higher priority stream type
from mpos import AudioManager
# Check if playback was rejected
success = AudioManager.play_wav("sound.wav", stream_type=AudioManager.STREAM_MUSIC)
if not success:
print("Higher priority audio is playing")
# Option 1: Wait and retry
# Option 2: Stop current playback (if appropriate)
AudioManager.stop()
AudioManager.play_wav("sound.wav", stream_type=AudioManager.STREAM_MUSIC)
WAV File Not Playing
Symptom: File exists but doesn't play, or plays with distortion
Cause: Unsupported WAV format
Requirements: - Encoding: PCM only (not MP3, AAC, or other compressed formats) - Bit depth: 8, 16, 24, or 32-bit - Channels: Mono or stereo - Sample rate: Any (auto-upsampled to ≥22050 Hz)
Solution: Convert WAV file to supported format using ffmpeg or audacity:
# Convert to 16-bit PCM, 44100 Hz, stereo
ffmpeg -i input.wav -acodec pcm_s16le -ar 44100 -ac 2 output.wav
# Convert to 16-bit PCM, 22050 Hz, mono (smaller file)
ffmpeg -i input.wav -acodec pcm_s16le -ar 22050 -ac 1 output.wav
No Sound on Device
Symptom: Playback succeeds but no sound comes out
Possible causes:
-
Volume set to 0 ```python from mpos import AudioManager
AudioManager.set_volume(50) # Set to 50% ```
-
Wrong audio device selected
- Check Settings → Advanced Settings → Audio Device
- Try "Auto-detect" or manually select device
- Restart required after changing audio device
-
Hardware not available (desktop)
python # Desktop builds have no audio hardware # AudioManager will return success but produce no sound -
I2C/Speaker not connected (Fri3d badge)
- Check if speaker is properly connected
- Verify I2S pins are not used by other peripherals
Audio Cuts Out or Stutters
Symptom: Playback starts but stops unexpectedly or has gaps
Cause: Higher priority stream interrupting, or insufficient memory
Solution:
1. Check for interrupting streams:
python
# Avoid notifications during music playback
# Or use callbacks to resume after interruption
-
Reduce memory usage:
- Close unused apps
- Use lower bitrate WAV files (22050 Hz instead of 44100 Hz)
- Free up memory with
gc.collect()
-
Check SD card speed:
- Use Class 10 or faster SD card for smooth WAV playback
Restart Required After Configuration Change
Symptom: Changed audio device in Settings but still using old device
Cause: Audio device is initialized at boot time
Solution: Restart the device after changing audio device preference
# After changing setting, restart is required
# Settings app should display a message: "Restart required to apply changes"
RTTTL Not Playing on Waveshare
Symptom: RTTTL tones don't play on Waveshare board
Cause: Waveshare board has no buzzer (PWM speaker)
Solution: - Use WAV files instead of RTTTL on Waveshare - RTTTL requires hardware buzzer (Fri3d badge only) - I2S cannot produce RTTTL tones
Recording Not Working
Symptom: record_wav() returns False or no audio recorded
Possible causes:
-
No microphone available ```python from mpos import AudioManager
if not AudioManager.has_microphone(): print("No microphone on this device") ```
-
Currently playing audio ```python from mpos import AudioManager import time
# I2S can only be TX or RX, not both if AudioManager.is_playing(): AudioManager.stop() time.sleep_ms(100) # Wait for cleanup AudioManager.record_wav("recording.wav") ```
-
Already recording ```python from mpos import AudioManager
if AudioManager.is_recording(): print("Already recording") ```
-
Wrong board - Waveshare doesn't have a microphone
Recording Quality Issues
Symptom: Recording sounds distorted or has noise
Solutions:
- Use appropriate sample rate: 16000 Hz is good for voice, 44100 Hz for music
- Check microphone placement: Keep microphone away from noise sources
- Verify I2S pin configuration: Check board file for correct pin assignments
Performance Tips
Optimizing WAV Files
- File size: Use 22050 Hz instead of 44100 Hz to reduce file size by 50%
- Channels: Use mono instead of stereo if positional audio isn't needed
- Bit depth: Use 16-bit for good quality with reasonable size
# Small file, good quality (recommended)
ffmpeg -i input.wav -acodec pcm_s16le -ar 22050 -ac 1 output.wav
# High quality, larger file
ffmpeg -i input.wav -acodec pcm_s16le -ar 44100 -ac 2 output.wav
Background Playback
AudioManager runs in a separate thread, so playback doesn't block the UI:
from mpos import AudioManager
# This doesn't block - returns immediately
AudioManager.play_wav("long_song.wav", stream_type=AudioManager.STREAM_MUSIC)
# UI remains responsive while audio plays
# Use on_complete callback to know when finished
AudioManager.play_wav(
"song.wav",
on_complete=lambda msg: print("Playback finished")
)
Memory Management
- WAV files are streamed from SD card (not loaded into RAM)
- Buffers are allocated during playback and freed after
- Multiple simultaneous streams not supported (priority system prevents this)
Desktop Testing
On desktop builds (Linux/macOS), AudioManager provides simulated recording for testing:
- Microphone simulation: Generates a 440Hz sine wave instead of real audio
- WAV file generation: Creates valid WAV files that can be played back
- Real-time simulation: Recording runs at realistic speed
This allows testing the Sound Recorder app and other recording features without hardware.
from mpos import AudioManager
# Desktop simulation is automatic when machine.I2S is not available
# The generated WAV file contains a 440Hz tone
AudioManager.record_wav("test.wav", duration_ms=5000)
# Creates a valid 5-second WAV file with 440Hz sine wave
See Also
- Creating Apps - Learn how to create MicroPythonOS apps
- WidgetAnimator - Smooth UI animations
- LightsManager - LED control for visual feedback
- SharedPreferences - Store audio preferences