|
2 | 2 | # |
3 | 3 | # SPDX-License-Identifier: MPL-2.0 |
4 | 4 |
|
5 | | -import threading |
6 | | -import time |
7 | | - |
8 | 5 | 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 |
11 | 9 |
|
| 10 | +logger = Logger("theremin", logging.DEBUG) |
12 | 11 |
|
13 | 12 | # configuration |
14 | 13 | 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) |
97 | 28 |
|
98 | 29 |
|
99 | 30 | # --- 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. |
100 | 33 | ui = WebUI() |
101 | 34 |
|
| 35 | + |
102 | 36 | 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 | + |
105 | 41 |
|
106 | 42 | def _freq_from_x(x): |
107 | 43 | return 20.0 * ((SAMPLE_RATE / 2.0 / 20.0) ** x) |
108 | 44 |
|
| 45 | + |
109 | 46 | def on_move(sid, data=None): |
110 | | - """Update desired frequency/amplitude and wake producer. |
| 47 | + """Update desired frequency/amplitude. |
111 | 48 |
|
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). |
116 | 51 | """ |
117 | | - global current_freq, current_amp |
118 | 52 | 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") |
122 | 56 | freq = float(freq) if freq is not None else _freq_from_x(x) |
123 | 57 | 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 | + |
129 | 67 |
|
130 | 68 | def on_power(sid, data=None): |
131 | | - global current_amp |
132 | 69 | d = data or {} |
133 | | - on = bool(d.get('on', False)) |
| 70 | + on = bool(d.get("on", False)) |
134 | 71 | if not on: |
135 | | - current_amp = 0.0 |
136 | | - prod_wake.set() |
| 72 | + wave_gen.set_amplitude(0.0) |
| 73 | + |
137 | 74 |
|
138 | 75 | def on_set_volume(sid, data=None): |
139 | | - global master_volume |
140 | 76 | 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 | + |
144 | 82 |
|
145 | 83 | 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) |
149 | 87 |
|
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