FastHTML Piano, Part 2
```python
from fastcore.all import *
from fasthtml.common import *
from fasthtml.jupyter import *
from IPython.display import display, Javascript
```
In Part 1 we defined piano keys like this:
```python
def Key(note, octave): return Div(
Div(note, Sub(octave, style='font-size:10px;pointer-events:none;'), style='position:absolute;bottom:0;text-align:center;width:100%;pointer-events:none;'),
onmouseover="event.target.style.backgroundColor = '#eef';",
onmouseout="event.target.style.backgroundColor = '#fff';",
style='cursor:pointer;font:16px "Open Sans","Lucida Grande","Arial",sans-serif;text-align:center;border:1px solid black;border-radius:5px;width:20px;height:80px;margin-right:3px;box-shadow:2px 2px darkgray;display:inline-block;position:relative;user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;')
```
```python
show(Key('C','5'), Key('D','5'))
```
We'll be adding frequencies to the keys.
Even though our piano has just the white keys, we need all the notes to calculate the frequencies:
```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']
There are 12 notes per octave. I define `npo` for later use:
```python
npo = len(notes_in_octave)
npo
```
We use a4's frequency to calculate the other note frequencies:
```python
a4_freq = 440.0
```
```python
notes_in_octave.index('A')
```
```python
a4i = notes_in_octave.index('A') + (4 * npo)
a4i
```
Instead of a hardcoded table in JS with all the note frequencies for all octaves, we define this Python function:
```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)
freq('A', 4)
```
```python
freq('C', 3)
```
I like the idea of showing the frequencies on the piano keys.
```python
def Key(note, octave):
f = freq(note,octave)
return Div(
Div(Div(f, style="font-size:10px;transform:rotate(90deg);position:absolute;left:-6px;bottom:50px;"), note, 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="event.target.style.backgroundColor = '#fff';",
style='cursor:pointer;font:10px "Open Sans","Lucida Grande","Arial",sans-serif;text-align:center;border:1px solid black;border-radius:5px;width:22px;height:80px;margin-right:3px;box-shadow:2px 2px darkgray;display:inline-block;position:relative;user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;')
```
```python
show(Key('A', 4))
```
```python
def Octave(n):
return Div(*notes_in_octave.map(partial(Key,octave=n)),
style='display:inline-block;padding:0 6px 0 0;')
show(Octave(4))
```
```python
def Keyboard(): return Div(*L(range(8)).map(Octave), style="width:auto;padding:0;margin:0;")
show(Keyboard())
```
The MDN article had this:
```python
show(Div(Keyboard(), style="overflow-x:scroll;overflow-y:hidden;width:100%;height:110px;white-space:nowrap;margin:10px;"))
```
But I think wrapping the keys without a horizontal scrollbar is nicer.
For now, I convert the original to a FastTag here:
```python
def SettingsBar():
return Div(
Div(
Span("Volume: ",style="vertical-align:middle;"),
Input(type="range",min=0,max=1.0,step=0.01,value=0.5,list="volumes",name="volume",style="vertical-align:middle;"),
Datalist(
Option(value=0.0,label="Mute"),
Option(value=1.0,label="100%"),
id="volumes",),
style="width:50%;position:absolute;left:0;display:table-cell;vertical-align:middle;"),
Div(
Span("Waveform: ", style="vertical-align:middle;"),
Select(
Option("Sine", value="sine"),
Option("Square", value="square"),
Option("Sawtooth", value="sawtooth"),
Option("Triangle", value="triangle"),
Option("Custom", value="custom"),
name="waveform",
style="vertical-align:middle;"),
style="width:50%;position:absolute;right:0;display:table-cell;vertical-align:middle;"),
style='padding-top:8px;font:14px "Open Sans","Lucida Grande","Arial",sans-serif;position:relative;vertical-align:middle;width:100%;height:80px;')
```
```python
show(SettingsBar())
```
## Creating Oscillators in JS
```python
%%javascript
window.audioContext = new AudioContext();
window.gainNode = audioContext.createGain();
window.gainNode.gain.value = 0.5;
function playTone(freq) {
const osc = window.audioContext.createOscillator();
osc.type = 'sine';
osc.frequency.value = freq;
osc.connect(window.gainNode);
window.gainNode.connect(window.audioContext.destination);
osc.start();
}
playTone(400)
```
## Creating Oscillators in Python
The MDN example creates 1 oscillator per note. I'm just playing around here and may change this later: instead of doing it in JS, I define a Python function to generate the Web Audio API JS code to do this:
```python
def mk_osc(freq):
return f"""if (!window.audioContext) {{
window.audioContext = new AudioContext();
window.gainNode = audioContext.createGain();
window.gainNode.gain.value = 0.5;
}}
const osc=window.audioContext.createOscillator();
osc.type='sine';
osc.frequency.value={freq};
osc.connect(window.gainNode);
window.gainNode.connect(window.audioContext.destination);
console.log('Starting freq {freq}');
osc.start()"""
```
```python
mk_osc(440.0)
```
"if (!window.audioContext) {\n window.audioContext = new AudioContext();\n window.gainNode = audioContext.createGain();\n window.gainNode.gain.value = 0.5;\n }\n const osc=window.audioContext.createOscillator();\n osc.type='sine';\n osc.frequency.value=440.0;\n osc.connect(window.gainNode);\n window.gainNode.connect(window.audioContext.destination);\n console.log('Starting freq 440.0');\n osc.start()"
Running this cell plays the 440Hz tone:
```python
display(Javascript(mk_osc(440.0)))
```
```python
def mk_stoposc(freq): return f"console.log('Stopping freq {freq}');osc.stop();";
```
```python
mk_stoposc(440.0)
```
```python
display(Javascript(mk_stoposc(440.0)))
```
This is tricker than I thought. mk_stoposc needs to get osc from window, I think. We probably want to create a list of oscillators for all note frequencies and waveforms, and attach it to window.
```python
def Key(note, octave):
f = freq(note,octave)
return Div(
Div(Div(f, style="font-size:10px;transform:rotate(90deg);position:absolute;left:-6px;bottom:50px;"), note, 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="event.target.style.backgroundColor = '#fff';",
onmousedown=mk_osc(f),
onmouseup=mk_stoposc(f),
style='cursor:pointer;font:10px "Open Sans","Lucida Grande","Arial",sans-serif;text-align:center;border:1px solid black;border-radius:5px;width:22px;height:80px;margin-right:3px;box-shadow:2px 2px darkgray;display:inline-block;position:relative;user-select:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;')
```
```python
show(Key('A', 4))
```
The tone plays, but `mk_stoposc` doesn't actually work here. To be continued...