[WIP] Add ability to play sound

Signed-off-by: Danila Fedorin <danila.fedorin@gmail.com>
This commit is contained in:
2025-12-06 17:42:59 -08:00
parent 1e5ed34f0f
commit 9660c0d665
2 changed files with 74 additions and 5 deletions

View File

@@ -3,6 +3,7 @@ title: "Some Music Theory From (Computational) First Principles"
date: 2025-09-20T18:36:28-07:00 date: 2025-09-20T18:36:28-07:00
draft: true draft: true
filters: ["./to-parens.lua"] filters: ["./to-parens.lua"]
custom_js: ["playsound.js"]
--- ---
Sound is a perturbation in air pressure that our ear recognizes and interprets. Sound is a perturbation in air pressure that our ear recognizes and interprets.
@@ -86,6 +87,17 @@ class Superimpose:
width, height width, height
) )
def hugo_shortcode(body):
return "{{" + "< " + body + " >" + "}}"
class PlayNotes:
def __init__(self, *hzs):
self.hzs = hzs
def _repr_html_(self):
toplay = ",".join([str(hz) for hz in self.hzs])
return hugo_shortcode(f"playsound \"{toplay}\"")
class VerticalStack: class VerticalStack:
def __init__(self, *args): def __init__(self, *args):
self.args = args self.args = args
@@ -156,6 +168,11 @@ middleC = Frequency(261.63)
middleC middleC
``` ```
```{python}
#| echo: false
PlayNotes(middleC.hz)
```
Great! Now, if you're a composer, you can play this note and make music out Great! Now, if you're a composer, you can play this note and make music out
of it. Except, music made with just one note is a bit boring, just like saying of it. Except, music made with just one note is a bit boring, just like saying
the same word over and over again won't make for an interesting story. the same word over and over again won't make for an interesting story.
@@ -165,13 +182,23 @@ other frequency.
```{python} ```{python}
g4 = Frequency(392.445) g4 = Frequency(392.445)
g4 g4
``` ```
```{python}
#| echo: false
PlayNotes(g4.hz)
```
```{python} ```{python}
fSharp4 = Frequency(370.000694) # we write this F# fSharp4 = Frequency(370.000694) # we write this F#
fSharp4 fSharp4
``` ```
```{python}
#| echo: false
PlayNotes(fSharp4.hz)
```
This is pretty cool. You can start making melodies with these notes, and sing This is pretty cool. You can start making melodies with these notes, and sing
some jingles. However, if your friend sings along with you, and happens to some jingles. However, if your friend sings along with you, and happens to
sing F# while you're singing the middle C, it's going to sound pretty awful. sing F# while you're singing the middle C, it's going to sound pretty awful.
@@ -194,6 +221,10 @@ show you the bigger picture.
```{python} ```{python}
Superimpose(Frequency(middleC.hz*4), Frequency(fSharp4.hz*4)) Superimpose(Frequency(middleC.hz*4), Frequency(fSharp4.hz*4))
``` ```
```{python}
#| echo: false
PlayNotes(middleC.hz, fSharp4.hz)
```
Looking at this picture, we can see that it's far more disordered than the Looking at this picture, we can see that it's far more disordered than the
pure sine waves we've been looking at so far. There's not much of a pattern pure sine waves we've been looking at so far. There's not much of a pattern
@@ -244,6 +275,11 @@ VerticalStack(
) )
``` ```
```{python}
#| echo: false
PlayNotes(middleC.hz, twiceMiddleC.hz)
```
You can easily inspect the new graph to verify that it has a repeating pattern, You can easily inspect the new graph to verify that it has a repeating pattern,
and that this pattern repeats exactly as frequently as the lower-frequency and that this pattern repeats exactly as frequently as the lower-frequency
note at the top. Indeed, these two notes sound quite good together. It turns note at the top. Indeed, these two notes sound quite good together. It turns
@@ -293,18 +329,28 @@ VerticalStack(
Superimpose(middleC, thriceMiddleC) Superimpose(middleC, thriceMiddleC)
) )
``` ```
```{python}
#| echo: false
PlayNotes(middleC.hz, thriceMiddleC.hz)
```
That's not bad! These two sound good together as well, but they are not That's not bad! These two sound good together as well, but they are not
in the same pitch class. There's only one problem: these notes are a bit in the same pitch class. There's only one problem: these notes are a bit
far apart in terms of pitch. Wait a minute --- weren't we just talking about far apart in terms of pitch. That `triceMiddleC` note is really high!
singing notes that were too high at half their original frequency? Wait a minute --- weren't we just talking about singing notes that were too
We can do that here. The result is a note we've already seen: high at half their original frequency? We can do that here. The result is a
note we've already seen:
```{python} ```{python}
print(thriceMiddleC.hz/2) print(thriceMiddleC.hz/2)
print(g4.hz) print(g4.hz)
``` ```
```{python}
#| echo: false
PlayNotes(middleC.hz, g4.hz)
```
In the end, we got G4 by multiplying our original frequency by $3/2$. What if In the end, we got G4 by multiplying our original frequency by $3/2$. What if
we keep applying this process to find more notes? Let's not even worry we keep applying this process to find more notes? Let's not even worry
about the specific frequencies (like `261.63`) for a moment. We'll start about the specific frequencies (like `261.63`) for a moment. We'll start
@@ -325,12 +371,20 @@ while len(seen) < 6:
seen.add(new_note) seen.add(new_note)
note = new_note note = new_note
```
For an admittedly handwavy reason, let's also throw in one note that we
get from going _backwards_: dividing by $2/3$ instead of multiplying.
This division puts us below our original frequency, so let's double it.
```{python}
# Throw in one more by going *backwards*. More on that in a bit. # Throw in one more by going *backwards*. More on that in a bit.
seen.add(Fraction(2/3) * 2) seen.add(Fraction(2/3) * 2)
fractions = sorted(list(seen)) fractions = sorted(list(seen))
fractions
```
```{python}
frequencies = [middleC.hz * float(frac) for frac in fractions] frequencies = [middleC.hz * float(frac) for frac in fractions]
frequencies frequencies
``` ```

View File

@@ -0,0 +1,15 @@
window.addEventListener("load", (event) => {
for (const elt of document.getElementsByClassName("mt-sound-play-button")) {
elt.addEventListener("click", (event) => {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
for (const freq of event.target.getAttribute("data-sound-info").split(",")) {
const oscillator = audioCtx.createOscillator();
oscillator.type = "sine"; // waveform: sine, square, sawtooth, triangle
oscillator.frequency.value = parseInt(freq); // Hz
oscillator.connect(audioCtx.destination);
oscillator.start();
oscillator.stop(audioCtx.currentTime + 2); // stop after 1 second
}
});
}
});