You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
AudioFlinger: revert to threaded method
The TaskManager (asyncio) was jittery when under heavy CPU load.
This commit is contained in:
@@ -3,9 +3,10 @@
|
||||
# Supports I2S (digital audio) and PWM buzzer (tones/ringtones)
|
||||
#
|
||||
# Simple routing: play_wav() -> I2S, play_rtttl() -> buzzer
|
||||
# Uses TaskManager (asyncio) for non-blocking background playback
|
||||
# Uses _thread for non-blocking background playback (separate thread from UI)
|
||||
|
||||
from mpos.task_manager import TaskManager
|
||||
import _thread
|
||||
import mpos.apps
|
||||
|
||||
# Stream type constants (priority order: higher number = higher priority)
|
||||
STREAM_MUSIC = 0 # Background music (lowest priority)
|
||||
@@ -16,7 +17,6 @@ STREAM_ALARM = 2 # Alarms/alerts (highest priority)
|
||||
_i2s_pins = None # I2S pin configuration dict (created per-stream)
|
||||
_buzzer_instance = None # PWM buzzer instance
|
||||
_current_stream = None # Currently playing stream
|
||||
_current_task = None # Currently running playback task
|
||||
_volume = 50 # System volume (0-100)
|
||||
|
||||
|
||||
@@ -86,27 +86,27 @@ def _check_audio_focus(stream_type):
|
||||
return True
|
||||
|
||||
|
||||
async def _playback_coroutine(stream):
|
||||
def _playback_thread(stream):
|
||||
"""
|
||||
Async coroutine for audio playback.
|
||||
Thread function for audio playback.
|
||||
Runs in a separate thread to avoid blocking the UI.
|
||||
|
||||
Args:
|
||||
stream: Stream instance (WAVStream or RTTTLStream)
|
||||
"""
|
||||
global _current_stream, _current_task
|
||||
global _current_stream
|
||||
|
||||
_current_stream = stream
|
||||
|
||||
try:
|
||||
# Run async playback
|
||||
await stream.play_async()
|
||||
# Run synchronous playback in this thread
|
||||
stream.play()
|
||||
except Exception as e:
|
||||
print(f"AudioFlinger: Playback error: {e}")
|
||||
finally:
|
||||
# Clear current stream
|
||||
if _current_stream == stream:
|
||||
_current_stream = None
|
||||
_current_task = None
|
||||
|
||||
|
||||
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)
|
||||
Returns:
|
||||
bool: True if playback started, False if rejected or unavailable
|
||||
"""
|
||||
global _current_task
|
||||
|
||||
if not _i2s_pins:
|
||||
print("AudioFlinger: play_wav() failed - I2S not configured")
|
||||
return False
|
||||
@@ -132,7 +130,7 @@ def play_wav(file_path, stream_type=STREAM_MUSIC, volume=None, on_complete=None)
|
||||
if not _check_audio_focus(stream_type):
|
||||
return False
|
||||
|
||||
# Create stream and start playback as async task
|
||||
# Create stream and start playback in separate thread
|
||||
try:
|
||||
from mpos.audio.stream_wav import WAVStream
|
||||
|
||||
@@ -144,7 +142,8 @@ def play_wav(file_path, stream_type=STREAM_MUSIC, volume=None, on_complete=None)
|
||||
on_complete=on_complete
|
||||
)
|
||||
|
||||
_current_task = TaskManager.create_task(_playback_coroutine(stream))
|
||||
_thread.stack_size(mpos.apps.good_stack_size())
|
||||
_thread.start_new_thread(_playback_thread, (stream,))
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -165,8 +164,6 @@ def play_rtttl(rtttl_string, stream_type=STREAM_NOTIFICATION, volume=None, on_co
|
||||
Returns:
|
||||
bool: True if playback started, False if rejected or unavailable
|
||||
"""
|
||||
global _current_task
|
||||
|
||||
if not _buzzer_instance:
|
||||
print("AudioFlinger: play_rtttl() failed - buzzer not configured")
|
||||
return False
|
||||
@@ -175,7 +172,7 @@ def play_rtttl(rtttl_string, stream_type=STREAM_NOTIFICATION, volume=None, on_co
|
||||
if not _check_audio_focus(stream_type):
|
||||
return False
|
||||
|
||||
# Create stream and start playback as async task
|
||||
# Create stream and start playback in separate thread
|
||||
try:
|
||||
from mpos.audio.stream_rtttl import RTTTLStream
|
||||
|
||||
@@ -187,7 +184,8 @@ def play_rtttl(rtttl_string, stream_type=STREAM_NOTIFICATION, volume=None, on_co
|
||||
on_complete=on_complete
|
||||
)
|
||||
|
||||
_current_task = TaskManager.create_task(_playback_coroutine(stream))
|
||||
_thread.stack_size(mpos.apps.good_stack_size())
|
||||
_thread.start_new_thread(_playback_thread, (stream,))
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -197,7 +195,7 @@ def play_rtttl(rtttl_string, stream_type=STREAM_NOTIFICATION, volume=None, on_co
|
||||
|
||||
def stop():
|
||||
"""Stop current audio playback."""
|
||||
global _current_stream, _current_task
|
||||
global _current_stream
|
||||
|
||||
if _current_stream:
|
||||
_current_stream.stop()
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# RTTTLStream - RTTTL Ringtone Playback Stream for AudioFlinger
|
||||
# Ring Tone Text Transfer Language parser and player
|
||||
# Uses async playback with TaskManager for non-blocking operation
|
||||
# Uses synchronous playback in a separate thread for non-blocking operation
|
||||
|
||||
import math
|
||||
|
||||
from mpos.task_manager import TaskManager
|
||||
import time
|
||||
|
||||
|
||||
class RTTTLStream:
|
||||
@@ -180,8 +179,8 @@ class RTTTLStream:
|
||||
|
||||
yield freq, msec
|
||||
|
||||
async def play_async(self):
|
||||
"""Play RTTTL tune via buzzer (runs as TaskManager task)."""
|
||||
def play(self):
|
||||
"""Play RTTTL tune via buzzer (runs in separate thread)."""
|
||||
self._is_playing = True
|
||||
|
||||
# Calculate exponential duty cycle for perceptually linear volume
|
||||
@@ -213,10 +212,10 @@ class RTTTLStream:
|
||||
self.buzzer.duty_u16(duty)
|
||||
|
||||
# Play for 90% of duration, silent for 10% (note separation)
|
||||
# Use async sleep to allow other tasks to run
|
||||
await TaskManager.sleep_ms(int(msec * 0.9))
|
||||
# Blocking sleep is OK - we're in a separate thread
|
||||
time.sleep_ms(int(msec * 0.9))
|
||||
self.buzzer.duty_u16(0)
|
||||
await TaskManager.sleep_ms(int(msec * 0.1))
|
||||
time.sleep_ms(int(msec * 0.1))
|
||||
|
||||
print(f"RTTTLStream: Finished playing '{self.name}'")
|
||||
if self.on_complete:
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# WAVStream - WAV File Playback Stream for AudioFlinger
|
||||
# Supports 8/16/24/32-bit PCM, mono+stereo, auto-upsampling, volume control
|
||||
# Uses async playback with TaskManager for non-blocking operation
|
||||
# Uses synchronous playback in a separate thread for non-blocking operation
|
||||
|
||||
import machine
|
||||
import micropython
|
||||
import os
|
||||
import sys
|
||||
|
||||
from mpos.task_manager import TaskManager
|
||||
import time
|
||||
|
||||
# Volume scaling function - Viper-optimized for ESP32 performance
|
||||
# NOTE: The line below is automatically commented out by build_mpos.sh during
|
||||
@@ -314,8 +313,8 @@ class WAVStream:
|
||||
# ----------------------------------------------------------------------
|
||||
# Main playback routine
|
||||
# ----------------------------------------------------------------------
|
||||
async def play_async(self):
|
||||
"""Main async playback routine (runs as TaskManager task)."""
|
||||
def play(self):
|
||||
"""Main synchronous playback routine (runs in separate thread)."""
|
||||
self._is_playing = True
|
||||
|
||||
try:
|
||||
@@ -365,9 +364,8 @@ class WAVStream:
|
||||
f.seek(data_start)
|
||||
|
||||
# Chunk size tuning notes:
|
||||
# - Smaller chunks = more responsive to stop(), better async yielding
|
||||
# - Smaller chunks = more responsive to stop()
|
||||
# - Larger chunks = less overhead, smoother audio
|
||||
# - 4096 bytes with async yield works well for responsiveness
|
||||
# - The 32KB I2S buffer handles timing smoothness
|
||||
chunk_size = 8192
|
||||
bytes_per_original_sample = (bits_per_sample // 8) * channels
|
||||
@@ -407,18 +405,15 @@ class WAVStream:
|
||||
scale_fixed = int(scale * 32768)
|
||||
_scale_audio_optimized(raw, len(raw), scale_fixed)
|
||||
|
||||
# 4. Output to I2S
|
||||
# 4. Output to I2S (blocking write is OK - we're in a separate thread)
|
||||
if self._i2s:
|
||||
self._i2s.write(raw)
|
||||
else:
|
||||
# Simulate playback timing if no I2S
|
||||
num_samples = len(raw) // (2 * channels)
|
||||
await TaskManager.sleep(num_samples / playback_rate)
|
||||
time.sleep(num_samples / playback_rate)
|
||||
|
||||
total_original += to_read
|
||||
|
||||
# Yield to other async tasks after each chunk
|
||||
await TaskManager.sleep_ms(0)
|
||||
|
||||
print(f"WAVStream: Finished playing {self.file_path}")
|
||||
if self.on_complete:
|
||||
|
||||
@@ -30,6 +30,10 @@ from .mocks import (
|
||||
MockTask,
|
||||
MockDownloadManager,
|
||||
|
||||
# Threading mocks
|
||||
MockThread,
|
||||
MockApps,
|
||||
|
||||
# Network mocks
|
||||
MockNetwork,
|
||||
MockRequests,
|
||||
@@ -60,6 +64,10 @@ __all__ = [
|
||||
'MockTask',
|
||||
'MockDownloadManager',
|
||||
|
||||
# Threading mocks
|
||||
'MockThread',
|
||||
'MockApps',
|
||||
|
||||
# Network mocks
|
||||
'MockNetwork',
|
||||
'MockRequests',
|
||||
|
||||
@@ -727,4 +727,57 @@ class MockDownloadManager:
|
||||
|
||||
def clear_history(self):
|
||||
"""Clear the call history."""
|
||||
self.call_history = []
|
||||
self.call_history = []
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Threading Mocks
|
||||
# =============================================================================
|
||||
|
||||
class MockThread:
|
||||
"""
|
||||
Mock _thread module for testing threaded operations.
|
||||
|
||||
Usage:
|
||||
sys.modules['_thread'] = MockThread
|
||||
"""
|
||||
|
||||
_started_threads = []
|
||||
_stack_size = 0
|
||||
|
||||
@classmethod
|
||||
def start_new_thread(cls, func, args):
|
||||
"""Record thread start but don't actually start a thread."""
|
||||
cls._started_threads.append((func, args))
|
||||
return len(cls._started_threads)
|
||||
|
||||
@classmethod
|
||||
def stack_size(cls, size=None):
|
||||
"""Mock stack_size."""
|
||||
if size is not None:
|
||||
cls._stack_size = size
|
||||
return cls._stack_size
|
||||
|
||||
@classmethod
|
||||
def clear_threads(cls):
|
||||
"""Clear recorded threads (for test cleanup)."""
|
||||
cls._started_threads = []
|
||||
|
||||
@classmethod
|
||||
def get_started_threads(cls):
|
||||
"""Get list of started threads (for test assertions)."""
|
||||
return cls._started_threads
|
||||
|
||||
|
||||
class MockApps:
|
||||
"""
|
||||
Mock mpos.apps module for testing.
|
||||
|
||||
Usage:
|
||||
sys.modules['mpos.apps'] = MockApps
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def good_stack_size():
|
||||
"""Return a reasonable stack size for testing."""
|
||||
return 8192
|
||||
@@ -7,15 +7,16 @@ from mpos.testing import (
|
||||
MockMachine,
|
||||
MockPWM,
|
||||
MockPin,
|
||||
MockTaskManager,
|
||||
create_mock_module,
|
||||
MockThread,
|
||||
MockApps,
|
||||
inject_mocks,
|
||||
)
|
||||
|
||||
# Inject mocks before importing AudioFlinger
|
||||
inject_mocks({
|
||||
'machine': MockMachine(),
|
||||
'mpos.task_manager': create_mock_module('mpos.task_manager', TaskManager=MockTaskManager),
|
||||
'_thread': MockThread,
|
||||
'mpos.apps': MockApps,
|
||||
})
|
||||
|
||||
# Now import the module to test
|
||||
|
||||
Reference in New Issue
Block a user