Skip to content

Commit 3d97847

Browse files
BeanRepodsammaruga
andauthored
Replace SineGenerator with WaveGenerator Brick in Theremin (#8)
* add WaveGenerator in Theremin * update volume handling and readme --------- Co-authored-by: Dario Sammaruga <d.sammaruga@ext.arduino.cc>
1 parent 8fe9cbd commit 3d97847

File tree

4 files changed

+69
-130
lines changed

4 files changed

+69
-130
lines changed

examples/theremin/README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This example generates real-time audio by creating sine waves at varying frequen
1212
## Bricks Used
1313

1414
- `web_ui`: Brick that provides the web interface and a WebSocket channel for real-time control of the theremin.
15+
- `wave_generator`: Brick that generates continuous audio waveforms and streams them to the USB speaker with smooth frequency and amplitude transitions.
1516

1617

1718
## Hardware and Software Requirements
@@ -51,30 +52,29 @@ This example generates real-time audio by creating sine waves at varying frequen
5152

5253
## How it Works
5354

54-
The application creates a real-time audio synthesizer controlled by a web interface. User interactions on the webpage are sent to the Python backend via a WebSocket. The backend then calculates the audio parameters, generates a sine wave, and streams the audio data directly to the connected **USB** audio device.
55+
The application creates a real-time audio synthesizer controlled by a web interface. User interactions on the webpage are sent to the Python backend via a WebSocket. The backend uses the `wave_generator` brick to continuously generate and stream audio to the connected **USB** audio device with smooth transitions.
5556

5657
- **User Interaction**: The frontend captures mouse or touch coordinates within a designated "play area".
5758
- **Real-time Communication**: These coordinates are sent to the Python backend in real-time using the `web_ui` Brick's WebSocket channel.
58-
- **Audio Synthesis**: The backend maps the X-coordinate to **frequency** and the Y-coordinate to **amplitude**. It uses a sine wave generator to create small blocks of audio data based on these parameters.
59-
- **Audio Output**: The generated audio blocks are continuously streamed to the **USB** audio device, creating a smooth and responsive sound.
59+
- **Audio Synthesis**: The backend maps the X-coordinate to **frequency** and the Y-coordinate to **amplitude**, then updates the `wave_generator` brick's state. The brick handles smooth transitions using configurable envelope parameters (attack, release, glide).
60+
- **Audio Output**: The `wave_generator` brick runs continuously in a background thread, generating audio blocks and streaming them to the **USB** audio device with minimal latency.
6061

6162
High-level data flow:
6263
```
63-
Web Browser Interaction → WebSocket → Python Backend → Sine Wave Generation → USB Audio Device Output
64+
Web Browser Interaction → WebSocket → Python Backend → WaveGenerator Brick → USB Audio Device Output
6465
```
6566

6667

6768
## Understanding the Code
6869

6970
### 🔧 Backend (`main.py`)
7071

71-
The Python code manages the web server, handles real-time user input, and performs all audio generation and playback.
72+
The Python code manages the web server, handles real-time user input, and controls the audio generation brick.
7273

7374
- `ui = WebUI()` – Initializes the web server that serves the HTML interface and handles WebSocket communication.
74-
- `speaker = Speaker(...)` – Initializes the connection to the USB audio device. This will raise an error if no compatible device is found.
75-
- `sine_gen = SineGenerator(...)` – Creates an instance of the audio synthesis engine.
76-
- `ui.on_message('theremin:move', on_move)` – Registers a handler that fires whenever the frontend sends new coordinates. This function updates the target frequency and amplitude.
77-
- `theremin_producer_loop()` – Core audio engine. Runs continuously, generating ~**30 ms** blocks of audio based on the current frequency and amplitude, and streams them to the audio device for playback. This non-blocking, continuous stream ensures smooth audio without cracks or pops.
75+
- `wave_gen = WaveGenerator(...)` – Creates the wave generator brick with configured envelope parameters (attack=0.01s, release=0.03s, glide=0.02s). The brick automatically manages the USB speaker connection and audio streaming in a background thread.
76+
- `ui.on_message('theremin:move', on_move)` – Registers a handler that fires whenever the frontend sends new coordinates. This function updates the wave generator's frequency and amplitude using `wave_gen.set_frequency()` and `wave_gen.set_amplitude()`.
77+
- The `wave_generator` brick handles all audio generation and streaming automatically, including smooth transitions between frequency and amplitude changes, continuous audio output with ~**30 ms** blocks, and non-blocking playback without cracks or pops.
7878

7979
### 💻 Frontend (`main.js`)
8080

@@ -84,7 +84,7 @@ The web interface provides the interactive play area and controls for the user.
8484
- **Event listeners** capture `mousedown`, `mousemove`, `mouseup` (and touch equivalents) to track user interaction in the play area.
8585
- `socket.emit('theremin:move', { x, y })` – Sends normalized (0.0–1.0) X and Y coordinates to the backend; emissions are **throttled to ~80 Hz (≈12 ms)** to avoid overload.
8686
- `socket.on('theremin:state', ...)` – Receives state updates from the backend (like the calculated frequency and amplitude) and updates the values displayed on the webpage.
87-
- `socket.emit('theremin:set_volume', { volume })` – Sends a **0.0–1.0** master volume value and updates a progress bar in the UI.
87+
- `socket.emit('theremin:set_volume', { volume })` – Sends a **0-100** hardware volume value to control the USB speaker's output level.
8888
- `socket.emit('theremin:power', { on })` – Toggles synth power (**On/Off**). After turning **On**, move/tap in the play area to resume sound.
8989

9090

examples/theremin/app.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ icon: 🎼
33
description: A simple theremin simulator that generates audio based on user input.
44
bricks:
55
- arduino:web_ui
6+
- arduino:wave_generator

examples/theremin/assets/main.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
const thereminSvg = document.getElementById('theremin-svg');
2222

23-
let currentVolume = 0.8; // Default volume
23+
let currentVolume = 80; // Default volume (0-100)
2424
let powerOn = false;
2525
let accessOn = false;
2626
let isGridOn = false;
@@ -97,9 +97,9 @@
9797
let newVolume = currentVolume;
9898

9999
if (plusBtn) {
100-
newVolume = Math.min(1.0, currentVolume + 0.1);
100+
newVolume = Math.min(100, currentVolume + 10);
101101
} else if (minusBtn) {
102-
newVolume = Math.max(0.0, currentVolume - 0.1);
102+
newVolume = Math.max(0, currentVolume - 10);
103103
}
104104

105105
if (newVolume !== currentVolume) {
@@ -274,7 +274,7 @@
274274
function updateVolumeIndicator(volume) {
275275
const indicator = document.getElementById('volume-indicator');
276276
if (indicator) {
277-
const angle = (volume - 0.5) * 180; // -90 to +90 degrees
277+
const angle = ((volume / 100.0) - 0.5) * 180; // -90 to +90 degrees
278278
indicator.style.transform = `rotate(${angle}deg)`;
279279
}
280280
}

examples/theremin/python/main.py

Lines changed: 54 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,150 +2,88 @@
22
#
33
# SPDX-License-Identifier: MPL-2.0
44

5-
import threading
6-
import time
7-
85
from arduino.app_bricks.web_ui import WebUI
9-
from arduino.app_peripherals.speaker import Speaker
10-
from arduino.app_utils import App, SineGenerator
6+
from arduino.app_bricks.wave_generator import WaveGenerator
7+
from arduino.app_utils import App, Logger
8+
import logging
119

10+
logger = Logger("theremin", logging.DEBUG)
1211

1312
# configuration
1413
SAMPLE_RATE = 16000
15-
# duration of each produced block (seconds).
16-
BLOCK_DUR = 0.03
17-
18-
# speaker setup
19-
speaker = Speaker(sample_rate=SAMPLE_RATE, format='FLOAT_LE')
20-
speaker.start()
21-
speaker.set_volume(80)
22-
23-
# runtime state (module-level)
24-
current_freq = 440.0
25-
current_amp = 0.0
26-
master_volume = 0.8
27-
running = True
28-
29-
# Sine generator instance encapsulates buffers/state
30-
sine_gen = SineGenerator(SAMPLE_RATE)
31-
# Configure envelope parameters: attack, release, and frequency glide (portamento)
32-
sine_gen.set_envelope_params(attack=0.01, release=0.03, glide=0.02)
33-
34-
35-
# --- Producer scheduling ---------------------------------------------------------
36-
# The example provides a producer loop that generates audio blocks at a steady
37-
# cadence (BLOCK_DUR). The loop is executed under the application's main
38-
# lifecycle by passing it to `App.run()`; we avoid starting background threads
39-
# directly from example code so the AppController can manage startup/shutdown.
40-
41-
# event to wake the producer when state changes (e.g. on_move updates freq/amp)
42-
prod_wake = threading.Event()
43-
44-
# Producer loop
45-
# The producer loop is executed inside App.run() by passing a user_loop callable.
46-
# This keeps the example simple and aligns with AppController's lifecycle management.
47-
def theremin_producer_loop():
48-
"""Single-iteration producer loop executed repeatedly by App.run().
49-
50-
This function performs one producer iteration: it generates a single
51-
block and plays it non-blocking. `App.run()` will call this repeatedly
52-
until the application shuts down (Ctrl+C).
53-
"""
54-
global running
55-
next_time = time.perf_counter()
56-
# lightweight single-iteration producer used by the App.run() user_loop.
57-
while running:
58-
# steady scheduling
59-
next_time += float(BLOCK_DUR)
60-
61-
# if no amplitude requested, avoid stopping the producer indefinitely.
62-
# Instead wait with a timeout and emit a silent block while idle. This
63-
# keeps scheduling steady and avoids large timing discontinuities when
64-
# the producer is woken again (which can produce audible cracks).
65-
if current_amp <= 0.0:
66-
prod_wake.clear()
67-
# wait up to one block duration; if woken earlier we proceed
68-
prod_wake.wait(timeout=BLOCK_DUR)
69-
# emit a silent block to keep audio device scheduling continuous
70-
if current_amp <= 0.0:
71-
data = sine_gen.generate_block(float(current_freq), 0.0, BLOCK_DUR, master_volume)
72-
speaker.play(data, block_on_queue=False)
73-
# maintain timing
74-
now = time.perf_counter()
75-
sleep_time = next_time - now
76-
if sleep_time > 0:
77-
time.sleep(sleep_time)
78-
else:
79-
next_time = now
80-
continue
81-
82-
# read targets
83-
freq = float(current_freq)
84-
amp = float(current_amp)
85-
86-
# generate one block and play non-blocking
87-
data = sine_gen.generate_block(freq, amp, BLOCK_DUR, master_volume)
88-
speaker.play(data, block_on_queue=False)
89-
90-
# wait until next scheduled time
91-
now = time.perf_counter()
92-
sleep_time = next_time - now
93-
if sleep_time > 0:
94-
time.sleep(sleep_time)
95-
else:
96-
next_time = now
14+
15+
# Wave generator brick - handles audio generation and streaming automatically
16+
wave_gen = WaveGenerator(
17+
sample_rate=SAMPLE_RATE,
18+
wave_type="sine",
19+
block_duration=0.03,
20+
attack=0.01,
21+
release=0.03,
22+
glide=0.02,
23+
)
24+
25+
# Set initial state
26+
wave_gen.set_frequency(440.0)
27+
wave_gen.set_amplitude(0.0)
9728

9829

9930
# --- Web UI and event handlers -----------------------------------------------------
31+
# The WaveGenerator brick handles audio generation and streaming automatically in
32+
# a background thread. We only need to update frequency and amplitude via its API.
10033
ui = WebUI()
10134

35+
10236
def on_connect(sid, data=None):
103-
ui.send_message('theremin:state', {'freq': current_freq, 'amp': current_amp})
104-
ui.send_message('theremin:volume', {'volume': master_volume})
37+
state = wave_gen.get_state()
38+
ui.send_message("theremin:state", {"freq": state["frequency"], "amp": state["amplitude"]})
39+
ui.send_message("theremin:volume", {"volume": state["volume"]})
40+
10541

10642
def _freq_from_x(x):
10743
return 20.0 * ((SAMPLE_RATE / 2.0 / 20.0) ** x)
10844

45+
10946
def on_move(sid, data=None):
110-
"""Update desired frequency/amplitude and wake producer.
47+
"""Update desired frequency/amplitude.
11148
112-
The frontend should only send on mousedown/move/mouseup (no aggressive
113-
repeat). This handler updates shared state and signals the producer. The
114-
actual audio scheduling is handled by the producer loop executed under
115-
`App.run()`.
49+
The WaveGenerator brick handles smooth transitions automatically using
50+
the configured envelope parameters (attack, release, glide).
11651
"""
117-
global current_freq, current_amp
11852
d = data or {}
119-
x = float(d.get('x', 0.0))
120-
y = float(d.get('y', 1.0))
121-
freq = d.get('freq')
53+
x = float(d.get("x", 0.0))
54+
y = float(d.get("y", 1.0))
55+
freq = d.get("freq")
12256
freq = float(freq) if freq is not None else _freq_from_x(x)
12357
amp = max(0.0, min(1.0, 1.0 - float(y)))
124-
current_freq = freq
125-
current_amp = amp
126-
# wake the producer so it reacts immediately
127-
prod_wake.set()
128-
ui.send_message('theremin:state', {'freq': freq, 'amp': amp}, room=sid)
58+
59+
logger.debug(f"on_move: x={x:.3f}, y={y:.3f} -> freq={freq:.1f}Hz, amp={amp:.3f}")
60+
61+
# Update wave generator state
62+
wave_gen.set_frequency(freq)
63+
wave_gen.set_amplitude(amp)
64+
65+
ui.send_message("theremin:state", {"freq": freq, "amp": amp}, room=sid)
66+
12967

13068
def on_power(sid, data=None):
131-
global current_amp
13269
d = data or {}
133-
on = bool(d.get('on', False))
70+
on = bool(d.get("on", False))
13471
if not on:
135-
current_amp = 0.0
136-
prod_wake.set()
72+
wave_gen.set_amplitude(0.0)
73+
13774

13875
def on_set_volume(sid, data=None):
139-
global master_volume
14076
d = data or {}
141-
v = float(d.get('volume', master_volume))
142-
master_volume = max(0.0, min(1.0, v))
143-
ui.send_message('theremin:volume', {'volume': master_volume})
77+
volume = int(d.get("volume", 100))
78+
volume = max(0, min(100, volume))
79+
wave_gen.set_volume(volume)
80+
ui.send_message("theremin:volume", {"volume": volume})
81+
14482

14583
ui.on_connect(on_connect)
146-
ui.on_message('theremin:move', on_move)
147-
ui.on_message('theremin:power', on_power)
148-
ui.on_message('theremin:set_volume', on_set_volume)
84+
ui.on_message("theremin:move", on_move)
85+
ui.on_message("theremin:power", on_power)
86+
ui.on_message("theremin:set_volume", on_set_volume)
14987

150-
# Run the app and use the theremin_producer_loop as the user-provided loop.
151-
App.run(user_loop=theremin_producer_loop)
88+
# Run the app - WaveGenerator handles audio generation automatically
89+
App.run()

0 commit comments

Comments
 (0)