Skip to content

Commit afe8434

Browse files
AudioFlinger: eliminate thread by using TaskManager (asyncio)
Also simplify, and move all testing mocks to a dedicated file.
1 parent 23a8f92 commit afe8434

File tree

11 files changed

+1004
-1212
lines changed

11 files changed

+1004
-1212
lines changed
Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
# AudioFlinger - Centralized Audio Management Service for MicroPythonOS
22
# Android-inspired audio routing with priority-based audio focus
3+
# Simple routing: play_wav() -> I2S, play_rtttl() -> buzzer
34

45
from . import audioflinger
56

67
# Re-export main API
78
from .audioflinger import (
8-
# Device types
9-
DEVICE_NULL,
10-
DEVICE_I2S,
11-
DEVICE_BUZZER,
12-
DEVICE_BOTH,
13-
14-
# Stream types
9+
# Stream types (for priority-based audio focus)
1510
STREAM_MUSIC,
1611
STREAM_NOTIFICATION,
1712
STREAM_ALARM,
@@ -25,17 +20,14 @@
2520
resume,
2621
set_volume,
2722
get_volume,
28-
get_device_type,
2923
is_playing,
24+
25+
# Hardware availability checks
26+
has_i2s,
27+
has_buzzer,
3028
)
3129

3230
__all__ = [
33-
# Device types
34-
'DEVICE_NULL',
35-
'DEVICE_I2S',
36-
'DEVICE_BUZZER',
37-
'DEVICE_BOTH',
38-
3931
# Stream types
4032
'STREAM_MUSIC',
4133
'STREAM_NOTIFICATION',
@@ -50,6 +42,7 @@
5042
'resume',
5143
'set_volume',
5244
'get_volume',
53-
'get_device_type',
5445
'is_playing',
46+
'has_i2s',
47+
'has_buzzer',
5548
]
Lines changed: 46 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,59 @@
11
# AudioFlinger - Core Audio Management Service
22
# Centralized audio routing with priority-based audio focus (Android-inspired)
33
# Supports I2S (digital audio) and PWM buzzer (tones/ringtones)
4+
#
5+
# Simple routing: play_wav() -> I2S, play_rtttl() -> buzzer
6+
# Uses TaskManager (asyncio) for non-blocking background playback
47

5-
# Device type constants
6-
DEVICE_NULL = 0 # No audio hardware (desktop fallback)
7-
DEVICE_I2S = 1 # Digital audio output (WAV playback)
8-
DEVICE_BUZZER = 2 # PWM buzzer (tones/RTTTL)
9-
DEVICE_BOTH = 3 # Both I2S and buzzer available
8+
from mpos.task_manager import TaskManager
109

1110
# Stream type constants (priority order: higher number = higher priority)
1211
STREAM_MUSIC = 0 # Background music (lowest priority)
1312
STREAM_NOTIFICATION = 1 # Notification sounds (medium priority)
1413
STREAM_ALARM = 2 # Alarms/alerts (highest priority)
1514

1615
# Module-level state (singleton pattern, follows battery_voltage.py)
17-
_device_type = DEVICE_NULL
1816
_i2s_pins = None # I2S pin configuration dict (created per-stream)
1917
_buzzer_instance = None # PWM buzzer instance
2018
_current_stream = None # Currently playing stream
19+
_current_task = None # Currently running playback task
2120
_volume = 50 # System volume (0-100)
22-
_stream_lock = None # Thread lock for stream management
2321

2422

25-
def init(device_type, i2s_pins=None, buzzer_instance=None):
23+
def init(i2s_pins=None, buzzer_instance=None):
2624
"""
2725
Initialize AudioFlinger with hardware configuration.
2826
2927
Args:
30-
device_type: One of DEVICE_NULL, DEVICE_I2S, DEVICE_BUZZER, DEVICE_BOTH
31-
i2s_pins: Dict with 'sck', 'ws', 'sd' pin numbers (for I2S devices)
32-
buzzer_instance: PWM instance for buzzer (for buzzer devices)
28+
i2s_pins: Dict with 'sck', 'ws', 'sd' pin numbers (for I2S/WAV playback)
29+
buzzer_instance: PWM instance for buzzer (for RTTTL playback)
3330
"""
34-
global _device_type, _i2s_pins, _buzzer_instance, _stream_lock
31+
global _i2s_pins, _buzzer_instance
3532

36-
_device_type = device_type
3733
_i2s_pins = i2s_pins
3834
_buzzer_instance = buzzer_instance
3935

40-
# Initialize thread lock for stream management
41-
try:
42-
import _thread
43-
_stream_lock = _thread.allocate_lock()
44-
except ImportError:
45-
# Desktop mode - no threading support
46-
_stream_lock = None
36+
# Build status message
37+
capabilities = []
38+
if i2s_pins:
39+
capabilities.append("I2S (WAV)")
40+
if buzzer_instance:
41+
capabilities.append("Buzzer (RTTTL)")
42+
43+
if capabilities:
44+
print(f"AudioFlinger initialized: {', '.join(capabilities)}")
45+
else:
46+
print("AudioFlinger initialized: No audio hardware")
47+
48+
49+
def has_i2s():
50+
"""Check if I2S audio is available for WAV playback."""
51+
return _i2s_pins is not None
4752

