Skip to content

Commit e64b475

Browse files
AudioFlinger: revert to threaded method
The TaskManager (asyncio) was jittery when under heavy CPU load.
1 parent 4836db5 commit e64b475

File tree

6 files changed

+96
-42
lines changed

6 files changed

+96
-42
lines changed

internal_filesystem/lib/mpos/audio/audioflinger.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
# Supports I2S (digital audio) and PWM buzzer (tones/ringtones)
44
#
55
# Simple routing: play_wav() -> I2S, play_rtttl() -> buzzer
6-
# Uses TaskManager (asyncio) for non-blocking background playback
6+
# Uses _thread for non-blocking background playback (separate thread from UI)
77

8-
from mpos.task_manager import TaskManager
8+
import _thread
9+
import mpos.apps
910

1011
# Stream type constants (priority order: higher number = higher priority)
1112
STREAM_MUSIC = 0 # Background music (lowest priority)
@@ -16,7 +17,6 @@
1617
_i2s_pins = None # I2S pin configuration dict (created per-stream)
1718
_buzzer_instance = None # PWM buzzer instance
1819
_current_stream = None # Currently playing stream
19-
_current_task = None # Currently running playback task
2020
_volume = 50 # System volume (0-100)
2121

2222

@@ -86,27 +86,27 @@ def _check_audio_focus(stream_type):
8686
return True
8787

8888

89-
async def _playback_coroutine(stream):
89+
def _playback_thread(stream):
9090
"""
91-
Async coroutine for audio playback.
91+
Thread function for audio playback.
92+
Runs in a separate thread to avoid blocking the UI.
9293
9394
Args:
9495
stream: Stream instance (WAVStream or RTTTLStream)
9596
"""
96-
global _current_stream, _current_task
97+
global _current_stream
9798

9899
_current_stream = stream
99100

100101
try:
101-
# Run async playback
102-
await stream.play_async()
102+
# Run synchronous playback in this thread
103+
stream.play()
103104
except Exception as e:
104105
print(f"AudioFlinger: Playback error: {e}")
105106
finally:
106107
# Clear current stream
107108
if _current_stream == stream:
108109
_current_stream = None
109-
_current_task = None
110110

111111

