Skip to content

Commit 8ee337c

Browse files
authored
Merge pull request #13 from FoamyGuy/volume_api
Volume and output interface API
2 parents 9259a09 + 7682a56 commit 8ee337c

File tree

6 files changed

+110
-25
lines changed

6 files changed

+110
-25
lines changed

adafruit_fruitjam/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ def __init__( # noqa: PLR0912,PLR0913,Too many branches,Too many arguments in f
194194
self.sd_check = self.peripherals.sd_check
195195
self.play_file = self.peripherals.play_file
196196
self.stop_play = self.peripherals.stop_play
197+
self.volume = self.peripherals.volume
198+
self.audio_output = self.peripherals.audio_output
197199

198200
self.image_converter_url = self.network.image_converter_url
199201
self.wget = self.network.wget

adafruit_fruitjam/peripherals.py

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import picodvi
4141
import storage
4242
import supervisor
43+
from adafruit_simplemath import map_range
4344
from digitalio import DigitalInOut, Direction, Pull
4445
from neopixel import NeoPixel
4546

@@ -133,13 +134,16 @@ def get_display_config():
133134
class Peripherals:
134135
"""Peripherals Helper Class for the FruitJam Library
135136
137+
:param audio_output: The audio output interface to use 'speaker' or 'headphone'
138+
:param safe_volume_limit: The maximum volume allowed for the audio output. Default is 15
139+
Using higher values can damage some speakers, change at your own risk.
136140
137141
Attributes:
138142
neopixels (NeoPxiels): The NeoPixels on the Fruit Jam board.
139143
See https://circuitpython.readthedocs.io/projects/neopixel/en/latest/api.html
140144
"""
141145

142-
def __init__(self):
146+
def __init__(self, audio_output="headphone", safe_volume_limit=12):
143147
self.neopixels = NeoPixel(board.NEOPIXEL, 5)
144148

145149
self._buttons = []
@@ -155,11 +159,14 @@ def __init__(self):
155159
# set sample rate & bit depth
156160
self._dac.configure_clocks(sample_rate=11030, bit_depth=16)
157161

158-
# use headphones
159-
self._dac.headphone_output = True
160-
self._dac.headphone_volume = -15 # dB
161-
162+
self._audio_output = audio_output
163+
self.audio_output = audio_output
162164
self._audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN)
165+
if safe_volume_limit < 1 or safe_volume_limit > 20:
166+
raise ValueError("safe_volume_limit must be between 1 and 20")
167+
self.safe_volume_limit = safe_volume_limit
168+
self._volume = 7
169+
self._apply_volume()
163170

164171
self._sd_mounted = False
165172
sd_pins_in_use = False
@@ -252,3 +259,61 @@ def stop_play(self):
252259
self.audio.stop()
253260
if self.wavfile is not None:
254261
self.wavfile.close()
262+
263+
@property
264+
def volume(self) -> int:
265+
"""
266+
The volume level of the Fruit Jam audio output. Valid values are 1-20.
267+
"""
268+
return self._volume
269+
270+
@volume.setter
271+
def volume(self, volume_level: int) -> None:
272+
"""
273+
:param volume_level: new volume level 1-20
274+
:return: None
275+
"""
276+
if not (1 <= volume_level <= 20):
277+
raise ValueError("Volume level must be between 1 and 20")
278+
279+
if volume_level > self.safe_volume_limit:
280+
raise ValueError(
281+
f"""Volume level must be less than or equal to
282+
safe_volume_limit: {self.safe_volume_limit}. Using higher values could damage speakers.
283+
To override this limitation set a larger value than {self.safe_volume_limit}
284+
for the safe_volume_limit with the constructor or property."""
285+
)
286+
287+
self._volume = volume_level
288+
self._apply_volume()
289+
290+
@property
291+
def audio_output(self) -> str:
292+
"""
293+
The audio output interface. 'speaker' or 'headphone'
294+
:return:
295+
"""
296+
return self._audio_output
297+
298+
@audio_output.setter
299+
def audio_output(self, audio_output: str) -> None:
300+
"""
301+
302+
:param audio_output: The audio interface to use 'speaker' or 'headphone'.
303+
:return: None
304+
"""
305+
if audio_output == "headphone":
306+
self._dac.headphone_output = True
307+
self._dac.speaker_output = False
308+
elif audio_output == "speaker":
309+
self._dac.headphone_output = False
310+
self._dac.speaker_output = True
311+
else:
312+
raise ValueError("audio_output must be either 'headphone' or 'speaker'")
313+
314+
def _apply_volume(self) -> None:
315+
"""
316+
Map the basic volume level to a db value and set it on the DAC.
317+
"""
318+
db_val = map_range(self._volume, 1, 20, -63, 23)
319+
self._dac.dac_volume = db_val

