Rework AudioManager

This commit is contained in:
Thomas Farstrike
2026-02-24 16:39:26 +01:00
parent 2f1ee282c3
commit daa7125052
14 changed files with 954 additions and 663 deletions
+63 -93
View File
@@ -5,8 +5,6 @@ import sys
# Import centralized mocks
from mpos.testing import (
MockMachine,
MockPWM,
MockPin,
MockThread,
inject_mocks,
)
@@ -26,17 +24,16 @@ class TestAudioManager(unittest.TestCase):
def setUp(self):
"""Initialize AudioManager before each test."""
self.buzzer = MockPWM(MockPin(46))
self.buzzer_pin = 46
self.i2s_pins = {'sck': 2, 'ws': 47, 'sd': 16}
# Reset singleton instance for each test
AudioManager._instance = None
AudioManager(
i2s_pins=self.i2s_pins,
buzzer_instance=self.buzzer
)
AudioManager()
AudioManager.add(AudioManager.Output("speaker", "i2s", i2s_pins=self.i2s_pins))
AudioManager.add(AudioManager.Output("buzzer", "buzzer", buzzer_pin=self.buzzer_pin))
# Reset volume to default after creating instance
AudioManager.set_volume(70)
@@ -47,32 +44,22 @@ class TestAudioManager(unittest.TestCase):
def test_initialization(self):
"""Test that AudioManager initializes correctly."""
am = AudioManager.get()
self.assertEqual(am._i2s_pins, self.i2s_pins)
self.assertEqual(am._buzzer_instance, self.buzzer)
self.assertEqual(len(am._outputs), 2)
self.assertEqual(am._outputs[0].i2s_pins, self.i2s_pins)
self.assertEqual(am._outputs[1].buzzer_pin, self.buzzer_pin)
def test_has_i2s(self):
"""Test has_i2s() returns correct value."""
# With I2S configured
AudioManager._instance = None
AudioManager(i2s_pins=self.i2s_pins, buzzer_instance=None)
self.assertTrue(AudioManager.has_i2s())
# Without I2S configured
AudioManager._instance = None
AudioManager(i2s_pins=None, buzzer_instance=self.buzzer)
self.assertFalse(AudioManager.has_i2s())
def test_get_outputs(self):
"""Test that get_outputs() returns configured outputs."""
outputs = AudioManager.get_outputs()
self.assertEqual(len(outputs), 2)
self.assertEqual(outputs[0].kind, "i2s")
self.assertEqual(outputs[1].kind, "buzzer")
def test_has_buzzer(self):
"""Test has_buzzer() returns correct value."""
# With buzzer configured
AudioManager._instance = None
AudioManager(i2s_pins=None, buzzer_instance=self.buzzer)
self.assertTrue(AudioManager.has_buzzer())
# Without buzzer configured
AudioManager._instance = None
AudioManager(i2s_pins=self.i2s_pins, buzzer_instance=None)
self.assertFalse(AudioManager.has_buzzer())
def test_default_output(self):
"""Test default output selection."""
default_output = AudioManager.get_default_output()
self.assertIsNotNone(default_output)
self.assertEqual(default_output.kind, "i2s")
def test_stream_types(self):
"""Test stream type constants and priority order."""
@@ -101,60 +88,53 @@ class TestAudioManager(unittest.TestCase):
"""Test that no hardware rejects all playback requests."""
# Re-initialize with no hardware
AudioManager._instance = None
AudioManager(i2s_pins=None, buzzer_instance=None)
AudioManager()
# WAV should be rejected (no I2S)
result = AudioManager.play_wav("test.wav")
self.assertFalse(result)
with self.assertRaises(ValueError):
AudioManager.player(file_path="test.wav").start()
# RTTTL should be rejected (no buzzer)
result = AudioManager.play_rtttl("Test:d=4,o=5,b=120:c")
self.assertFalse(result)
with self.assertRaises(ValueError):
AudioManager.player(rtttl="Test:d=4,o=5,b=120:c").start()
def test_i2s_only_rejects_rtttl(self):
"""Test that I2S-only config rejects buzzer playback."""
# Re-initialize with I2S only
AudioManager._instance = None
AudioManager(i2s_pins=self.i2s_pins, buzzer_instance=None)
AudioManager()
AudioManager.add(AudioManager.Output("speaker", "i2s", i2s_pins=self.i2s_pins))
# RTTTL should be rejected (no buzzer)
result = AudioManager.play_rtttl("Test:d=4,o=5,b=120:c")
self.assertFalse(result)
with self.assertRaises(ValueError):
AudioManager.player(rtttl="Test:d=4,o=5,b=120:c").start()
def test_buzzer_only_rejects_wav(self):
"""Test that buzzer-only config rejects I2S playback."""
# Re-initialize with buzzer only
AudioManager._instance = None
AudioManager(i2s_pins=None, buzzer_instance=self.buzzer)
AudioManager()
AudioManager.add(AudioManager.Output("buzzer", "buzzer", buzzer_pin=self.buzzer_pin))
# WAV should be rejected (no I2S)
result = AudioManager.play_wav("test.wav")
self.assertFalse(result)
with self.assertRaises(ValueError):
AudioManager.player(file_path="test.wav").start()
def test_is_playing_initially_false(self):
"""Test that is_playing() returns False initially."""
# Reset to ensure clean state
AudioManager._instance = None
AudioManager(i2s_pins=self.i2s_pins, buzzer_instance=self.buzzer)
self.assertFalse(AudioManager.is_playing())
AudioManager()
AudioManager.add(AudioManager.Output("speaker", "i2s", i2s_pins=self.i2s_pins))
self.assertFalse(AudioManager.player(file_path="test.wav").is_playing())
def test_stop_with_no_playback(self):
"""Test that stop() can be called when nothing is playing."""
# Should not raise exception
AudioManager.stop()
self.assertFalse(AudioManager.is_playing())
def test_audio_focus_check_no_current_stream(self):
"""Test audio focus allows playback when no stream is active."""
am = AudioManager.get()
result = am._check_audio_focus(AudioManager.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)
AudioManager(i2s_pins=None, buzzer_instance=None)
self.assertEqual(AudioManager.get_volume(), 70)
# After init, volume should be at default (50)
AudioManager._instance = None
AudioManager()
self.assertEqual(AudioManager.get_volume(), 50)
class TestAudioManagerRecording(unittest.TestCase):
@@ -162,20 +142,15 @@ class TestAudioManagerRecording(unittest.TestCase):
def setUp(self):
"""Initialize AudioManager 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}
self.i2s_pins_with_mic = {'sck': 2, 'ws': 47, 'sd_in': 15}
# Reset singleton instance for each test
AudioManager._instance = None
AudioManager(
i2s_pins=self.i2s_pins_with_mic,
buzzer_instance=self.buzzer
)
AudioManager()
AudioManager.add(AudioManager.Input("mic", "i2s", i2s_pins=self.i2s_pins_with_mic))
# Reset volume to default after creating instance
AudioManager.set_volume(70)
@@ -183,43 +158,38 @@ class TestAudioManagerRecording(unittest.TestCase):
"""Clean up after each test."""
AudioManager.stop()
def test_has_microphone_with_sd_in(self):
"""Test has_microphone() returns True when sd_in pin is configured."""
AudioManager._instance = None
AudioManager(i2s_pins=self.i2s_pins_with_mic, buzzer_instance=None)
self.assertTrue(AudioManager.has_microphone())
def test_get_inputs(self):
"""Test get_inputs() returns configured inputs."""
inputs = AudioManager.get_inputs()
self.assertEqual(len(inputs), 1)
self.assertEqual(inputs[0].kind, "i2s")
def test_has_microphone_without_sd_in(self):
"""Test has_microphone() returns False when sd_in pin is not configured."""
AudioManager._instance = None
AudioManager(i2s_pins=self.i2s_pins_no_mic, buzzer_instance=None)
self.assertFalse(AudioManager.has_microphone())
def test_has_microphone_no_i2s(self):
"""Test has_microphone() returns False when no I2S is configured."""
AudioManager._instance = None
AudioManager(i2s_pins=None, buzzer_instance=self.buzzer)
self.assertFalse(AudioManager.has_microphone())
def test_default_input(self):
"""Test default input selection."""
default_input = AudioManager.get_default_input()
self.assertIsNotNone(default_input)
self.assertEqual(default_input.kind, "i2s")
def test_is_recording_initially_false(self):
"""Test that is_recording() returns False initially."""
self.assertFalse(AudioManager.is_recording())
recorder = AudioManager.recorder(file_path="test.wav")
self.assertFalse(recorder.is_recording())
def test_record_wav_no_microphone(self):
"""Test that record_wav() fails when no microphone is configured."""
"""Test that recorder() fails when no microphone is configured."""
AudioManager._instance = None
AudioManager(i2s_pins=self.i2s_pins_no_mic, buzzer_instance=None)
result = AudioManager.record_wav("test.wav")
self.assertFalse(result, "record_wav() fails when no microphone is configured")
AudioManager()
with self.assertRaises(ValueError):
AudioManager.recorder(file_path="test.wav").start()
def test_record_wav_no_i2s(self):
AudioManager._instance = None
AudioManager(i2s_pins=None, buzzer_instance=self.buzzer)
result = AudioManager.record_wav("test.wav")
self.assertFalse(result, "record_wav() should fail when no I2S is configured")
AudioManager()
AudioManager.add(AudioManager.Input("mic", "adc", adc_mic_pin=4))
recorder = AudioManager.recorder(file_path="test.wav")
self.assertFalse(recorder.is_recording())
def test_stop_with_no_recording(self):
"""Test that stop() can be called when nothing is recording."""
# Should not raise exception
AudioManager.stop()
self.assertFalse(AudioManager.is_recording())