Skip to content

Commit 7f39976

Browse files
rjtokenringdsammaruga
authored andcommitted
Work ok effects
1 parent 30e5623 commit 7f39976

File tree

6 files changed

+209
-21
lines changed

6 files changed

+209
-21
lines changed

src/arduino/app_bricks/sound_generator/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Sound Generator Brick
22

3-
Play sounds and melodies
3+
Play sounds and melodies
44

55
## Code example and usage
66

@@ -35,7 +35,7 @@ waveform can be customized to change effect. For example, for a retro-gaming sou
3535
player = SoundGenerator(wave_form="square")
3636
```
3737

38-
instead, to have a more "rock" like sound, you can add effect
38+
instead, to have a more "rock" like sound, you can add effects like:
3939

4040
```python
4141
player = SoundGenerator(sound_effects=[SoundEffect.adsr(), SoundEffect.overdrive(drive=180.0), SoundEffect.chorus(depth_ms=15, rate_hz=0.2, mix=0.4)])

src/arduino/app_bricks/sound_generator/__init__.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import threading
88
from typing import Iterable
99
import numpy as np
10+
import time
1011

1112
from .effects import *
1213
from .loaders import ABCNotationLoader
@@ -73,13 +74,14 @@ def __init__(
7374
signal (e.g., [SoundEffect.adsr()]). See SoundEffect class for available effects.
7475
"""
7576

76-
self._wave_gen = WaveGenerator(sample_rate=self.SAMPLE_RATE, wave_form=wave_form)
77+
self._cfg_lock = threading.Lock()
78+
self._init_wave_generator(wave_form)
79+
7780
self._bpm = bpm
7881
self.time_signature = time_signature
7982
self._master_volume = master_volume
8083
self._sound_effects = sound_effects
8184

82-
self._cfg_lock = threading.Lock()
8385
self._notes = {}
8486
for octave in range(octaves):
8587
notes = self._fill_node_frequencies(octave)
@@ -91,6 +93,19 @@ def start(self):
9193
def stop(self):
9294
pass
9395

96+
def _init_wave_generator(self, wave_form: str):
97+
with self._cfg_lock:
98+
self._wave_gen = WaveGenerator(sample_rate=self.SAMPLE_RATE, wave_form=wave_form)
99+
100+
def set_wave_form(self, wave_form: str):
101+
"""
102+
Set the wave form type for sound generation.
103+
Args:
104+
wave_form (str): The type of wave form to generate. Supported values
105+
are "sine", "square", "triangle" and "sawtooth".
106+
"""
107+
self._init_wave_generator(wave_form)
108+
94109
def set_master_volume(self, volume: float):
95110
"""
96111
Set the master volume level.
@@ -368,14 +383,14 @@ def play_tone(self, note: str, duration: float = 0.25, volume: float = None) ->
368383
data = self._apply_sound_effects(data, frequency)
369384
return self._to_bytes(data)
370385

371-
def play_abc(self, abc_string: str, volume: float = None) -> Iterable[bytes]:
386+
def play_abc(self, abc_string: str, volume: float = None) -> Iterable[tuple[bytes, float]]:
372387
"""
373388
Play a sequence of musical notes defined in ABC notation.
374389
Args:
375390
abc_string (str): ABC notation string defining the sequence of notes.
376391
volume (float, optional): Volume level (0.0 to 1.0). If None, uses master volume.
377392
Returns:
378-
Iterable[bytes]: An iterable yielding the audio blocks of the played notes (float32).
393+
Iterable[tuple[bytes, float]]: An iterable yielding the audio blocks of the played notes (float32) and its duration.
379394
"""
380395
if not abc_string or abc_string.strip() == "":
381396
return
@@ -387,7 +402,7 @@ def play_abc(self, abc_string: str, volume: float = None) -> Iterable[bytes]:
387402
if frequency is not None and frequency >= 0.0:
388403
data = self._wave_gen.generate_block(float(frequency), duration, volume)
389404
data = self._apply_sound_effects(data, frequency)
390-
yield self._to_bytes(data)
405+
yield (self._to_bytes(data), duration)
391406

392407

393408
@brick
@@ -424,20 +439,26 @@ def __init__(
424439
sound_effects=sound_effects,
425440
)
426441

442+
self._started = threading.Event()
427443
if output_device is None:
428-
self._self_created_device = True
444+
self.external_speaker = False
429445
self._output_device = Speaker(sample_rate=self.SAMPLE_RATE, format="FLOAT_LE")
446+
self.start()
430447
else:
431-
self._self_created_device = False
448+
self.external_speaker = True
432449
self._output_device = output_device
433450

434451
def start(self):
435-
if self._self_created_device:
452+
if self._started.is_set():
453+
return
454+
if self.external_speaker == False:
436455
self._output_device.start(notify_if_started=False)
456+
self._started.set()
437457

438458
def stop(self):
439-
if self._self_created_device:
459+
if self.external_speaker == False:
440460
self._output_device.stop()
461+
self._started.clear()
441462

442463
def set_master_volume(self, volume: float):
443464
"""
@@ -508,15 +529,20 @@ def play_tone(self, note: str, duration: float = 0.25, volume: float = None):
508529
data = super().play_tone(note, duration, volume)
509530
self._output_device.play(data, block_on_queue=False)
510531

511-
def play_abc(self, abc_string: str, volume: float = None):
532+
def play_abc(self, abc_string: str, volume: float = None, wait_completion: bool = False):
512533
"""
513534
Play a sequence of musical notes defined in ABC notation.
514535
Args:
515536
abc_string (str): ABC notation string defining the sequence of notes.
516537
volume (float, optional): Volume level (0.0 to 1.0). If None, uses master volume.
538+
wait_completion (bool): If True, block until the entire sequence has been played.
517539
"""
518540
if not abc_string or abc_string.strip() == "":
519541
return
520542
player = super().play_abc(abc_string, volume)
521-
for data in player:
522-
self._output_device.play(data, block_on_queue=False)
543+
overall_duration = 0.0
544+
for data, duration in player:
545+
self._output_device.play(data, block_on_queue=True)
546+
overall_duration += duration
547+
if wait_completion:
548+
time.sleep(overall_duration)

src/arduino/app_bricks/sound_generator/effects.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class SoundEffect:
99
@staticmethod
10-
def overdrive(drive: float = 1.0) -> np.ndarray:
10+
def overdrive(drive: float = 100.0) -> np.ndarray:
1111
"""
1212
Apply overdrive effect to the audio signal.
1313
Args:
@@ -29,12 +29,11 @@ def apply(self, signal: np.ndarray) -> np.ndarray:
2929
return SoundEffectOverdrive(drive)
3030

3131
@staticmethod
32-
def chorus(fs: int = 16000, depth_ms=10, rate_hz: float = 0.25, mix: float = 0.5) -> np.ndarray:
32+
def chorus(depth_ms=10, rate_hz: float = 0.25, mix: float = 0.5) -> np.ndarray:
3333
"""
3434
Apply chorus effect to the audio signal.
3535
Args:
3636
signal (np.ndarray): Input audio signal.
37-
fs (int): Sampling frequency in Hz.
3837
depth_ms (float): Depth of the chorus effect in milliseconds.
3938
rate_hz (float): Rate of the LFO in Hz.
4039
mix (float): Mix ratio between dry and wet signals (0.0 to 1.0).
@@ -43,8 +42,8 @@ def chorus(fs: int = 16000, depth_ms=10, rate_hz: float = 0.25, mix: float = 0.5
4342
"""
4443

4544
class SoundEffectChorus:
46-
def __init__(self, fs: int = 16000, depth_ms: int = 10, rate_hz: float = 0.25, mix: float = 0.5):
47-
self.fs = fs
45+
def __init__(self, depth_ms: int = 10, rate_hz: float = 0.25, mix: float = 0.5):
46+
self.fs = 16000 # sample rate
4847
self.depth_ms = depth_ms
4948
self.rate_hz = rate_hz
5049
self.mix = mix
@@ -67,14 +66,13 @@ def apply(self, signal: np.ndarray) -> np.ndarray:
6766
# mix dry/wet
6867
return ((1 - self.mix) * signal + self.mix * out).astype(np.float32)
6968

70-
return SoundEffectChorus(fs, depth_ms, rate_hz, mix)
69+
return SoundEffectChorus(depth_ms, rate_hz, mix)
7170

7271
@staticmethod
7372
def adsr(attack: float = 0.015, decay: float = 0.2, sustain: float = 0.5, release: float = 0.35):
7473
"""
7574
Apply ADSR (attack/decay/sustain/release) envelope to the audio signal.
7675
Args:
77-
fs (int): Sampling frequency in Hz.
7876
attack (float): Attack time in seconds.
7977
decay (float): Decay time in seconds.
8078
sustain (float): Sustain level (0.0 to 1.0).
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA <http://www.arduino.cc>
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
# EXAMPLE_NAME: Stream a sequence of notes over websocket via WebUI
6+
import time
7+
from arduino.app_utils import *
8+
from arduino.app_bricks.web_ui import WebUI
9+
from arduino.app_bricks.sound_generator import SoundGeneratorStreamer, SoundEffect
10+
11+
ui = WebUI()
12+
13+
player = SoundGeneratorStreamer(master_volume=1.0, wave_form="square", bpm=120, sound_effects=[SoundEffect.adsr()])
14+
15+
tune_sequence = [
16+
("E5", 0.125),
17+
("E5", 0.125),
18+
("REST", 0.125),
19+
("E5", 0.125),
20+
("REST", 0.125),
21+
("C5", 0.125),
22+
("E5", 0.125),
23+
("REST", 0.125),
24+
("G5", 0.25),
25+
("REST", 0.25),
26+
("G4", 0.25),
27+
("REST", 0.25),
28+
("C5", 0.25),
29+
("REST", 0.125),
30+
("G4", 0.25),
31+
("REST", 0.125),
32+
("E4", 0.25),
33+
("REST", 0.125),
34+
("A4", 0.25),
35+
("B4", 0.25),
36+
("Bb4", 0.125),
37+
("A4", 0.25),
38+
("G4", 0.125),
39+
("E5", 0.125),
40+
("G5", 0.125),
41+
("A5", 0.25),
42+
("F5", 0.125),
43+
("G5", 0.125),
44+
("REST", 0.125),
45+
("E5", 0.25),
46+
("C5", 0.125),
47+
("D5", 0.125),
48+
("B4", 0.25),
49+
]
50+
51+
52+
def user_lp():
53+
while True:
54+
overall_time = 0
55+
for note, duration in tune_sequence:
56+
frame = player.play_tone(note, duration)
57+
entry = {
58+
"raw_data": frame,
59+
}
60+
ui.send_message("audio_frame", entry)
61+
overall_time += duration
62+
63+
time.sleep(overall_time) # wait for the whole sequence to finish before restarting
64+
65+
66+
App.run(user_loop=user_lp)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA <http://www.arduino.cc>
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
# EXAMPLE_NAME: Play music in ABC notation
6+
from arduino.app_bricks.sound_generator import SoundGenerator, SoundEffect
7+
from arduino.app_utils import App
8+
9+
player = SoundGenerator(sound_effects=[SoundEffect.adsr()])
10+
11+
12+
def play_melody():
13+
abc_music = """
14+
X:1
15+
T:Twinkle, Twinkle Little Star - #11
16+
T:Alphabet Song
17+
C:Traditional Kid's Song
18+
M:4/4
19+
L:1/4
20+
K:D
21+
|"D"D D A A|"G"B B "D"A2
22+
|"G"G G "D"F F|"A"E/2E/2E/2E/2 "D"D2
23+
|A A "G"G G|"D"F F "A"E2
24+
|"D"A A "G"G G|"D"F F "A"E2
25+
|"D"D D A A|"G"B B "D"A2
26+
|"G"G G "D"F F|"A"E E "D"D2|
27+
"""
28+
player.play_abc(abc_music, wait_completion=True)
29+
30+
31+
App.run(user_loop=play_melody)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA <http://www.arduino.cc>
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
# EXAMPLE_NAME: Play a sequence using effects
6+
from arduino.app_bricks.sound_generator import SoundGenerator, SoundEffect
7+
from arduino.app_utils import App
8+
9+
player = SoundGenerator()
10+
11+
tune_sequence = [
12+
("A4", 0.25),
13+
("C5", 0.25),
14+
("E5", 0.25),
15+
("C5", 0.25),
16+
("A4", 0.25),
17+
("C5", 0.25),
18+
("E5", 0.25),
19+
("REST", 0.25),
20+
("G4", 0.25),
21+
("B4", 0.25),
22+
("D5", 0.25),
23+
("B4", 0.25),
24+
("G4", 0.25),
25+
("B4", 0.25),
26+
("D5", 0.25),
27+
("REST", 0.25),
28+
("A4", 0.25),
29+
("A4", 0.25),
30+
("C5", 0.25),
31+
("E5", 0.25),
32+
("F5", 0.5),
33+
("E5", 0.25),
34+
("REST", 0.25),
35+
("D5", 0.25),
36+
("C5", 0.25),
37+
("B4", 0.25),
38+
("A4", 0.25),
39+
("G4", 0.5),
40+
("B4", 0.5),
41+
("REST", 1),
42+
]
43+
44+
# Play as a retro-game sound
45+
player.set_wave_form("square")
46+
player.set_effects([SoundEffect.adsr()]) # For a more synththetic sound, add SoundEffect.bitcrusher() effect
47+
for note, duration in tune_sequence:
48+
player.play_tone(note, duration)
49+
50+
# Play with distortion
51+
player.set_wave_form("sine")
52+
player.set_effects([SoundEffect.adsr(), SoundEffect.chorus(), SoundEffect.overdrive(drive=200.0)])
53+
for note, duration in tune_sequence:
54+
player.play_tone(note, duration)
55+
56+
# Vibrato effect
57+
player.set_effects([SoundEffect.adsr(), SoundEffect.vibrato()])
58+
for note, duration in tune_sequence:
59+
player.play_tone(note, duration)
60+
61+
# Tremolo effect
62+
player.set_wave_form("triangle")
63+
player.set_effects([SoundEffect.adsr(), SoundEffect.tremolo(), SoundEffect.chorus()])
64+
for note, duration in tune_sequence:
65+
player.play_tone(note, duration)
66+
67+
App.run()

0 commit comments

Comments
 (0)