112112
def play_wav(file_path, stream_type=STREAM_MUSIC, volume=None, on_complete=None):
@@ -122,8 +122,6 @@ def play_wav(file_path, stream_type=STREAM_MUSIC, volume=None, on_complete=None)
122122
Returns:
123123
bool: True if playback started, False if rejected or unavailable
124124
"""
125-
global _current_task
126-
127125
if not _i2s_pins:
128126
print("AudioFlinger: play_wav() failed - I2S not configured")
129127
return False
@@ -132,7 +130,7 @@ def play_wav(file_path, stream_type=STREAM_MUSIC, volume=None, on_complete=None)
132130
if not _check_audio_focus(stream_type):
133131
return False
134132

135-
# Create stream and start playback as async task
133+
# Create stream and start playback in separate thread
136134
try:
137135
from mpos.audio.stream_wav import WAVStream
138136

@@ -144,7 +142,8 @@ def play_wav(file_path, stream_type=STREAM_MUSIC, volume=None, on_complete=None)
144142
on_complete=on_complete
145143
)
146144

147-
_current_task = TaskManager.create_task(_playback_coroutine(stream))
145+
_thread.stack_size(mpos.apps.good_stack_size())
146+
_thread.start_new_thread(_playback_thread, (stream,))
148147
return True
149148

150149
except Exception as e:
@@ -165,8 +164,6 @@ def play_rtttl(rtttl_string, stream_type=STREAM_NOTIFICATION, volume=None, on_co
165164
Returns:
166165
bool: True if playback started, False if rejected or unavailable
167166
"""
168-
global _current_task
169-
170167
if not _buzzer_instance:
171168
print("AudioFlinger: play_rtttl() failed - buzzer not configured")
172169
return False
@@ -175,7 +172,7 @@ def play_rtttl(rtttl_string, stream_type=STREAM_NOTIFICATION, volume=None, on_co
175172
if not _check_audio_focus(stream_type):
176173
return False
177174

178-
# Create stream and start playback as async task
175+
# Create stream and start playback in separate thread
179176
try:
180177
from mpos.audio.stream_rtttl import RTTTLStream
181178

@@ -187,7 +184,8 @@ def play_rtttl(rtttl_string, stream_type=STREAM_NOTIFICATION, volume=None, on_co
187184
on_complete=on_complete
188185
)
189186

190-
_current_task = TaskManager.create_task(_playback_coroutine(stream))
187+
_thread.stack_size(mpos.apps.good_stack_size())
188+
_thread.start_new_thread(_playback_thread, (stream,))
191189
return True
192190

193191
except Exception as e:
@@ -197,7 +195,7 @@ def play_rtttl(rtttl_string, stream_type=STREAM_NOTIFICATION, volume=None, on_co
197195

198196
def stop():
199197
"""Stop current audio playback."""
200-
global _current_stream, _current_task
198+
global _current_stream
201199

202200
if _current_stream:
203201
_current_stream.stop()

internal_filesystem/lib/mpos/audio/stream_rtttl.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
# RTTTLStream - RTTTL Ringtone Playback Stream for AudioFlinger
22
# Ring Tone Text Transfer Language parser and player
3-
# Uses async playback with TaskManager for non-blocking operation
3+
# Uses synchronous playback in a separate thread for non-blocking operation
44

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

98

109
class RTTTLStream:
@@ -180,8 +179,8 @@ def _notes(self):
180179

181180
yield freq, msec
182181

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

187186
# Calculate exponential duty cycle for perceptually linear volume
@@ -213,10 +212,10 @@ async def play_async(self):
213212
self.buzzer.duty_u16(duty)
214213

215214
# Play for 90% of duration, silent for 10% (note separation)
216-
# Use async sleep to allow other tasks to run
217-
await TaskManager.sleep_ms(int(msec * 0.9))
215+
# Blocking sleep is OK - we're in a separate thread
216+
time.sleep_ms(int(msec * 0.9))
218217
self.buzzer.duty_u16(0)
219-
await TaskManager.sleep_ms(int(msec * 0.1))
218+
time.sleep_ms(int(msec * 0.1))
220219

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

internal_filesystem/lib/mpos/audio/stream_wav.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
# WAVStream - WAV File Playback Stream for AudioFlinger
22
# Supports 8/16/24/32-bit PCM, mono+stereo, auto-upsampling, volume control
3-
# Uses async playback with TaskManager for non-blocking operation
3+
# Uses synchronous playback in a separate thread for non-blocking operation
44

55
import machine
66
import micropython
77
import os
88
import sys
9-
10-
from mpos.task_manager import TaskManager
9+
import time
1110

1211
# Volume scaling function - Viper-optimized for ESP32 performance
1312
# NOTE: The line below is automatically commented out by build_mpos.sh during
@@ -314,8 +313,8 @@ def _upsample_buffer(raw, factor):
314313
# ----------------------------------------------------------------------
315314
# Main playback routine
316315
# ----------------------------------------------------------------------
317-
async def play_async(self):
318-
"""Main async playback routine (runs as TaskManager task)."""
316+
def play(self):
317+
"""Main synchronous playback routine (runs in separate thread)."""
319318
self._is_playing = True
320319

321320
try:
@@ -365,9 +364,8 @@ async def play_async(self):
365364
f.seek(data_start)
366365

367366
# Chunk size tuning notes:
368-
# - Smaller chunks = more responsive to stop(), better async yielding
367+
# - Smaller chunks = more responsive to stop()
369368
# - Larger chunks = less overhead, smoother audio
370-
# - 4096 bytes with async yield works well for responsiveness
371369
# - The 32KB I2S buffer handles timing smoothness
372370
chunk_size = 8192
373371
bytes_per_original_sample = (bits_per_sample // 8) * channels
@@ -407,18 +405,15 @@ async def play_async(self):
407405
scale_fixed = int(scale * 32768)
408406
_scale_audio_optimized(raw, len(raw), scale_fixed)
409407

410-
# 4. Output to I2S
408+
# 4. Output to I2S (blocking write is OK - we're in a separate thread)
411409
if self._i2s:
412410
self._i2s.write(raw)
413411
else:
414412
# Simulate playback timing if no I2S
415413
num_samples = len(raw) // (2 * channels)
416-
await TaskManager.sleep(num_samples / playback_rate)
414+
time.sleep(num_samples / playback_rate)
417415

418416
total_original += to_read
419-
420-
# Yield to other async tasks after each chunk
421-
await TaskManager.sleep_ms(0)
422417

423418
print(f"WAVStream: Finished playing {self.file_path}")
424419
if self.on_complete:

internal_filesystem/lib/mpos/testing/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
MockTask,
3131
MockDownloadManager,
3232

33+
# Threading mocks
34+
MockThread,
35+
MockApps,
36+
3337
# Network mocks
3438
MockNetwork,
3539
MockRequests,
@@ -60,6 +64,10 @@
6064
'MockTask',
6165
'MockDownloadManager',
6266

67+
# Threading mocks
68+
'MockThread',
69+
'MockApps',
70+
6371
# Network mocks
6472
'MockNetwork',
6573
'MockRequests',

internal_filesystem/lib/mpos/testing/mocks.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,4 +727,57 @@ def set_fail_after_bytes(self, bytes_count):
727727

728728
def clear_history(self):
729729
"""Clear the call history."""
730-
self.call_history = []
730+
self.call_history = []
731+
732+
733+
# =============================================================================
734+
# Threading Mocks
735+
# =============================================================================
736+
737+
class MockThread:
738+
"""
739+
Mock _thread module for testing threaded operations.
740+
741+
Usage:
742+
sys.modules['_thread'] = MockThread
743+
"""
744+
745+
_started_threads = []
746+
_stack_size = 0
747+
748+
@classmethod
749+
def start_new_thread(cls, func, args):
750+
"""Record thread start but don't actually start a thread."""
751+
cls._started_threads.append((func, args))
752+
return len(cls._started_threads)
753+
754+
@classmethod
755+
def stack_size(cls, size=None):
756+
"""Mock stack_size."""
757+
if size is not None:
758+
cls._stack_size = size
759+
return cls._stack_size
760+
761+
@classmethod
762+
def clear_threads(cls):
763+
"""Clear recorded threads (for test cleanup)."""
764+
cls._started_threads = []
765+
766+
@classmethod
767+
def get_started_threads(cls):
768+
"""Get list of started threads (for test assertions)."""
769+
return cls._started_threads
770+
771+
772+
class MockApps:
773+
"""
774+
Mock mpos.apps module for testing.
775+
776+
Usage:
777+
sys.modules['mpos.apps'] = MockApps
778+
"""
779+
780+
@staticmethod
781+
def good_stack_size():
782+
"""Return a reasonable stack size for testing."""
783+
return 8192

tests/test_audioflinger.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@
77
MockMachine,
88
MockPWM,
99
MockPin,
10-
MockTaskManager,
11-
create_mock_module,
10+
MockThread,
11+
MockApps,
1212
inject_mocks,
1313
)
1414

1515
# Inject mocks before importing AudioFlinger
1616
inject_mocks({
1717
'machine': MockMachine(),
18-
'mpos.task_manager': create_mock_module('mpos.task_manager', TaskManager=MockTaskManager),
18+
'_thread': MockThread,
19+
'mpos.apps': MockApps,
1920
})
2021

2122
# Now import the module to test

0 commit comments

Comments
 (0)