FastHTML Piano, Part 3

```python from fastcore.all import * from fasthtml.common import * ```
```python settings_js = """window.audioContext = new AudioContext(); window.gainNode = audioContext.createGain(); window.gainNode.gain.value = 0.5; window.activeOscillators = new Map(); """ ```
## Piano Settings Bar
A volume slider to change the gainNode's value:
```python def VolumeInput(v=0.5): return Div( Span("Volume: ",style="vertical-align:middle;"), Input(type="range",min=0,max=1.0,step=0.01,value=v,name="volume", onchange=f"window.gainNode.gain.value=event.target.value;console.log('Volume changed to ', event.target.value);")) show(VolumeInput()) ```
Volume:
```python show(VolumeInput(0.8)) ```
Volume:
A dropdown to choose a waveform type:
```python def WaveformInput(): return Div( Script("function updateWaveform(type) {window.waveformType = type;console.log('Waveform set to', type)}"), Span("Waveform: "), Select( Option("Sine", value="sine"), Option("Square", value="square"), Option("Sawtooth", value="sawtooth"), Option("Triangle", value="triangle"), Option("Custom", value="custom"), onchange="updateWaveform(event.target.value)", name="waveform")) show(WaveformInput()) ```
Waveform:
Note: I defined a JS function in `Script` just to see if I can do that and call it in `onchange`. But it's simple enough that inline would have been fine.
The `SettingsBar` combines both inputs:
```python def SettingsBar(): return Div( Script(settings_js), VolumeInput(), WaveformInput()) show(SettingsBar()) ```
Volume:
Waveform:
## Notes and Frequencies
Let's modify the Key function to add click handling, and create the corresponding JavaScript to start and stop the oscillator when clicking. Here's how:
```python notes_in_octave = L(['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']) notes_in_octave ```
(#12) ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']
```python npo = len(notes_in_octave) a4_freq = 440.0 a4i = notes_in_octave.index('A') + (4 * npo) ```
```python def freq(note, octave): ni = notes_in_octave.index(note) + (octave * npo) semitones_from_a4 = ni - a4i freq = a4_freq * (2 ** (semitones_from_a4 / npo)) return round(freq, ndigits=1) ```
```python key_js = """ function playTone(freq) { const osc = window.audioContext.createOscillator(); osc.type = window.waveformType || 'sine'; osc.frequency.value = freq; osc.connect(window.gainNode); window.gainNode.connect(window.audioContext.destination); osc.start(); window.activeOscillators.set(freq, osc); } function stopTone(freq) { const osc = window.activeOscillators.get(freq); if (osc) { osc.stop(); osc.disconnect(); window.activeOscillators.delete(freq); } }""" ```
```python def Key(note, octave): f = freq(note,octave) is_black = '#' in note style = ('cursor:pointer;font:10px "Open Sans","Lucida Grande","Arial",sans-serif;text-align:center;' f'border:1px solid black;border-radius:5px;width:{20 if not is_black else 16}px;' f'height:{80 if not is_black else 50}px;' f'background-color:{" black" if is_black else "white"};color:{" white" if is_black else "black"};' f'box-shadow:2px 2px darkgray;display:inline-block;position:relative;' f'{"top:-30px;" if is_black else ""}' 'user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;') return Div( Script(key_js), Div(Div(f, style=f'font-size:10px;transform:rotate(90deg);position:absolute;left:-6px;{"bottom:24px;" if is_black else "bottom:50px;"}'), note.replace('#','♯'), Sub(octave, style='font-size:10px;pointer-events:none;'), style='position:absolute;bottom:1px;text-align:center;width:100%;pointer-events:none;'), onmouseover="event.target.style.backgroundColor = '#eef';", onmouseout=f"event.target.style.backgroundColor = '#{'000' if is_black else 'fff'}';", onmousedown=f"playTone({f})", onmouseup=f"stopTone({f})", onmouseleave=f"stopTone({f})", style=style) ```
```python def Octave(n): return Div(*notes_in_octave.map(partial(Key,octave=n)), style='display:inline-block;padding:0 6px 0 0;') ```
```python show(Octave(4)) ```
261.6
C4
277.2
C♯4
293.7
D4
311.1
D♯4
329.6
E4
349.2
F4
370.0
F♯4
392.0
G4
415.3
G♯4
440.0
A4
466.2
A♯4
493.9
B4
```python def Keyboard(): return Div(*L(range(8)).map(Octave), style="width:auto;padding:0;margin:0;") show(Keyboard()) ```
16.4
C0
17.3
C♯0
18.4
D0
19.4
D♯0
20.6
E0
21.8
F0
23.1
F♯0
24.5
G0
26.0
G♯0
27.5
A0
29.1
A♯0
30.9
B0
32.7
C1
34.6
C♯1
36.7
D1
38.9
D♯1
41.2
E1
43.7
F1
46.2
F♯1
49.0
G1
51.9
G♯1
55.0
A1
58.3
A♯1
61.7
B1
65.4
C2
69.3
C♯2
73.4
D2
77.8
D♯2
82.4
E2
87.3
F2
92.5
F♯2
98.0
G2
103.8
G♯2
110.0
A2
116.5
A♯2
123.5
B2
130.8
C3
138.6
C♯3
146.8
D3
155.6
D♯3
164.8
E3
174.6
F3
185.0
F♯3
196.0
G3
207.7
G♯3
220.0
A3
233.1
A♯3
246.9
B3
261.6
C4
277.2
C♯4
293.7
D4
311.1
D♯4
329.6
E4
349.2
F4
370.0
F♯4
392.0
G4
415.3
G♯4
440.0
A4
466.2
A♯4
493.9
B4
523.3
C5
554.4
C♯5
587.3
D5
622.3
D♯5
659.3
E5
698.5
F5
740.0
F♯5
784.0
G5
830.6
G♯5
880.0
A5
932.3
A♯5
987.8
B5
1046.5
C6
1108.7
C♯6
1174.7
D6
1244.5
D♯6
1318.5
E6
1396.9
F6
1480.0
F♯6
1568.0
G6
1661.2
G♯6
1760.0
A6
1864.7
A♯6
1975.5
B6
2093.0
C7
2217.5
C♯7
2349.3
D7
2489.0
D♯7
2637.0
E7
2793.8
F7
2960.0
F♯7
3136.0
G7
3322.4
G♯7
3520.0
A7
3729.3
A♯7
3951.1
B7
It's coming together! Remaining TODOs that I may or may not get to, depending on motivation, time, and interest from others: * Simplify to a list of one oscillator per note * Connect waveform select * Allow user to hold down the mouse and slide across keys * Computer keyboard control to allow for chords * Get it working on mobile and check non-Chrome browsers * Limit octaves to the audible ones Bonus nice-to-haves: * Make the black keys overlap the white ones correctly so it looks more pianolike * Songs We'll see though, no promises.