[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
draft: true
filters: ["./to-parens.lua"]
custom_js: ["playsound.js"]
---
Sound is a perturbation in air pressure that our ear recognizes and interprets.
@@ -86,6 +87,17 @@ class Superimpose:
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:
def __init__(self, *args):
self.args = args
@@ -156,6 +168,11 @@ middleC = Frequency(261.63)
middleC
```
```{python}
#| echo: false
PlayNotes(middleC.hz)
```
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
the same word over and over again won't make for an interesting story.
@@ -165,13 +182,23 @@ other frequency.
```{python}
g4 = Frequency(392.445)
g4
```
```{python}
#| echo: false
PlayNotes(g4.hz)
```
```{python}
fSharp4 = Frequency(370.000694) # we write this F#
fSharp4
```
```{python}
#| echo: false
PlayNotes(fSharp4.hz)
```
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
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}
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
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,
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
@@ -293,18 +329,28 @@ VerticalStack(
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
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
singing notes that were too high at half their original frequency?
We can do that here. The result is a note we've already seen:
far apart in terms of pitch. That `triceMiddleC` note is really high!
Wait a minute --- weren't we just talking about singing notes that were too
high at half their original frequency? We can do that here. The result is a
note we've already seen:
```{python}
print(thriceMiddleC.hz/2)
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
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
@@ -325,12 +371,20 @@ while len(seen) < 6:
seen.add(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.
seen.add(Fraction(2/3) * 2)
fractions = sorted(list(seen))
fractions
```
```{python}
frequencies = [middleC.hz * float(frac) for frac in fractions]
frequencies
```