From f35068b0d386671085f6e8ffeb9cb7cf7a5ddd3b Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 19 Aug 2025 11:31:14 -0500 Subject: [PATCH 01/12] audio volume and interface APIs --- adafruit_fruitjam/peripherals.py | 75 +++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 461bbcf..3369797 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -38,6 +38,7 @@ import displayio import framebufferio import picodvi +import simpleio import storage import supervisor from digitalio import DigitalInOut, Direction, Pull @@ -133,13 +134,16 @@ def get_display_config(): 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 + Using higher values can damage some speakers, change at your own risk. Attributes: neopixels (NeoPxiels): The NeoPixels on the Fruit Jam board. See https://circuitpython.readthedocs.io/projects/neopixel/en/latest/api.html """ - def __init__(self): + def __init__(self, audio_output="headphone", safe_volume_limit=15): self.neopixels = NeoPixel(board.NEOPIXEL, 5) self._buttons = [] @@ -155,11 +159,13 @@ def __init__(self): # set sample rate & bit depth self._dac.configure_clocks(sample_rate=11030, bit_depth=16) - # use headphones - self._dac.headphone_output = True - self._dac.headphone_volume = -15 # dB - + 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 = 13 + self._apply_volume() self._sd_mounted = False sd_pins_in_use = False @@ -252,3 +258,62 @@ def stop_play(self): self.audio.stop() if self.wavfile is not None: self.wavfile.close() + + @property + def volume(self) -> int: + """ + The volume level of the Fruit Jam audio output. Valid values are 1-20. + """ + return self._volume + + @volume.setter + def volume(self, volume_level: int) -> None: + """ + :param volume_level: new volume level 1-20 + :return: None + """ + if volume_level < 1 or volume_level > 20: + raise ValueError("Volume level must be between 1 and 20") + + if volume_level > self.safe_volume_limit: + raise ValueError( + f"Volume level must be less than or equal to " + + f"safe_volume_limit: {self.safe_volume_limit}. " + + f"Using higher values could damage speakers. " + + f"To override this limitation pass a value larger than 15 " + + f"for the safe_volume_limit argument of the constructor." + ) + + self._volume = volume_level + self._apply_volume() + + @property + def audio_output(self) -> str: + """ + The audio output interface. 'speaker' or 'headphone' + :return: + """ + return self._audio_output + + @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 + elif audio_output == "speaker": + self._dac.headphone_output = False + self._dac.speaker_output = True + else: + raise ValueError("audio_output must be either 'headphone' or 'speaker'") + + def _apply_volume(self) -> None: + """ + Map the basic volume level to a db value and set it on the DAC. + """ + db_val = simpleio.map_range(self._volume, 1, 20, -63, 23) + self._dac.dac_volume = db_val From c332f74892329cfb4f7e45dfa70037d1d4065067 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 19 Aug 2025 11:32:11 -0500 Subject: [PATCH 02/12] add simpleio to reqs --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index d26fdd0..bf6cdf7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ adafruit-circuitpython-requests adafruit-circuitpython-bitmap-font adafruit-circuitpython-display-text adafruit-circuitpython-sd +adafruit-circuitpython-simpleio From 9825076bcfa29c5c4c6d5981715e9530f8cd41fd Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 19 Aug 2025 16:07:54 -0500 Subject: [PATCH 03/12] update audio examples to use new volume API. --- examples/fruitjam_headphone.py | 15 +++++---------- examples/fruitjam_speaker.py | 15 +++++---------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/examples/fruitjam_headphone.py b/examples/fruitjam_headphone.py index c5d3a94..434f6b6 100644 --- a/examples/fruitjam_headphone.py +++ b/examples/fruitjam_headphone.py @@ -5,21 +5,16 @@ import adafruit_fruitjam -pobj = adafruit_fruitjam.peripherals.Peripherals() -dac = pobj.dac # use Fruit Jam's codec - -# Route once for headphones -dac.headphone_output = True -dac.speaker_output = False +pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="headphone") FILES = ["beep.wav", "dip.wav", "rise.wav"] -VOLUMES_DB = [12, 6, 0, -6, -12] +VOLUMES = [5, 7, 10, 12, 15] while True: print("\n=== Headphones Test ===") - for vol in VOLUMES_DB: - dac.dac_volume = vol - print(f"Headphones volume: {vol} dB") + for vol in VOLUMES: + pobj.volume = vol + print(f"Headphones volume: {vol}") for f in FILES: print(f" -> {f}") pobj.play_file(f) diff --git a/examples/fruitjam_speaker.py b/examples/fruitjam_speaker.py index 02e75fa..f7a61b3 100644 --- a/examples/fruitjam_speaker.py +++ b/examples/fruitjam_speaker.py @@ -5,21 +5,16 @@ import adafruit_fruitjam -pobj = adafruit_fruitjam.peripherals.Peripherals() -dac = pobj.dac # use Fruit Jam's codec - -# Route once for speaker -dac.headphone_output = False -dac.speaker_output = True +pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="speaker") FILES = ["beep.wav", "dip.wav", "rise.wav"] -VOLUMES_DB = [12, 6, 0, -6, -12] +VOLUMES = [5, 7, 10, 12, 15] while True: print("\n=== Speaker Test ===") - for vol in VOLUMES_DB: - dac.dac_volume = vol - print(f"Speaker volume: {vol} dB") + for vol in VOLUMES: + pobj.volume = vol + print(f"Speaker volume: {vol}") for f in FILES: print(f" -> {f}") pobj.play_file(f) From a722ba18350d291a57cb6242d151e1ae4c2e2691 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 19 Aug 2025 16:10:41 -0500 Subject: [PATCH 04/12] convenience accessors for volume and audio_output properties on main fruit_jam object. --- adafruit_fruitjam/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/adafruit_fruitjam/__init__.py b/adafruit_fruitjam/__init__.py index 57240b9..5727d2b 100644 --- a/adafruit_fruitjam/__init__.py +++ b/adafruit_fruitjam/__init__.py @@ -194,6 +194,8 @@ def __init__( # noqa: PLR0912,PLR0913,Too many branches,Too many arguments in f self.sd_check = self.peripherals.sd_check self.play_file = self.peripherals.play_file self.stop_play = self.peripherals.stop_play + self.volume = self.peripherals.volume + self.audio_output = self.peripherals.audio_output self.image_converter_url = self.network.image_converter_url self.wget = self.network.wget From 499835660468cead62c703bceabf4d617dd7c361 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 19 Aug 2025 16:31:11 -0500 Subject: [PATCH 05/12] add synthio example. --- examples/fruitjam_synthio_speaker.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 examples/fruitjam_synthio_speaker.py diff --git a/examples/fruitjam_synthio_speaker.py b/examples/fruitjam_synthio_speaker.py new file mode 100644 index 0000000..235267c --- /dev/null +++ b/examples/fruitjam_synthio_speaker.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT +import time + +import synthio + +import adafruit_fruitjam + +pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="headphone") + +synth = synthio.Synthesizer(sample_rate=44100) +pobj.audio.play(synth) +VOLUMES = [5, 7, 10, 12, 15] +C_major_scale = [60, 62, 64, 65, 67, 69, 71, 72, 71, 69, 67, 65, 64, 62, 60] +while True: + print("\n=== Synthio Test ===") + for vol in VOLUMES: + pobj.volume = vol + print(f"Speaker volume: {vol}") + for note in C_major_scale: + synth.press(note) + time.sleep(0.1) + synth.release(note) + time.sleep(0.05) + + time.sleep(1.0) From eb35d69e1ba437242455d82d1cdcbdd6a781e52f Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 20 Aug 2025 07:54:10 -0500 Subject: [PATCH 06/12] update printed volume label --- examples/fruitjam_synthio_speaker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/fruitjam_synthio_speaker.py b/examples/fruitjam_synthio_speaker.py index 235267c..f5c4d40 100644 --- a/examples/fruitjam_synthio_speaker.py +++ b/examples/fruitjam_synthio_speaker.py @@ -17,7 +17,7 @@ print("\n=== Synthio Test ===") for vol in VOLUMES: pobj.volume = vol - print(f"Speaker volume: {vol}") + print(f"Volume: {vol}") for note in C_major_scale: synth.press(note) time.sleep(0.1) From 3f33c18ddf70cd8a50ab446a17b197f9c78119dc Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 27 Aug 2025 10:32:40 -0500 Subject: [PATCH 07/12] fix _audio_output initialization error --- adafruit_fruitjam/peripherals.py | 1 + 1 file changed, 1 insertion(+) diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 3369797..7d80d1f 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -159,6 +159,7 @@ def __init__(self, audio_output="headphone", safe_volume_limit=15): # set sample rate & bit depth self._dac.configure_clocks(sample_rate=11030, bit_depth=16) + 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: From f2325f3811e4eb0e4b95527d62a86d7c2c8fcd59 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 27 Aug 2025 10:42:40 -0500 Subject: [PATCH 08/12] lower default volume --- 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 7d80d1f..16a5dc0 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -165,7 +165,7 @@ def __init__(self, audio_output="headphone", safe_volume_limit=15): 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 = 13 + self._volume = 7 self._apply_volume() self._sd_mounted = False From c41e65bdeb1528a694799c5f02653148b52e7fee Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 27 Aug 2025 10:46:26 -0500 Subject: [PATCH 09/12] lower default volume limit, update examples, change warning message --- adafruit_fruitjam/peripherals.py | 4 ++-- examples/fruitjam_headphone.py | 2 +- examples/fruitjam_speaker.py | 2 +- examples/fruitjam_synthio_speaker.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 16a5dc0..0445e20 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -143,7 +143,7 @@ class Peripherals: See https://circuitpython.readthedocs.io/projects/neopixel/en/latest/api.html """ - def __init__(self, audio_output="headphone", safe_volume_limit=15): + def __init__(self, audio_output="headphone", safe_volume_limit=12): self.neopixels = NeoPixel(board.NEOPIXEL, 5) self._buttons = [] @@ -281,7 +281,7 @@ def volume(self, volume_level: int) -> None: f"Volume level must be less than or equal to " + f"safe_volume_limit: {self.safe_volume_limit}. " + f"Using higher values could damage speakers. " - + f"To override this limitation pass a value larger than 15 " + + f"To override this limitation pass a value larger {self.safe_volume_limit} " + f"for the safe_volume_limit argument of the constructor." ) diff --git a/examples/fruitjam_headphone.py b/examples/fruitjam_headphone.py index 434f6b6..decd1e5 100644 --- a/examples/fruitjam_headphone.py +++ b/examples/fruitjam_headphone.py @@ -8,7 +8,7 @@ pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="headphone") FILES = ["beep.wav", "dip.wav", "rise.wav"] -VOLUMES = [5, 7, 10, 12, 15] +VOLUMES = [5, 7, 10, 11, 12] while True: print("\n=== Headphones Test ===") diff --git a/examples/fruitjam_speaker.py b/examples/fruitjam_speaker.py index f7a61b3..64ec015 100644 --- a/examples/fruitjam_speaker.py +++ b/examples/fruitjam_speaker.py @@ -8,7 +8,7 @@ pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="speaker") FILES = ["beep.wav", "dip.wav", "rise.wav"] -VOLUMES = [5, 7, 10, 12, 15] +VOLUMES = [5, 7, 10, 11, 12] while True: print("\n=== Speaker Test ===") diff --git a/examples/fruitjam_synthio_speaker.py b/examples/fruitjam_synthio_speaker.py index f5c4d40..a44ce28 100644 --- a/examples/fruitjam_synthio_speaker.py +++ b/examples/fruitjam_synthio_speaker.py @@ -11,7 +11,7 @@ synth = synthio.Synthesizer(sample_rate=44100) pobj.audio.play(synth) -VOLUMES = [5, 7, 10, 12, 15] +VOLUMES = [5, 7, 10, 11, 12] C_major_scale = [60, 62, 64, 65, 67, 69, 71, 72, 71, 69, 67, 65, 64, 62, 60] while True: print("\n=== Synthio Test ===") From 45d7cc12322dfe97ba9008830191b6a8e89e1089 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Wed, 27 Aug 2025 18:09:26 -0500 Subject: [PATCH 10/12] simplified condition logic Co-authored-by: Dan Halbert --- 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 0445e20..44015e4 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -273,7 +273,7 @@ def volume(self, volume_level: int) -> None: :param volume_level: new volume level 1-20 :return: None """ - if volume_level < 1 or volume_level > 20: + if not (1 <= volume_level <= 20): raise ValueError("Volume level must be between 1 and 20") if volume_level > self.safe_volume_limit: From 733ef1b9257346a102c0d5b44b93f68596ada41d Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 28 Aug 2025 08:40:51 -0500 Subject: [PATCH 11/12] use adafruit_simplemath instead of simpleio. Use multi-line string for warning message --- adafruit_fruitjam/peripherals.py | 14 +++++++------- requirements.txt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 0445e20..64ca900 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -38,9 +38,9 @@ import displayio import framebufferio import picodvi -import simpleio import storage import supervisor +from adafruit_simplemath import map_range from digitalio import DigitalInOut, Direction, Pull from neopixel import NeoPixel @@ -278,11 +278,11 @@ def volume(self, volume_level: int) -> None: if volume_level > self.safe_volume_limit: raise ValueError( - f"Volume level must be less than or equal to " - + f"safe_volume_limit: {self.safe_volume_limit}. " - + f"Using higher values could damage speakers. " - + f"To override this limitation pass a value larger {self.safe_volume_limit} " - + f"for the safe_volume_limit argument of the constructor." + f"""Volume level must be less than or equal to +safe_volume_limit: {self.safe_volume_limit}. +Using higher values could damage speakers. +To override this limitation pass a value larger {self.safe_volume_limit} +for the safe_volume_limit argument of the constructor.""" ) self._volume = volume_level @@ -316,5 +316,5 @@ def _apply_volume(self) -> None: """ Map the basic volume level to a db value and set it on the DAC. """ - db_val = simpleio.map_range(self._volume, 1, 20, -63, 23) + db_val = map_range(self._volume, 1, 20, -63, 23) self._dac.dac_volume = db_val diff --git a/requirements.txt b/requirements.txt index 38aefe0..bd4358c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,4 @@ adafruit-circuitpython-display-text adafruit-circuitpython-sd adafruit-circuitpython-ntp adafruit-circuitpython-connectionmanager -adafruit-circuitpython-simpleio +adafruit-circuitpython-simplemath From 7682a56c51895cc0408feb9c8eca512994f78b80 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 28 Aug 2025 08:44:48 -0500 Subject: [PATCH 12/12] clarify wording in the volume warning --- adafruit_fruitjam/peripherals.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/adafruit_fruitjam/peripherals.py b/adafruit_fruitjam/peripherals.py index 5e3a0b7..35e03c5 100644 --- a/adafruit_fruitjam/peripherals.py +++ b/adafruit_fruitjam/peripherals.py @@ -279,10 +279,9 @@ def volume(self, volume_level: int) -> None: if volume_level > self.safe_volume_limit: raise ValueError( f"""Volume level must be less than or equal to -safe_volume_limit: {self.safe_volume_limit}. -Using higher values could damage speakers. -To override this limitation pass a value larger {self.safe_volume_limit} -for the safe_volume_limit argument of the constructor.""" +safe_volume_limit: {self.safe_volume_limit}. Using higher values could damage speakers. +To override this limitation set a larger value than {self.safe_volume_limit} +for the safe_volume_limit with the constructor or property.""" ) self._volume = volume_level