From 1f9eee3a9d697d047543f8d514eaf3e7ea13ffc4 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 26 Jan 2026 13:13:54 +0100 Subject: [PATCH] Rename AudioFlinger to AudioManager framework --- .../assets/music_player.py | 16 +- .../assets/sound_recorder.py | 20 +- internal_filesystem/lib/mpos/__init__.py | 4 +- .../lib/mpos/audio/__init__.py | 4 +- .../{audioflinger.py => audiomanager.py} | 80 +++---- .../lib/mpos/audio/stream_record.py | 2 +- .../lib/mpos/audio/stream_rtttl.py | 2 +- .../lib/mpos/audio/stream_wav.py | 2 +- .../lib/mpos/board/fri3d_2024.py | 10 +- .../lib/mpos/board/fri3d_2026.py | 6 +- internal_filesystem/lib/mpos/board/linux.py | 4 +- .../lib/mpos/hardware/fri3d/__init__.py | 2 +- tests/test_audioflinger.py | 225 ------------------ 13 files changed, 76 insertions(+), 301 deletions(-) rename internal_filesystem/lib/mpos/audio/{audioflinger.py => audiomanager.py} (85%) delete mode 100644 tests/test_audioflinger.py diff --git a/internal_filesystem/apps/com.micropythonos.musicplayer/assets/music_player.py b/internal_filesystem/apps/com.micropythonos.musicplayer/assets/music_player.py index c648a61a..a5a979b4 100644 --- a/internal_filesystem/apps/com.micropythonos.musicplayer/assets/music_player.py +++ b/internal_filesystem/apps/com.micropythonos.musicplayer/assets/music_player.py @@ -2,7 +2,7 @@ import machine import os import time -from mpos import Activity, Intent, sdcard, get_event_name, AudioFlinger +from mpos import Activity, Intent, sdcard, get_event_name, AudioManager class MusicPlayer(Activity): @@ -63,17 +63,17 @@ class FullscreenPlayer(Activity): self._filename = self.getIntent().extras.get("filename") qr_screen = lv.obj() self._slider_label=lv.label(qr_screen) - self._slider_label.set_text(f"Volume: {AudioFlinger.get_volume()}%") + self._slider_label.set_text(f"Volume: {AudioManager.get_volume()}%") self._slider_label.align(lv.ALIGN.TOP_MID,0,lv.pct(4)) self._slider=lv.slider(qr_screen) self._slider.set_range(0,16) - self._slider.set_value(int(AudioFlinger.get_volume()/6.25), False) + self._slider.set_value(int(AudioManager.get_volume()/6.25), False) self._slider.set_width(lv.pct(90)) self._slider.align_to(self._slider_label,lv.ALIGN.OUT_BOTTOM_MID,0,10) def volume_slider_changed(e): volume_int = self._slider.get_value()*6.25 self._slider_label.set_text(f"Volume: {volume_int}%") - AudioFlinger.set_volume(volume_int) + AudioManager.set_volume(volume_int) self._slider.add_event_cb(volume_slider_changed,lv.EVENT.VALUE_CHANGED,None) self._filename_label = lv.label(qr_screen) self._filename_label.align(lv.ALIGN.CENTER,0,0) @@ -100,12 +100,12 @@ class FullscreenPlayer(Activity): print("Not playing any file...") else: print(f"Playing file {self._filename}") - AudioFlinger.stop() + AudioManager.stop() time.sleep(0.1) - success = AudioFlinger.play_wav( + success = AudioManager.play_wav( self._filename, - stream_type=AudioFlinger.STREAM_MUSIC, + stream_type=AudioManager.STREAM_MUSIC, on_complete=self.player_finished ) @@ -125,7 +125,7 @@ class FullscreenPlayer(Activity): obj.set_style_border_width(0, lv.PART.MAIN) def stop_button_clicked(self, event): - AudioFlinger.stop() + AudioManager.stop() self.finish() def player_finished(self, result=None): diff --git a/internal_filesystem/apps/com.micropythonos.soundrecorder/assets/sound_recorder.py b/internal_filesystem/apps/com.micropythonos.soundrecorder/assets/sound_recorder.py index f3882c6b..12aebc41 100644 --- a/internal_filesystem/apps/com.micropythonos.soundrecorder/assets/sound_recorder.py +++ b/internal_filesystem/apps/com.micropythonos.soundrecorder/assets/sound_recorder.py @@ -2,7 +2,7 @@ import os import time -from mpos import Activity, ui, AudioFlinger +from mpos import Activity, ui, AudioManager def _makedirs(path): @@ -136,7 +136,7 @@ class SoundRecorder(Activity): def _update_status(self): """Update status label based on microphone availability.""" - if AudioFlinger.has_microphone(): + if AudioManager.has_microphone(): self._status_label.set_text("Microphone ready") self._status_label.set_style_text_color(lv.color_hex(0x00AA00), lv.PART.MAIN) self._record_button.remove_flag(lv.obj.FLAG.HIDDEN) @@ -243,9 +243,9 @@ class SoundRecorder(Activity): def _start_recording(self): """Start recording audio.""" print("SoundRecorder: _start_recording called") - print(f"SoundRecorder: has_microphone() = {AudioFlinger.has_microphone()}") + print(f"SoundRecorder: has_microphone() = {AudioManager.has_microphone()}") - if not AudioFlinger.has_microphone(): + if not AudioManager.has_microphone(): print("SoundRecorder: No microphone available - aborting") return @@ -263,12 +263,12 @@ class SoundRecorder(Activity): return # Start recording - print(f"SoundRecorder: Calling AudioFlinger.record_wav()") + print(f"SoundRecorder: Calling AudioManager.record_wav()") print(f" file_path: {file_path}") print(f" duration_ms: {self._current_max_duration_ms}") print(f" sample_rate: {self.SAMPLE_RATE}") - success = AudioFlinger.record_wav( + success = AudioManager.record_wav( file_path=file_path, duration_ms=self._current_max_duration_ms, on_complete=self._on_recording_complete, @@ -302,7 +302,7 @@ class SoundRecorder(Activity): def _stop_recording(self): """Stop recording audio.""" - AudioFlinger.stop() + AudioManager.stop() self._is_recording = False # Show "Saving..." status immediately (file finalization takes time on SD card) @@ -364,13 +364,13 @@ class SoundRecorder(Activity): """Handle play button click.""" if self._last_recording and not self._is_recording: # Stop any current playback - AudioFlinger.stop() + AudioManager.stop() time.sleep_ms(100) # Play the recording - success = AudioFlinger.play_wav( + success = AudioManager.play_wav( self._last_recording, - stream_type=AudioFlinger.STREAM_MUSIC, + stream_type=AudioManager.STREAM_MUSIC, on_complete=self._on_playback_complete, volume=100 ) diff --git a/internal_filesystem/lib/mpos/__init__.py b/internal_filesystem/lib/mpos/__init__.py index 49d8516a..b76cfa72 100644 --- a/internal_filesystem/lib/mpos/__init__.py +++ b/internal_filesystem/lib/mpos/__init__.py @@ -8,7 +8,7 @@ from .content.app_manager import AppManager from .config import SharedPreferences from .net.connectivity_manager import ConnectivityManager from .net.wifi_service import WifiService -from .audio.audioflinger import AudioFlinger +from .audio.audiomanager import AudioManager from .net.download_manager import DownloadManager from .task_manager import TaskManager from .camera_manager import CameraManager @@ -66,7 +66,7 @@ __all__ = [ "App", "Activity", "SharedPreferences", - "ConnectivityManager", "DownloadManager", "WifiService", "AudioFlinger", "Intent", + "ConnectivityManager", "DownloadManager", "WifiService", "AudioManager", "Intent", "ActivityNavigator", "AppManager", "TaskManager", "CameraManager", "BatteryManager", # Device and build info "DeviceInfo", "BuildInfo", diff --git a/internal_filesystem/lib/mpos/audio/__init__.py b/internal_filesystem/lib/mpos/audio/__init__.py index c4879590..d009cb77 100644 --- a/internal_filesystem/lib/mpos/audio/__init__.py +++ b/internal_filesystem/lib/mpos/audio/__init__.py @@ -1,5 +1,5 @@ -# AudioFlinger - Centralized Audio Management Service for MicroPythonOS +# AudioManager - Centralized Audio Management Service for MicroPythonOS # Android-inspired audio routing with priority-based audio focus # Simple routing: play_wav() -> I2S, play_rtttl() -> buzzer, record_wav() -> I2S mic -from .audioflinger import AudioFlinger +from .audiomanager import AudioManager diff --git a/internal_filesystem/lib/mpos/audio/audioflinger.py b/internal_filesystem/lib/mpos/audio/audiomanager.py similarity index 85% rename from internal_filesystem/lib/mpos/audio/audioflinger.py rename to internal_filesystem/lib/mpos/audio/audiomanager.py index 27a1119b..306d69be 100644 --- a/internal_filesystem/lib/mpos/audio/audioflinger.py +++ b/internal_filesystem/lib/mpos/audio/audiomanager.py @@ -1,4 +1,4 @@ -# AudioFlinger - Core Audio Management Service +# AudioManager - Core Audio Management Service # Centralized audio routing with priority-based audio focus (Android-inspired) # Supports I2S (digital audio) and PWM buzzer (tones/ringtones) # @@ -9,20 +9,20 @@ import _thread from ..task_manager import TaskManager -class AudioFlinger: +class AudioManager: """ Centralized audio management service with priority-based audio focus. Implements singleton pattern for single audio service instance. Usage: - from mpos import AudioFlinger + from mpos import AudioManager # Direct class method calls (no .get() needed) - AudioFlinger.init(i2s_pins=pins, buzzer_instance=buzzer) - AudioFlinger.play_wav("music.wav", stream_type=AudioFlinger.STREAM_MUSIC) - AudioFlinger.set_volume(80) - volume = AudioFlinger.get_volume() - AudioFlinger.stop() + AudioManager.init(i2s_pins=pins, buzzer_instance=buzzer) + AudioManager.play_wav("music.wav", stream_type=AudioManager.STREAM_MUSIC) + AudioManager.set_volume(80) + volume = AudioManager.get_volume() + AudioManager.stop() """ # Stream type constants (priority order: higher number = higher priority) @@ -34,15 +34,15 @@ class AudioFlinger: def __init__(self, i2s_pins=None, buzzer_instance=None): """ - Initialize AudioFlinger instance with optional hardware configuration. + Initialize AudioManager instance with optional hardware configuration. Args: i2s_pins: Dict with 'sck', 'ws', 'sd' pin numbers (for I2S/WAV playback) buzzer_instance: PWM instance for buzzer (for RTTTL playback) """ - if AudioFlinger._instance: + if AudioManager._instance: return - AudioFlinger._instance = self + AudioManager._instance = self self._i2s_pins = i2s_pins # I2S pin configuration dict (created per-stream) self._buzzer_instance = buzzer_instance # PWM buzzer instance @@ -58,9 +58,9 @@ class AudioFlinger: capabilities.append("Buzzer (RTTTL)") if capabilities: - print(f"AudioFlinger initialized: {', '.join(capabilities)}") + print(f"AudioManager initialized: {', '.join(capabilities)}") else: - print("AudioFlinger initialized: No audio hardware") + print("AudioManager initialized: No audio hardware") @classmethod def get(cls): @@ -100,11 +100,11 @@ class AudioFlinger: # Check priority if stream_type <= self._current_stream.stream_type: - print(f"AudioFlinger: Stream rejected (priority {stream_type} <= current {self._current_stream.stream_type})") + print(f"AudioManager: Stream rejected (priority {stream_type} <= current {self._current_stream.stream_type})") return False # Higher priority stream - interrupt current - print(f"AudioFlinger: Interrupting stream (priority {stream_type} > current {self._current_stream.stream_type})") + print(f"AudioManager: Interrupting stream (priority {stream_type} > current {self._current_stream.stream_type})") self._current_stream.stop() return True @@ -122,7 +122,7 @@ class AudioFlinger: # Run synchronous playback in this thread stream.play() except Exception as e: - print(f"AudioFlinger: Playback error: {e}") + print(f"AudioManager: Playback error: {e}") finally: # Clear current stream if self._current_stream == stream: @@ -145,7 +145,7 @@ class AudioFlinger: stream_type = self.STREAM_MUSIC if not self._i2s_pins: - print("AudioFlinger: play_wav() failed - I2S not configured") + print("AudioManager: play_wav() failed - I2S not configured") return False # Check audio focus @@ -169,7 +169,7 @@ class AudioFlinger: return True except Exception as e: - print(f"AudioFlinger: play_wav() failed: {e}") + print(f"AudioManager: play_wav() failed: {e}") return False def play_rtttl(self, rtttl_string, stream_type=None, volume=None, on_complete=None): @@ -189,7 +189,7 @@ class AudioFlinger: stream_type = self.STREAM_NOTIFICATION if not self._buzzer_instance: - print("AudioFlinger: play_rtttl() failed - buzzer not configured") + print("AudioManager: play_rtttl() failed - buzzer not configured") return False # Check audio focus @@ -213,7 +213,7 @@ class AudioFlinger: return True except Exception as e: - print(f"AudioFlinger: play_rtttl() failed: {e}") + print(f"AudioManager: play_rtttl() failed: {e}") return False def _recording_thread(self, stream): @@ -230,7 +230,7 @@ class AudioFlinger: # Run synchronous recording in this thread stream.record() except Exception as e: - print(f"AudioFlinger: Recording error: {e}") + print(f"AudioManager: Recording error: {e}") finally: # Clear current recording if self._current_recording == stream: @@ -249,7 +249,7 @@ class AudioFlinger: Returns: bool: True if recording started, False if rejected or unavailable """ - print(f"AudioFlinger.record_wav() called") + print(f"AudioManager.record_wav() called") print(f" file_path: {file_path}") print(f" duration_ms: {duration_ms}") print(f" sample_rate: {sample_rate}") @@ -257,25 +257,25 @@ class AudioFlinger: print(f" has_microphone(): {self.has_microphone()}") if not self.has_microphone(): - print("AudioFlinger: record_wav() failed - microphone not configured") + print("AudioManager: record_wav() failed - microphone not configured") return False # Cannot record while playing (I2S can only be TX or RX, not both) if self.is_playing(): - print("AudioFlinger: Cannot record while playing") + print("AudioManager: Cannot record while playing") return False # Cannot start new recording while already recording if self.is_recording(): - print("AudioFlinger: Already recording") + print("AudioManager: Already recording") return False # Create stream and start recording in separate thread try: - print("AudioFlinger: Importing RecordStream...") + print("AudioManager: Importing RecordStream...") from mpos.audio.stream_record import RecordStream - print("AudioFlinger: Creating RecordStream instance...") + print("AudioManager: Creating RecordStream instance...") stream = RecordStream( file_path=file_path, duration_ms=duration_ms, @@ -284,15 +284,15 @@ class AudioFlinger: on_complete=on_complete ) - print("AudioFlinger: Starting recording thread...") + print("AudioManager: Starting recording thread...") _thread.stack_size(TaskManager.good_stack_size()) _thread.start_new_thread(self._recording_thread, (stream,)) - print("AudioFlinger: Recording thread started successfully") + print("AudioManager: Recording thread started successfully") return True except Exception as e: import sys - print(f"AudioFlinger: record_wav() failed: {e}") + print(f"AudioManager: record_wav() failed: {e}") sys.print_exception(e) return False @@ -302,16 +302,16 @@ class AudioFlinger: if self._current_stream: self._current_stream.stop() - print("AudioFlinger: Playback stopped") + print("AudioManager: Playback stopped") stopped = True if self._current_recording: self._current_recording.stop() - print("AudioFlinger: Recording stopped") + print("AudioManager: Recording stopped") stopped = True if not stopped: - print("AudioFlinger: No playback or recording to stop") + print("AudioManager: No playback or recording to stop") def pause(self): """ @@ -320,9 +320,9 @@ class AudioFlinger: """ if self._current_stream and hasattr(self._current_stream, 'pause'): self._current_stream.pause() - print("AudioFlinger: Playback paused") + print("AudioManager: Playback paused") else: - print("AudioFlinger: Pause not supported or no playback active") + print("AudioManager: Pause not supported or no playback active") def resume(self): """ @@ -331,9 +331,9 @@ class AudioFlinger: """ if self._current_stream and hasattr(self._current_stream, 'resume'): self._current_stream.resume() - print("AudioFlinger: Playback resumed") + print("AudioManager: Playback resumed") else: - print("AudioFlinger: Resume not supported or no playback active") + print("AudioManager: Resume not supported or no playback active") def set_volume(self, volume): """ @@ -396,7 +396,7 @@ _methods_to_delegate = [ ] for method_name in _methods_to_delegate: - _original_methods[method_name] = getattr(AudioFlinger, method_name) + _original_methods[method_name] = getattr(AudioManager, method_name) # Helper to create delegating class methods def _make_class_method(method_name): @@ -410,6 +410,6 @@ def _make_class_method(method_name): return class_method -# Attach class methods to AudioFlinger +# Attach class methods to AudioManager for method_name in _methods_to_delegate: - setattr(AudioFlinger, method_name, _make_class_method(method_name)) + setattr(AudioManager, method_name, _make_class_method(method_name)) diff --git a/internal_filesystem/lib/mpos/audio/stream_record.py b/internal_filesystem/lib/mpos/audio/stream_record.py index 3a4990f7..d12a580e 100644 --- a/internal_filesystem/lib/mpos/audio/stream_record.py +++ b/internal_filesystem/lib/mpos/audio/stream_record.py @@ -1,4 +1,4 @@ -# RecordStream - WAV File Recording Stream for AudioFlinger +# RecordStream - WAV File Recording Stream for AudioManager # Records 16-bit mono PCM audio from I2S microphone to WAV file # Uses synchronous recording in a separate thread for non-blocking operation # On desktop (no I2S hardware), generates a 440Hz sine wave for testing diff --git a/internal_filesystem/lib/mpos/audio/stream_rtttl.py b/internal_filesystem/lib/mpos/audio/stream_rtttl.py index d02761f5..83469d10 100644 --- a/internal_filesystem/lib/mpos/audio/stream_rtttl.py +++ b/internal_filesystem/lib/mpos/audio/stream_rtttl.py @@ -1,4 +1,4 @@ -# RTTTLStream - RTTTL Ringtone Playback Stream for AudioFlinger +# RTTTLStream - RTTTL Ringtone Playback Stream for AudioManager # Ring Tone Text Transfer Language parser and player # Uses synchronous playback in a separate thread for non-blocking operation diff --git a/internal_filesystem/lib/mpos/audio/stream_wav.py b/internal_filesystem/lib/mpos/audio/stream_wav.py index 10e4801a..7516dbae 100644 --- a/internal_filesystem/lib/mpos/audio/stream_wav.py +++ b/internal_filesystem/lib/mpos/audio/stream_wav.py @@ -1,4 +1,4 @@ -# WAVStream - WAV File Playback Stream for AudioFlinger +# WAVStream - WAV File Playback Stream for AudioManager # Supports 8/16/24/32-bit PCM, mono+stereo, auto-upsampling, volume control # Uses synchronous playback in a separate thread for non-blocking operation diff --git a/internal_filesystem/lib/mpos/board/fri3d_2024.py b/internal_filesystem/lib/mpos/board/fri3d_2024.py index 953c6346..56194e02 100644 --- a/internal_filesystem/lib/mpos/board/fri3d_2024.py +++ b/internal_filesystem/lib/mpos/board/fri3d_2024.py @@ -296,7 +296,7 @@ mpos.sdcard.init(spi_bus, cs_pin=14) # === AUDIO HARDWARE === from machine import PWM, Pin -from mpos import AudioFlinger +from mpos import AudioManager # Initialize buzzer (GPIO 46) buzzer = PWM(Pin(46), freq=550, duty=0) @@ -315,8 +315,8 @@ i2s_pins = { 'sd_in': 15, # DIN - Serial Data IN (microphone) } -# Initialize AudioFlinger with I2S and buzzer -AudioFlinger(i2s_pins=i2s_pins, buzzer_instance=buzzer) +# Initialize AudioManager with I2S and buzzer +AudioManager(i2s_pins=i2s_pins, buzzer_instance=buzzer) # === LED HARDWARE === import mpos.lights as LightsManager @@ -349,9 +349,9 @@ def startup_wow_effect(): #startup_jingle = "ShortBeeps:d=32,o=5,b=320:c6,c7" # Start the jingle - AudioFlinger.play_rtttl( + AudioManager.play_rtttl( startup_jingle, - stream_type=AudioFlinger.STREAM_NOTIFICATION, + stream_type=AudioManager.STREAM_NOTIFICATION, volume=60 ) diff --git a/internal_filesystem/lib/mpos/board/fri3d_2026.py b/internal_filesystem/lib/mpos/board/fri3d_2026.py index 724abe12..82febe57 100644 --- a/internal_filesystem/lib/mpos/board/fri3d_2026.py +++ b/internal_filesystem/lib/mpos/board/fri3d_2026.py @@ -200,7 +200,7 @@ mpos.sdcard.init(spi_bus, cs_pin=14) # === AUDIO HARDWARE === from machine import PWM, Pin -from mpos import AudioFlinger +from mpos import AudioManager # Initialize buzzer: now sits on PC14/CC1 of the CH32X035GxUx so needs custom code #buzzer = PWM(Pin(46), freq=550, duty=0) @@ -219,8 +219,8 @@ i2s_pins = { 'sd_in': 15, # DIN - Serial Data IN (microphone) } -# Initialize AudioFlinger with I2S (buzzer TODO) -AudioFlinger(i2s_pins=i2s_pins) +# Initialize AudioManager with I2S (buzzer TODO) +AudioManager(i2s_pins=i2s_pins) # === LED HARDWARE === import mpos.lights as LightsManager diff --git a/internal_filesystem/lib/mpos/board/linux.py b/internal_filesystem/lib/mpos/board/linux.py index 4c7df84a..dd00f307 100644 --- a/internal_filesystem/lib/mpos/board/linux.py +++ b/internal_filesystem/lib/mpos/board/linux.py @@ -98,7 +98,7 @@ def adc_to_voltage(adc_value): BatteryManager.init_adc(999, adc_to_voltage) # === AUDIO HARDWARE === -from mpos import AudioFlinger +from mpos import AudioManager # Desktop builds have no real audio hardware, but we simulate microphone # recording with a 440Hz sine wave for testing WAV file generation @@ -110,7 +110,7 @@ i2s_pins = { 'sck_in': 0, # Simulated - not used on desktop 'sd_in': 0, # Simulated - enables microphone simulation } -AudioFlinger(i2s_pins=i2s_pins) +AudioManager(i2s_pins=i2s_pins) # === LED HARDWARE === # Note: Desktop builds have no LED hardware diff --git a/internal_filesystem/lib/mpos/hardware/fri3d/__init__.py b/internal_filesystem/lib/mpos/hardware/fri3d/__init__.py index 18919b17..792a35be 100644 --- a/internal_filesystem/lib/mpos/hardware/fri3d/__init__.py +++ b/internal_filesystem/lib/mpos/hardware/fri3d/__init__.py @@ -1,5 +1,5 @@ # Fri3d Camp 2024 Badge Hardware Drivers -# These are simple wrappers that can be used by services like AudioFlinger +# These are simple wrappers that can be used by services like AudioManager from .buzzer import BuzzerConfig from .leds import LEDConfig diff --git a/tests/test_audioflinger.py b/tests/test_audioflinger.py deleted file mode 100644 index ab51ab38..00000000 --- a/tests/test_audioflinger.py +++ /dev/null @@ -1,225 +0,0 @@ -# Unit tests for AudioFlinger service -import unittest -import sys - -# Import centralized mocks -from mpos.testing import ( - MockMachine, - MockPWM, - MockPin, - MockThread, - inject_mocks, -) - -# Inject mocks before importing AudioFlinger -inject_mocks({ - 'machine': MockMachine(), - '_thread': MockThread, -}) - -# Now import the module to test -from mpos.audio.audioflinger import AudioFlinger - - -class TestAudioFlinger(unittest.TestCase): - """Test cases for AudioFlinger service.""" - - def setUp(self): - """Initialize AudioFlinger before each test.""" - self.buzzer = MockPWM(MockPin(46)) - self.i2s_pins = {'sck': 2, 'ws': 47, 'sd': 16} - - # Reset singleton instance for each test - AudioFlinger._instance = None - - AudioFlinger( - i2s_pins=self.i2s_pins, - buzzer_instance=self.buzzer - ) - - # Reset volume to default after creating instance - AudioFlinger.set_volume(70) - - def tearDown(self): - """Clean up after each test.""" - AudioFlinger.stop() - - def test_initialization(self): - """Test that AudioFlinger initializes correctly.""" - af = AudioFlinger.get() - self.assertEqual(af._i2s_pins, self.i2s_pins) - self.assertEqual(af._buzzer_instance, self.buzzer) - - def test_has_i2s(self): - """Test has_i2s() returns correct value.""" - # With I2S configured - AudioFlinger._instance = None - AudioFlinger(i2s_pins=self.i2s_pins, buzzer_instance=None) - self.assertTrue(AudioFlinger.has_i2s()) - - # Without I2S configured - AudioFlinger._instance = None - AudioFlinger(i2s_pins=None, buzzer_instance=self.buzzer) - self.assertFalse(AudioFlinger.has_i2s()) - - def test_has_buzzer(self): - """Test has_buzzer() returns correct value.""" - # With buzzer configured - AudioFlinger._instance = None - AudioFlinger(i2s_pins=None, buzzer_instance=self.buzzer) - self.assertTrue(AudioFlinger.has_buzzer()) - - # Without buzzer configured - AudioFlinger._instance = None - AudioFlinger(i2s_pins=self.i2s_pins, buzzer_instance=None) - self.assertFalse(AudioFlinger.has_buzzer()) - - def test_stream_types(self): - """Test stream type constants and priority order.""" - self.assertEqual(AudioFlinger.STREAM_MUSIC, 0) - self.assertEqual(AudioFlinger.STREAM_NOTIFICATION, 1) - self.assertEqual(AudioFlinger.STREAM_ALARM, 2) - - # Higher number = higher priority - self.assertTrue(AudioFlinger.STREAM_MUSIC < AudioFlinger.STREAM_NOTIFICATION) - self.assertTrue(AudioFlinger.STREAM_NOTIFICATION < AudioFlinger.STREAM_ALARM) - - def test_volume_control(self): - """Test volume get/set operations.""" - # Set volume - AudioFlinger.set_volume(50) - self.assertEqual(AudioFlinger.get_volume(), 50) - - # Test clamping to 0-100 range - AudioFlinger.set_volume(150) - self.assertEqual(AudioFlinger.get_volume(), 100) - - AudioFlinger.set_volume(-10) - self.assertEqual(AudioFlinger.get_volume(), 0) - - def test_no_hardware_rejects_playback(self): - """Test that no hardware rejects all playback requests.""" - # Re-initialize with no hardware - AudioFlinger._instance = None - AudioFlinger(i2s_pins=None, buzzer_instance=None) - - # WAV should be rejected (no I2S) - result = AudioFlinger.play_wav("test.wav") - self.assertFalse(result) - - # RTTTL should be rejected (no buzzer) - result = AudioFlinger.play_rtttl("Test:d=4,o=5,b=120:c") - self.assertFalse(result) - - def test_i2s_only_rejects_rtttl(self): - """Test that I2S-only config rejects buzzer playback.""" - # Re-initialize with I2S only - AudioFlinger._instance = None - AudioFlinger(i2s_pins=self.i2s_pins, buzzer_instance=None) - - # RTTTL should be rejected (no buzzer) - result = AudioFlinger.play_rtttl("Test:d=4,o=5,b=120:c") - self.assertFalse(result) - - def test_buzzer_only_rejects_wav(self): - """Test that buzzer-only config rejects I2S playback.""" - # Re-initialize with buzzer only - AudioFlinger._instance = None - AudioFlinger(i2s_pins=None, buzzer_instance=self.buzzer) - - # WAV should be rejected (no I2S) - result = AudioFlinger.play_wav("test.wav") - self.assertFalse(result) - - def test_is_playing_initially_false(self): - """Test that is_playing() returns False initially.""" - # Reset to ensure clean state - AudioFlinger._instance = None - AudioFlinger(i2s_pins=self.i2s_pins, buzzer_instance=self.buzzer) - self.assertFalse(AudioFlinger.is_playing()) - - def test_stop_with_no_playback(self): - """Test that stop() can be called when nothing is playing.""" - # Should not raise exception - AudioFlinger.stop() - self.assertFalse(AudioFlinger.is_playing()) - - def test_audio_focus_check_no_current_stream(self): - """Test audio focus allows playback when no stream is active.""" - af = AudioFlinger.get() - result = af._check_audio_focus(AudioFlinger.STREAM_MUSIC) - self.assertTrue(result) - - def test_volume_default_value(self): - """Test that default volume is reasonable.""" - # After init, volume should be at default (70) - AudioFlinger(i2s_pins=None, buzzer_instance=None) - self.assertEqual(AudioFlinger.get_volume(), 70) - - -class TestAudioFlingerRecording(unittest.TestCase): - """Test cases for AudioFlinger recording functionality.""" - - def setUp(self): - """Initialize AudioFlinger with microphone before each test.""" - self.buzzer = MockPWM(MockPin(46)) - # I2S pins with microphone input - self.i2s_pins_with_mic = {'sck': 2, 'ws': 47, 'sd': 16, 'sd_in': 15} - # I2S pins without microphone input - self.i2s_pins_no_mic = {'sck': 2, 'ws': 47, 'sd': 16} - - # Reset singleton instance for each test - AudioFlinger._instance = None - - AudioFlinger( - i2s_pins=self.i2s_pins_with_mic, - buzzer_instance=self.buzzer - ) - - # Reset volume to default after creating instance - AudioFlinger.set_volume(70) - - def tearDown(self): - """Clean up after each test.""" - AudioFlinger.stop() - - def test_has_microphone_with_sd_in(self): - """Test has_microphone() returns True when sd_in pin is configured.""" - AudioFlinger._instance = None - AudioFlinger(i2s_pins=self.i2s_pins_with_mic, buzzer_instance=None) - self.assertTrue(AudioFlinger.has_microphone()) - - def test_has_microphone_without_sd_in(self): - """Test has_microphone() returns False when sd_in pin is not configured.""" - AudioFlinger._instance = None - AudioFlinger(i2s_pins=self.i2s_pins_no_mic, buzzer_instance=None) - self.assertFalse(AudioFlinger.has_microphone()) - - def test_has_microphone_no_i2s(self): - """Test has_microphone() returns False when no I2S is configured.""" - AudioFlinger._instance = None - AudioFlinger(i2s_pins=None, buzzer_instance=self.buzzer) - self.assertFalse(AudioFlinger.has_microphone()) - - def test_is_recording_initially_false(self): - """Test that is_recording() returns False initially.""" - self.assertFalse(AudioFlinger.is_recording()) - - def test_record_wav_no_microphone(self): - """Test that record_wav() fails when no microphone is configured.""" - AudioFlinger._instance = None - AudioFlinger(i2s_pins=self.i2s_pins_no_mic, buzzer_instance=None) - result = AudioFlinger.record_wav("test.wav") - self.assertFalse(result, "record_wav() fails when no microphone is configured") - - def test_record_wav_no_i2s(self): - AudioFlinger._instance = None - AudioFlinger(i2s_pins=None, buzzer_instance=self.buzzer) - result = AudioFlinger.record_wav("test.wav") - self.assertFalse(result, "record_wav() should fail when no I2S is configured") - - def test_stop_with_no_recording(self): - """Test that stop() can be called when nothing is recording.""" - # Should not raise exception - AudioFlinger.stop() - self.assertFalse(AudioFlinger.is_recording())