examples/fruitjam_headphone.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,16 @@
55

66
import adafruit_fruitjam
77

8-
pobj = adafruit_fruitjam.peripherals.Peripherals()
9-
dac = pobj.dac # use Fruit Jam's codec
10-
11-
# Route once for headphones
12-
dac.headphone_output = True
13-
dac.speaker_output = False
8+
pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="headphone")
149

1510
FILES = ["beep.wav", "dip.wav", "rise.wav"]
16-
VOLUMES_DB = [12, 6, 0, -6, -12]
11+
VOLUMES = [5, 7, 10, 11, 12]
1712

1813
while True:
1914
print("\n=== Headphones Test ===")
20-
for vol in VOLUMES_DB:
21-
dac.dac_volume = vol
22-
print(f"Headphones volume: {vol} dB")
15+
for vol in VOLUMES:
16+
pobj.volume = vol
17+
print(f"Headphones volume: {vol}")
2318
for f in FILES:
2419
print(f" -> {f}")
2520
pobj.play_file(f)

examples/fruitjam_speaker.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,16 @@
55

66
import adafruit_fruitjam
77

8-
pobj = adafruit_fruitjam.peripherals.Peripherals()
9-
dac = pobj.dac # use Fruit Jam's codec
10-
11-
# Route once for speaker
12-
dac.headphone_output = False
13-
dac.speaker_output = True
8+
pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="speaker")
149

1510
FILES = ["beep.wav", "dip.wav", "rise.wav"]
16-
VOLUMES_DB = [12, 6, 0, -6, -12]
11+
VOLUMES = [5, 7, 10, 11, 12]
1712

1813
while True:
1914
print("\n=== Speaker Test ===")
20-
for vol in VOLUMES_DB:
21-
dac.dac_volume = vol
22-
print(f"Speaker volume: {vol} dB")
15+
for vol in VOLUMES:
16+
pobj.volume = vol
17+
print(f"Speaker volume: {vol}")
2318
for f in FILES:
2419
print(f" -> {f}")
2520
pobj.play_file(f)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
import time
5+
6+
import synthio
7+
8+
import adafruit_fruitjam
9+
10+
pobj = adafruit_fruitjam.peripherals.Peripherals(audio_output="headphone")
11+
12+
synth = synthio.Synthesizer(sample_rate=44100)
13+
pobj.audio.play(synth)
14+
VOLUMES = [5, 7, 10, 11, 12]
15+
C_major_scale = [60, 62, 64, 65, 67, 69, 71, 72, 71, 69, 67, 65, 64, 62, 60]
16+
while True:
17+
print("\n=== Synthio Test ===")
18+
for vol in VOLUMES:
19+
pobj.volume = vol
20+
print(f"Volume: {vol}")
21+
for note in C_major_scale:
22+
synth.press(note)
23+
time.sleep(0.1)
24+
synth.release(note)
25+
time.sleep(0.05)
26+
27+
time.sleep(1.0)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ adafruit-circuitpython-display-text
1515
adafruit-circuitpython-sd
1616
adafruit-circuitpython-ntp
1717
adafruit-circuitpython-connectionmanager
18+
adafruit-circuitpython-simplemath

0 commit comments

Comments
 (0)