Skip to content

Commit 8cc7882

Browse files
committed
add WaveGenerator in Theremin
1 parent 8fe9cbd commit 8cc7882

File tree

3 files changed

+62
-125
lines changed

3 files changed

+62
-125
lines changed

examples/theremin/README.md

Lines changed: 9 additions & 9 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

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/python/main.py

Lines changed: 52 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,150 +2,86 @@
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
118

129

1310
# configuration
1411
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
12+
13+
# Wave generator brick - handles audio generation and streaming automatically
14+
wave_gen = WaveGenerator(
15+
sample_rate=SAMPLE_RATE,
16+
wave_type="sine",
17+
block_duration=0.03,
18+
attack=0.01,
19+
release=0.03,
20+
glide=0.02,
21+
)
22+
23+
# Set initial state
24+
wave_gen.set_frequency(440.0)
25+
wave_gen.set_amplitude(0.0)
26+
wave_gen.set_volume(0.8)
9727

9828

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

34+
10235
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})
36+
state = wave_gen.get_state()
37+
ui.send_message("theremin:state", {"freq": state["frequency"], "amp": state["amplitude"]})
38+
ui.send_message("theremin:volume", {"volume": state["master_volume"]})
39+
10540

10641
def _freq_from_x(x):
10742
return 20.0 * ((SAMPLE_RATE / 2.0 / 20.0) ** x)
10843

44+
10945
def on_move(sid, data=None):
110-
"""Update desired frequency/amplitude and wake producer.
46+
"""Update desired frequency/amplitude.
11147
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()`.
48+
The WaveGenerator brick handles smooth transitions automatically using
49+
the configured envelope parameters (attack, release, glide).
11650
"""
117-
global current_freq, current_amp
11851
d = data or {}
119-
x = float(d.get('x', 0.0))
120-
y = float(d.get('y', 1.0))
121-
freq = d.get('freq')
52+
x = float(d.get("x", 0.0))
53+
y = float(d.get("y", 1.0))
54+
freq = d.get("freq")
12255
freq = float(freq) if freq is not None else _freq_from_x(x)
12356
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)
57+
58+
# Update wave generator state
59+
wave_gen.set_frequency(freq)
60+
wave_gen.set_amplitude(amp)
61+
62+
ui.send_message("theremin:state", {"freq": freq, "amp": amp}, room=sid)
63+
12964

13065
def on_power(sid, data=None):
131-
global current_amp
13266
d = data or {}
133-
on = bool(d.get('on', False))
67+
on = bool(d.get("on", False))
13468
if not on:
135-
current_amp = 0.0
136-
prod_wake.set()
69+
wave_gen.set_amplitude(0.0)
70+
13771

13872
def on_set_volume(sid, data=None):
139-
global master_volume
14073
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})
74+
state = wave_gen.get_state()
75+
v = float(d.get("volume", state["master_volume"]))
76+
v = max(0.0, min(1.0, v))
77+
wave_gen.set_volume(v)
78+
ui.send_message("theremin:volume", {"volume": v})
79+
14480

14581
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)
82+
ui.on_message("theremin:move", on_move)
83+
ui.on_message("theremin:power", on_power)
84+
ui.on_message("theremin:set_volume", on_set_volume)
14985

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

0 commit comments

Comments
 (0)