48-
device_names = {
49-
DEVICE_NULL: "NULL (no audio)",
50-
DEVICE_I2S: "I2S (digital audio)",
51-
DEVICE_BUZZER: "Buzzer (PWM tones)",
52-
DEVICE_BOTH: "Both (I2S + Buzzer)"
53-
}
5453

55-
print(f"AudioFlinger initialized: {device_names.get(device_type, 'Unknown')}")
54+
def has_buzzer():
55+
"""Check if buzzer is available for RTTTL playback."""
56+
return _buzzer_instance is not None
5657

5758

5859
def _check_audio_focus(stream_type):
@@ -85,35 +86,27 @@ def _check_audio_focus(stream_type):
8586
return True
8687

8788

88-
def _playback_thread(stream):
89+
async def _playback_coroutine(stream):
8990
"""
90-
Background thread function for audio playback.
91+
Async coroutine for audio playback.
9192
9293
Args:
9394
stream: Stream instance (WAVStream or RTTTLStream)
9495
"""
95-
global _current_stream
96+
global _current_stream, _current_task
9697

97-
# Acquire lock and set as current stream
98-
if _stream_lock:
99-
_stream_lock.acquire()
10098
_current_stream = stream
101-
if _stream_lock:
102-
_stream_lock.release()
10399

104100
try:
105-
# Run playback (blocks until complete or stopped)
106-
stream.play()
101+
# Run async playback
102+
await stream.play_async()
107103
except Exception as e:
108104
print(f"AudioFlinger: Playback error: {e}")
109105
finally:
110106
# Clear current stream
111-
if _stream_lock:
112-
_stream_lock.acquire()
113107
if _current_stream == stream:
114108
_current_stream = None
115-
if _stream_lock:
116-
_stream_lock.release()
109+
_current_task = None
117110

118111

119112
def play_wav(file_path, stream_type=STREAM_MUSIC, volume=None, on_complete=None):
@@ -129,29 +122,19 @@ def play_wav(file_path, stream_type=STREAM_MUSIC, volume=None, on_complete=None)
129122
Returns:
130123
bool: True if playback started, False if rejected or unavailable
131124
"""
132-
if _device_type not in (DEVICE_I2S, DEVICE_BOTH):
133-
print("AudioFlinger: play_wav() failed - no I2S device available")
134-
return False
125+
global _current_task
135126

136127
if not _i2s_pins:
137-
print("AudioFlinger: play_wav() failed - I2S pins not configured")
128+
print("AudioFlinger: play_wav() failed - I2S not configured")
138129
return False
139130

140131
# Check audio focus
141-
if _stream_lock:
142-
_stream_lock.acquire()
143-
can_start = _check_audio_focus(stream_type)
144-
if _stream_lock:
145-
_stream_lock.release()
146-
147-
if not can_start:
132+
if not _check_audio_focus(stream_type):
148133
return False
149134

150-
# Create stream and start playback in background thread
135+
# Create stream and start playback as async task
151136
try:
152137
from mpos.audio.stream_wav import WAVStream
153-
import _thread
154-
import mpos.apps
155138

156139
stream = WAVStream(
157140
file_path=file_path,
@@ -161,8 +144,7 @@ def play_wav(file_path, stream_type=STREAM_MUSIC, volume=None, on_complete=None)
161144
on_complete=on_complete
162145
)
163146

164-
_thread.stack_size(mpos.apps.good_stack_size())
165-
_thread.start_new_thread(_playback_thread, (stream,))
147+
_current_task = TaskManager.create_task(_playback_coroutine(stream))
166148
return True
167149

168150
except Exception as e:
@@ -183,29 +165,19 @@ def play_rtttl(rtttl_string, stream_type=STREAM_NOTIFICATION, volume=None, on_co
183165
Returns:
184166
bool: True if playback started, False if rejected or unavailable
185167
"""
186-
if _device_type not in (DEVICE_BUZZER, DEVICE_BOTH):
187-
print("AudioFlinger: play_rtttl() failed - no buzzer device available")
188-
return False
168+
global _current_task
189169

190170
if not _buzzer_instance:
191-
print("AudioFlinger: play_rtttl() failed - buzzer not initialized")
171+
print("AudioFlinger: play_rtttl() failed - buzzer not configured")
192172
return False
193173

194174
# Check audio focus
195-
if _stream_lock:
196-
_stream_lock.acquire()
197-
can_start = _check_audio_focus(stream_type)
198-
if _stream_lock:
199-
_stream_lock.release()
200-
201-
if not can_start:
175+
if not _check_audio_focus(stream_type):
202176
return False
203177

