From 9ab7c6615cbff040a9ab2faf1ff983dc5b445376 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Fri, 5 Sep 2025 10:52:00 -0500 Subject: [PATCH 1/6] Check if dac is present and add parameters for sample rate and bit depth --- adafruit_fruitjam/peripherals.py | 115 ++++++++++++++++++------------- 1 file changed, 69 insertions(+), 46 deletions(-) diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 8ef8e89..1fefc27 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -27,6 +27,7 @@ """ import os +import time import adafruit_sdcard import adafruit_tlv320 @@ -135,15 +136,23 @@ class Peripherals: """Peripherals Helper Class for the FruitJam Library :param audio_output: The audio output interface to use 'speaker' or 'headphone' - :param safe_volume_limit: The maximum volume allowed for the audio output. Default is 15 + :param safe_volume_limit: The maximum volume allowed for the audio output. Default is 12. Using higher values can damage some speakers, change at your own risk. + :param sample_rate: The sample rate to play back audio data in hertz. Default is 11025. + :param bit_depth: The bits per sample of the audio data. Supports 8 and 16 bits. Default is 16. Attributes: neopixels (NeoPxiels): The NeoPixels on the Fruit Jam board. See https://circuitpython.readthedocs.io/projects/neopixel/en/latest/api.html """ - def __init__(self, audio_output="headphone", safe_volume_limit=12): + def __init__( + self, + audio_output: str = "headphone", + safe_volume_limit: int = 12, + sample_rate: int = 11025, + bit_depth: int = 16, + ): self.neopixels = NeoPixel(board.NEOPIXEL, 5) self._buttons = [] @@ -154,19 +163,24 @@ def __init__(self, audio_output="headphone", safe_volume_limit=12): self._buttons.append(switch) i2c = board.I2C() - self._dac = adafruit_tlv320.TLV320DAC3100(i2c) + while not i2c.try_lock(): + time.sleep(0.01) + self._dac_present = 0x18 in i2c.scan() - # set sample rate & bit depth - self._dac.configure_clocks(sample_rate=11030, bit_depth=16) + if self._dac_present: + self._dac = adafruit_tlv320.TLV320DAC3100(i2c) + + # set sample rate & bit depth + self._dac.configure_clocks(sample_rate=sample_rate, bit_depth=bit_depth) + + self._audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN) - self._audio_output = audio_output - self.audio_output = audio_output - self._audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN) if safe_volume_limit < 1 or safe_volume_limit > 20: raise ValueError("safe_volume_limit must be between 1 and 20") self.safe_volume_limit = safe_volume_limit - self._volume = 7 - self._apply_volume() + + self.audio_output = audio_output + self._apply_volume(7) self._sd_mounted = False sd_pins_in_use = False @@ -227,14 +241,18 @@ def any_button_pressed(self) -> bool: return True in [not button.value for (i, button) in enumerate(self._buttons)] @property - def dac(self): - return self._dac + def dac_present(self) -> bool: + return self._dac_present + + @property + def dac(self) -> adafruit_tlv320.TLV320DAC3100 | None: + return self._dac if self._dac_present else None @property - def audio(self): - return self._audio + def audio(self) -> audiobusio.I2SOut | None: + return self._audio if self._dac_present else None - def sd_check(self): + def sd_check(self) -> bool: return self._sd_mounted def play_file(self, file_name, wait_to_finish=True): @@ -244,34 +262,36 @@ def play_file(self, file_name, wait_to_finish=True): :param bool wait_to_finish: flag to determine if this is a blocking call """ - - # can't use `with` because we need wavefile to remain open after return - self.wavfile = open(file_name, "rb") - wavedata = audiocore.WaveFile(self.wavfile) - self.audio.play(wavedata) - if not wait_to_finish: - return - while self.audio.playing: - pass - self.wavfile.close() + if self._dac_present: + # can't use `with` because we need wavefile to remain open after return + self.wavfile = open(file_name, "rb") + wavedata = audiocore.WaveFile(self.wavfile) + self.audio.play(wavedata) + if not wait_to_finish: + return + while self.audio.playing: + pass + self.wavfile.close() def play_mp3_file(self, filename): - if self._mp3_decoder is None: - from audiomp3 import MP3Decoder # noqa: PLC0415, import outside top-level + if self._dac_present: + if self._mp3_decoder is None: + from audiomp3 import MP3Decoder # noqa: PLC0415, import outside top-level - self._mp3_decoder = MP3Decoder(filename) - else: - self._mp3_decoder.open(filename) + self._mp3_decoder = MP3Decoder(filename) + else: + self._mp3_decoder.open(filename) - self.audio.play(self._mp3_decoder) - while self.audio.playing: - pass + self.audio.play(self._mp3_decoder) + while self.audio.playing: + pass def stop_play(self): """Stops playing a wav file.""" - self.audio.stop() - if self.wavfile is not None: - self.wavfile.close() + if self._dac_present: + self.audio.stop() + if self.wavfile is not None: + self.wavfile.close() @property def volume(self) -> int: @@ -297,8 +317,7 @@ def volume(self, volume_level: int) -> None: for the safe_volume_limit with the constructor or property.""" ) - self._volume = volume_level - self._apply_volume() + self._apply_volume(volume_level) @property def audio_output(self) -> str: @@ -311,22 +330,26 @@ def audio_output(self) -> str: @audio_output.setter def audio_output(self, audio_output: str) -> None: """ - :param audio_output: The audio interface to use 'speaker' or 'headphone'. :return: None """ if audio_output == "headphone": - self._dac.headphone_output = True - self._dac.speaker_output = False + if self._dac_present: + self._dac.headphone_output = True + self._dac.speaker_output = False elif audio_output == "speaker": - self._dac.headphone_output = False - self._dac.speaker_output = True + if self._dac_present: + self._dac.headphone_output = False + self._dac.speaker_output = True else: raise ValueError("audio_output must be either 'headphone' or 'speaker'") + self._audio_output = audio_output - def _apply_volume(self) -> None: + def _apply_volume(self, volume_level: int) -> None: """ Map the basic volume level to a db value and set it on the DAC. """ - db_val = map_range(self._volume, 1, 20, -63, 23) - self._dac.dac_volume = db_val + self._volume = volume_level + if self._dac_present: + db_val = map_range(self._volume, 1, 20, -63, 23) + self._dac.dac_volume = db_val From eab3b0a79d05d4e1cf03668da28d7515f548ebbd Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Fri, 5 Sep 2025 11:02:00 -0500 Subject: [PATCH 2/6] Remove `None` from return type union --- adafruit_fruitjam/peripherals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 1fefc27..2d6e23d 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -245,11 +245,11 @@ def dac_present(self) -> bool: return self._dac_present @property - def dac(self) -> adafruit_tlv320.TLV320DAC3100 | None: + def dac(self) -> adafruit_tlv320.TLV320DAC3100: return self._dac if self._dac_present else None @property - def audio(self) -> audiobusio.I2SOut | None: + def audio(self) -> audiobusio.I2SOut: return self._audio if self._dac_present else None def sd_check(self) -> bool: From bb877082b360202e6a9879dbbb07d939db09ac92 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Fri, 5 Sep 2025 14:30:19 -0500 Subject: [PATCH 3/6] Configurable dac and audio objects and i2c parameter --- adafruit_fruitjam/peripherals.py | 95 ++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 2d6e23d..54fa2a6 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -140,18 +140,20 @@ class Peripherals: Using higher values can damage some speakers, change at your own risk. :param sample_rate: The sample rate to play back audio data in hertz. Default is 11025. :param bit_depth: The bits per sample of the audio data. Supports 8 and 16 bits. Default is 16. + :param i2c: The I2C bus the audio DAC is connected to. Set as None to disable audio. Attributes: neopixels (NeoPxiels): The NeoPixels on the Fruit Jam board. See https://circuitpython.readthedocs.io/projects/neopixel/en/latest/api.html """ - def __init__( + def __init__( # noqa: PLR0913 self, audio_output: str = "headphone", safe_volume_limit: int = 12, sample_rate: int = 11025, bit_depth: int = 16, + i2c: busio.I2C = board.I2C(), ): self.neopixels = NeoPixel(board.NEOPIXEL, 5) @@ -162,18 +164,27 @@ def __init__( switch.pull = Pull.UP self._buttons.append(switch) - i2c = board.I2C() - while not i2c.try_lock(): - time.sleep(0.01) - self._dac_present = 0x18 in i2c.scan() + if i2c is not None: + while not i2c.try_lock(): + time.sleep(0.01) + dac_present = 0x18 in i2c.scan() + i2c.unlock() - if self._dac_present: + if i2c is not None and dac_present: self._dac = adafruit_tlv320.TLV320DAC3100(i2c) # set sample rate & bit depth self._dac.configure_clocks(sample_rate=sample_rate, bit_depth=bit_depth) - self._audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN) + self._audio = ( + audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN) + if "I2S_BCLK" in dir(board) + else None + ) + + else: + self._dac = None + self._audio = None if safe_volume_limit < 1 or safe_volume_limit > 20: raise ValueError("safe_volume_limit must be between 1 and 20") @@ -240,17 +251,30 @@ def any_button_pressed(self) -> bool: """ return True in [not button.value for (i, button) in enumerate(self._buttons)] - @property - def dac_present(self) -> bool: - return self._dac_present - @property def dac(self) -> adafruit_tlv320.TLV320DAC3100: - return self._dac if self._dac_present else None + return self._dac + + @dac.setter + def dac(self, value: adafruit_tlv320.TLV320DAC3100) -> None: + if self._dac is not None: + self._dac.reset() + del self._dac + self._dac = value + self._apply_audio_output() + self._apply_volume() @property def audio(self) -> audiobusio.I2SOut: - return self._audio if self._dac_present else None + return self._audio + + @audio.setter + def audio(self, value: audiobusio.I2SOut) -> None: + if self._audio is not None: + self._audio.stop() + self._audio.deinit() + del self._audio + self._audio = value def sd_check(self) -> bool: return self._sd_mounted @@ -262,19 +286,19 @@ def play_file(self, file_name, wait_to_finish=True): :param bool wait_to_finish: flag to determine if this is a blocking call """ - if self._dac_present: + if self._audio is not None: # can't use `with` because we need wavefile to remain open after return self.wavfile = open(file_name, "rb") wavedata = audiocore.WaveFile(self.wavfile) - self.audio.play(wavedata) + self._audio.play(wavedata) if not wait_to_finish: return - while self.audio.playing: + while self._audio.playing: pass self.wavfile.close() def play_mp3_file(self, filename): - if self._dac_present: + if self._audio is not None: if self._mp3_decoder is None: from audiomp3 import MP3Decoder # noqa: PLC0415, import outside top-level @@ -282,14 +306,14 @@ def play_mp3_file(self, filename): else: self._mp3_decoder.open(filename) - self.audio.play(self._mp3_decoder) - while self.audio.playing: + self._audio.play(self._mp3_decoder) + while self._audio.playing: pass def stop_play(self): """Stops playing a wav file.""" - if self._dac_present: - self.audio.stop() + if self._audio is not None: + self._audio.stop() if self.wavfile is not None: self.wavfile.close() @@ -333,23 +357,26 @@ def audio_output(self, audio_output: str) -> None: :param audio_output: The audio interface to use 'speaker' or 'headphone'. :return: None """ - if audio_output == "headphone": - if self._dac_present: - self._dac.headphone_output = True - self._dac.speaker_output = False - elif audio_output == "speaker": - if self._dac_present: - self._dac.headphone_output = False - self._dac.speaker_output = True - else: + if audio_output not in {"headphone", "speaker"}: raise ValueError("audio_output must be either 'headphone' or 'speaker'") - self._audio_output = audio_output + self._apply_audio_output(audio_output) + + def _apply_audio_output(self, audio_output: str = None) -> None: + """ + Assign the output of the dac based on the desired setting. + """ + if audio_output is not None: + self._audio_output = audio_output + if self._dac is not None: + self._dac.headphone_output = self._audio_output == "headphone" + self._dac.speaker_output = self._audio_output == "speaker" - def _apply_volume(self, volume_level: int) -> None: + def _apply_volume(self, volume_level: int = None) -> None: """ Map the basic volume level to a db value and set it on the DAC. """ - self._volume = volume_level - if self._dac_present: + if volume_level is not None: + self._volume = volume_level + if self._dac is not None: db_val = map_range(self._volume, 1, 20, -63, 23) self._dac.dac_volume = db_val From 05b5c27100935180c9a4d287c89c774cd33c44b2 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Fri, 5 Sep 2025 14:42:49 -0500 Subject: [PATCH 4/6] Use `None` as default value and allow `False` to disable --- adafruit_fruitjam/peripherals.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 54fa2a6..783ad5d 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -140,7 +140,7 @@ class Peripherals: Using higher values can damage some speakers, change at your own risk. :param sample_rate: The sample rate to play back audio data in hertz. Default is 11025. :param bit_depth: The bits per sample of the audio data. Supports 8 and 16 bits. Default is 16. - :param i2c: The I2C bus the audio DAC is connected to. Set as None to disable audio. + :param i2c: The I2C bus the audio DAC is connected to. Set as False to disable audio. Attributes: neopixels (NeoPxiels): The NeoPixels on the Fruit Jam board. @@ -153,7 +153,7 @@ def __init__( # noqa: PLR0913 safe_volume_limit: int = 12, sample_rate: int = 11025, bit_depth: int = 16, - i2c: busio.I2C = board.I2C(), + i2c: busio.I2C = None, ): self.neopixels = NeoPixel(board.NEOPIXEL, 5) @@ -164,13 +164,15 @@ def __init__( # noqa: PLR0913 switch.pull = Pull.UP self._buttons.append(switch) - if i2c is not None: + if i2c is None: + i2c = board.I2C() + if i2c is not False: while not i2c.try_lock(): time.sleep(0.01) dac_present = 0x18 in i2c.scan() i2c.unlock() - if i2c is not None and dac_present: + if i2c is not False and dac_present: self._dac = adafruit_tlv320.TLV320DAC3100(i2c) # set sample rate & bit depth From ca3d7aaf90d2da9a9e27ea520192cb4495e1ea92 Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Fri, 5 Sep 2025 14:45:36 -0500 Subject: [PATCH 5/6] Ignore PLR0912 too many branches --- adafruit_fruitjam/peripherals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 783ad5d..9f84f9f 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -147,7 +147,7 @@ class Peripherals: See https://circuitpython.readthedocs.io/projects/neopixel/en/latest/api.html """ - def __init__( # noqa: PLR0913 + def __init__( # noqa: PLR0913, PLR0912 self, audio_output: str = "headphone", safe_volume_limit: int = 12, From 98366650e397a4f99569c4b39c0ef2c9f13f2d6b Mon Sep 17 00:00:00 2001 From: Cooper Dalrymple Date: Sun, 7 Sep 2025 08:29:47 -0500 Subject: [PATCH 6/6] Initialize i2s bus independently from dac --- adafruit_fruitjam/peripherals.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 9f84f9f..8e80c89 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -166,26 +166,25 @@ def __init__( # noqa: PLR0913, PLR0912 if i2c is None: i2c = board.I2C() - if i2c is not False: + if i2c is False: + self._dac = None + else: while not i2c.try_lock(): time.sleep(0.01) dac_present = 0x18 in i2c.scan() i2c.unlock() - if i2c is not False and dac_present: - self._dac = adafruit_tlv320.TLV320DAC3100(i2c) - - # set sample rate & bit depth - self._dac.configure_clocks(sample_rate=sample_rate, bit_depth=bit_depth) - - self._audio = ( - audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN) - if "I2S_BCLK" in dir(board) - else None - ) + if dac_present: + self._dac = adafruit_tlv320.TLV320DAC3100(i2c) + self._dac.configure_clocks( # set sample rate & bit depth + sample_rate=sample_rate, bit_depth=bit_depth + ) + else: + self._dac = None + if "I2S_BCLK" in dir(board) and "I2S_WS" in dir(board) and "I2S_DIN" in dir(board): + self._audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN) else: - self._dac = None self._audio = None if safe_volume_limit < 1 or safe_volume_limit > 20: