diff --git a/content/blog/music_theory/index.qmd b/content/blog/music_theory/index.qmd index f101f99..47be57b 100644 --- a/content/blog/music_theory/index.qmd +++ b/content/blog/music_theory/index.qmd @@ -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 ``` diff --git a/content/blog/music_theory/playsound.js b/content/blog/music_theory/playsound.js new file mode 100644 index 0000000..6230d47 --- /dev/null +++ b/content/blog/music_theory/playsound.js @@ -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 + } + }); + } +});