204-
# Create stream and start playback in background thread
178+
# Create stream and start playback as async task
205179
try:
206180
from mpos.audio.stream_rtttl import RTTTLStream
207-
import _thread
208-
import mpos.apps
209181

210182
stream = RTTTLStream(
211183
rtttl_string=rtttl_string,
@@ -215,8 +187,7 @@ def play_rtttl(rtttl_string, stream_type=STREAM_NOTIFICATION, volume=None, on_co
215187
on_complete=on_complete
216188
)
217189

218-
_thread.stack_size(mpos.apps.good_stack_size())
219-
_thread.start_new_thread(_playback_thread, (stream,))
190+
_current_task = TaskManager.create_task(_playback_coroutine(stream))
220191
return True
221192

222193
except Exception as e:
@@ -226,60 +197,38 @@ def play_rtttl(rtttl_string, stream_type=STREAM_NOTIFICATION, volume=None, on_co
226197

227198
def stop():
228199
"""Stop current audio playback."""
229-
global _current_stream
230-
231-
if _stream_lock:
232-
_stream_lock.acquire()
200+
global _current_stream, _current_task
233201

234202
if _current_stream:
235203
_current_stream.stop()
236204
print("AudioFlinger: Playback stopped")
237205
else:
238206
print("AudioFlinger: No playback to stop")
239207

240-
if _stream_lock:
241-
_stream_lock.release()
242-
243208

244209
def pause():
245210
"""
246211
Pause current audio playback (if supported by stream).
247212
Note: Most streams don't support pause, only stop.
248213
"""
249-
global _current_stream
250-
251-
if _stream_lock:
252-
_stream_lock.acquire()
253-
254214
if _current_stream and hasattr(_current_stream, 'pause'):
255215
_current_stream.pause()
256216
print("AudioFlinger: Playback paused")
257217
else:
258218
print("AudioFlinger: Pause not supported or no playback active")
259219

260-
if _stream_lock:
261-
_stream_lock.release()
262-
263220

264221
def resume():
265222
"""
266223
Resume paused audio playback (if supported by stream).
267224
Note: Most streams don't support resume, only play.
268225
"""
269-
global _current_stream
270-
271-
if _stream_lock:
272-
_stream_lock.acquire()
273-
274226
if _current_stream and hasattr(_current_stream, 'resume'):
275227
_current_stream.resume()
276228
print("AudioFlinger: Playback resumed")
277229
else:
278230
print("AudioFlinger: Resume not supported or no playback active")
279231

280-
if _stream_lock:
281-
_stream_lock.release()
282-
283232

284233
def set_volume(volume):
285234
"""
@@ -304,29 +253,11 @@ def get_volume():
304253
return _volume
305254

306255

307-
def get_device_type():
308-
"""
309-
Get configured audio device type.
310-
311-
Returns:
312-
int: Device type (DEVICE_NULL, DEVICE_I2S, DEVICE_BUZZER, DEVICE_BOTH)
313-
"""
314-
return _device_type
315-
316-
317256
def is_playing():
318257
"""
319258
Check if audio is currently playing.
320259
321260
Returns:
322261
bool: True if playback active, False otherwise
323262
"""
324-
if _stream_lock:
325-
_stream_lock.acquire()
326-
327-
result = _current_stream is not None and _current_stream.is_playing()
328-
329-
if _stream_lock:
330-
_stream_lock.release()
331-
332-
return result
263+
return _current_stream is not None and _current_stream.is_playing()

internal_filesystem/lib/mpos/audio/stream_rtttl.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# RTTTLStream - RTTTL Ringtone Playback Stream for AudioFlinger
22
# Ring Tone Text Transfer Language parser and player
3-
# Ported from Fri3d Camp 2024 Badge firmware
3+
# Uses async playback with TaskManager for non-blocking operation
44

55
import math
6-
import time
6+
7+
from mpos.task_manager import TaskManager
78

89

910
class RTTTLStream:
@@ -179,8 +180,8 @@ def _notes(self):
179180

180181
yield freq, msec
181182

182-
def play(self):
183-
"""Play RTTTL tune via buzzer (runs in background thread)."""
183+
async def play_async(self):
184+
"""Play RTTTL tune via buzzer (runs as TaskManager task)."""
184185
self._is_playing = True
185186

186187
# Calculate exponential duty cycle for perceptually linear volume
@@ -212,9 +213,10 @@ def play(self):
212213
self.buzzer.duty_u16(duty)
213214

214215
# Play for 90% of duration, silent for 10% (note separation)
215-
time.sleep_ms(int(msec * 0.9))
216+
# Use async sleep to allow other tasks to run
217+
await TaskManager.sleep_ms(int(msec * 0.9))
216218
self.buzzer.duty_u16(0)
217-
time.sleep_ms(int(msec * 0.1))
219+
await TaskManager.sleep_ms(int(msec * 0.1))
218220

219221
print(f"RTTTLStream: Finished playing '{self.name}'")
220222
if self.on_complete:

0 commit comments

Comments
 (0)