Skip to content

Instantly share code, notes, and snippets.

@ZoomTen
Last active August 4, 2024 05:02
Show Gist options
  • Save ZoomTen/854ec664997ab7aef9a9f6edca3ca10c to your computer and use it in GitHub Desktop.
Save ZoomTen/854ec664997ab7aef9a9f6edca3ca10c to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"id": "656c1fb9-b815-4559-b793-bcf93bed6d14",
"metadata": {},
"source": [
"# Figuring out pan law for Game Boy\n",
"\n",
"[Pan Law](https://en.wikipedia.org/wiki/Panning_law) impl usually raises the volume when a sound is panned full left or full right. Usually:\n",
"* the sound is in full volume when it's right in front of my face\n",
"* when the sound is only on either side of my head I *perceive* about a 3dB drop in volume, because only one of my ears are receiving it\n",
"* I raise the volume up by 3dB to compensate for it.\n",
"\n",
"Here the assumption for GB is:\n",
"\n",
"* I'm composing songs in stereo, so I pan the channels as I please and adjust the mix to sound good *given* the panning sequence.\n",
"* I'm composing it for a sound engine where stereo is the default.\n",
"* the sound engine has a \"mono\" mode, where it ignores panning commands and *always* places sounds \"at the center\".\n",
"* the noise channel has predefined drum sounds, so I will ignore accounting for it. Instead I'll only focus on the square channels.\n",
"\n",
"the game boy has some quirks that make the differences in perception over volume even worse:\n",
"\n",
"* the Game Boy only cares which speaker(s) to output sound from, consequently for stereo it can only do hard pan left/right.\n",
"* \"center\" panning is basically just outputting the sound from both speakers, absolutely no compensation.\n",
"* The end result is that my songs when played in mono mode has horrible mixing. Specifically, the channels play *too loud*.\n",
"\n",
"Because I assume the stereo sound is the intended volume, I need to compensate in mono mode by *decreasing* the volume when the sound engine is in mono mode."
]
},
{
"cell_type": "markdown",
"id": "a61991e3-8cb3-43f8-a0be-0072a83fa86e",
"metadata": {},
"source": [
"As mentioned, the console doesn't do any compensation, so I have to do it myself in the sound engine. The Game Boy's square envelope has a starting volume, I can mess about with that. But to calculate the volume to adjust it to, I need to:\n",
"1. get the overall volume of the note\n",
"2. apply the (inverse) pan law to it, creating a target volume\n",
"3. find the volume that matches up to it and apply that instead of the intended volume in mono mode.\n",
"\n",
"To do step 1, I need to know how the Game Boy envelope thing works. When there is an envelope, its volume changes [every 64 Hz](https://gbdev.io/pandocs/Audio_details.html#div-apu). An envelope of 1 means \"change volume every 1 * 64 Hz\", 2 is \"2 * 64 Hz\" and so on, for both decay and attack.\n",
"\n",
"The volume changes are linear, but aren't smooth—they correspond to the GB's 16 levels for square and noise.\n",
"\n",
"<table>\n",
"<tr><th>Volume</th><th>Tick (1)</th><th>Tick (2)</th><th>Tick (3)</th></tr>\n",
"<tr><td>0F (15/15)</td><td>0</td><td>0</td><td>0</td><td></td></tr>\n",
"<tr><td>0E (14/15)</td><td>64</td><td>128</td><td>192</td><td></td></tr>\n",
"<tr><td>0D (13/15)</td><td>128</td><td>256</td><td>384</td><td></td></tr>\n",
"<tr><td>0C (12/15)</td><td>192</td><td>384</td><td>576</td><td></td></tr>\n",
"<tr><td>0B (11/15)</td><td>256</td><td>512</td><td>768</td><td></td></tr>\n",
"<tr><td>0A (10/15)</td><td>320</td><td>640</td><td>960</td><td></td></tr>\n",
"<tr><td>09 (9/15)</td><td>384</td><td>768</td><td>1152</td><td></td></tr>\n",
"<tr><td>...</td><td>...</td><td>...</td><td>...</td><td></td></tr>\n",
"</table>\n",
"\n",
"Unfortunately—as a consequence of this specific way of doing envelopes—when the envelope's direction is down, the initial volume determines the \"effective\" length of a note. This is different from the built-in [length timer](https://gbdev.io/pandocs/Audio.html#length-timer). This means that lowering the initial volume makes the note shorter, and vice versa. Here I assume that volume control is more important than preserving effective length."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "3bd7a87f-d3df-4a6f-820d-ce4ef19e1ad2",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Period 0.25 1000 Hz square wave for 10.00 seconds at 44100 Hz\n"
]
}
],
"source": [
"# Assume I'm working with 44.1khz\n",
"sample_rate = 44_100\n",
"frequency = 1000\n",
"period = 1/4\n",
"seconds = 10\n",
"\n",
"print(\n",
" \"Period %.2f %d Hz square wave for %.2f seconds at %d Hz\" %\n",
" (period, frequency, seconds, sample_rate)\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "96c73326-dc2e-4388-bddc-c26c044c54a9",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"One cycle = 11 samples (44 samples are HIGH, the rest are LOW)\n",
"\n"
]
}
],
"source": [
"# How many samples is one cycle?\n",
"one_cycle = sample_rate // frequency\n",
"\n",
"# For how many samples should the sample be HIGH?\n",
"no_of_up_samples = int(one_cycle * period)\n",
"\n",
"print(\n",
" \"One cycle = %d samples (%d samples are HIGH, the rest are LOW)\\n\" %\n",
" (no_of_up_samples, one_cycle)\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "51a67347-d2d1-410e-8fbc-0d79a6de471a",
"metadata": {},
"outputs": [],
"source": [
"# Simulate the sound register contents\n",
"starting_volume = 0xf/15\n",
"envelope_value = 7\n",
"going_up = False"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "2a720b4a-e5e5-42f6-a985-9e07c75c46fb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"First sample to hit zero or max: 77168\n"
]
}
],
"source": [
"# Let's model a Game Boy...\n",
"\n",
"# First let's safeguard against input errors\n",
"if envelope_value > 7:\n",
" envelope_value = 7\n",
"elif envelope_value < 0:\n",
" envelope_value = 0\n",
"if starting_volume > 1:\n",
" starting_volume = 1\n",
"elif starting_volume < 0:\n",
" starting_volume = 0\n",
"\n",
"# As mentioned before, the volume changes every (64 * envelope_value) Hz\n",
"# so let's determine how many samples is that\n",
"if envelope_value == 0:\n",
" change_every = 0\n",
"else:\n",
" change_every = (sample_rate // 64) * envelope_value\n",
"\n",
"# Prepare the buckets and initialize the output volume\n",
"samples = []\n",
"volume = starting_volume\n",
"\n",
"# To keep track of the \"effective\" sound length (not the configurable sound length! let's assume that's \"infinity\")\n",
"# If the envelope is going up, this tracks the first sample to hit the max volume instead.\n",
"has_hit_zero_yet = False\n",
"first_sample_to_hit_zero = 0\n",
"\n",
"# Let's simulate what an emulator might do, ignoring how the Game Boy itself\n",
"# generates a square wave; for simplicity I'm doing a *pure* sine wave\n",
"for i in range(int(sample_rate * seconds)):\n",
" # This is what changes the actual volume output\n",
" if change_every != 0:\n",
" if going_up:\n",
" if (i % change_every == 0) and (i != 0) and (volume < 1):\n",
" volume += 1/15\n",
" else:\n",
" if (i % change_every == 0) and (i != 0) and (volume > 0):\n",
" volume -= 1/15\n",
"\n",
" # Clamp the resulting volume\n",
" if volume < 0:\n",
" volume = 0\n",
" elif volume > 1:\n",
" volume = 1\n",
"\n",
" # Create the actual square wave\n",
" p = i % one_cycle\n",
" if p < no_of_up_samples:\n",
" # Should output HIGH\n",
" samples += [.12 * volume]\n",
" else:\n",
" # Should output LOW\n",
" samples += [-.12* volume]\n",
"\n",
" # Now keep track of the number of the first sample where the envelope\n",
" # reaches its Final Destination™\n",
" if going_up:\n",
" if (volume > 14/15) and not has_hit_zero_yet:\n",
" has_hit_zero_yet = True\n",
" first_sample_to_hit_zero = i\n",
" else:\n",
" if (volume == 0) and not has_hit_zero_yet:\n",
" has_hit_zero_yet = True\n",
" first_sample_to_hit_zero = i\n",
"\n",
"print(\n",
" \"First sample to hit zero or max: %d\" % first_sample_to_hit_zero\n",
")\n",
"\n",
"# Add one more envelope sweep cycle to account for the 0\n",
"first_sample_to_hit_zero += change_every"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "1157a908-e9da-4052-932b-023894685a85",
"metadata": {
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"(0.0, 81991.0)"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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nDly5MgRmTlzZp2ddkdEZOzYseLq6ipbtmwxmYKnqKhIiRkzZowEBATI5s2bZffu3RIVFSVRUVHK8tLpd/7yl7/Ivn37ZNOmTeLp6Vnm9DtTpkyRI0eOyPz588ucfqe+XLNTp06VH3/8UbKzs+XAgQMydepU0Wg08v3334sI+6yq7px9QoT9Vp7JkyfLli1bJDs7W3766SeJiYkRDw8PycvLExH2W3l27doltra28vbbb8vx48fliy++EEdHR1m6dKkSw8+GspWUlEhAQIC88sorZst4vZUtISFBmjZtqkzJtmbNGvHw8JCXX35ZialP11uDKopFRD788EMJCAgQnU4nnTt3lp9//lntlCwmPT1dAJi9EhISROT2VCivv/66eHt7i16vl+joaDl27JjJNv744w8ZPHiwODs7i4uLiwwfPlwKCwtNYvbv3y/dunUTvV4vTZs2ldmzZ5vlsmLFCmnVqpXodDpp06aNbNiwwWLHfb/K6jMAkpycrMRcv35d/va3v0njxo3F0dFR+vfvL7///rvJdk6fPi19+vQRBwcH8fDwkMmTJ8vNmzdNYtLT06Vjx46i0+kkODjYZB+l6ss1O2LECAkMDBSdTieenp4SHR2tFMQi7LOqursoZr+VLT4+Xnx9fUWn00nTpk0lPj7eZK5d9lv5vvnmG2nbtq3o9Xpp3bq1LFy40GQ5PxvK9t133wkAs74Q4fVWHoPBIBMmTJCAgACxt7eX4OBgefXVV02mTqtP15tG5I7HjhARERERWaEGM6aYiIiIiKi6WBQTERERkdVjUUxEREREVo9FMRERERFZPRbFRERERGT1WBQTERERkdVjUUxEREREVo9FMRERERFZPRbFRERERGT1WBQTERERkdVjUUxEREREVo9FMRERERFZvf8Dmt3bSkmzipoAAAAASUVORK5CYII=",
"text/plain": [
"<Figure size 640x480 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Now, plot the resulting wave\n",
"\n",
"from matplotlib import pyplot as plt\n",
"%matplotlib inline\n",
"fig = plt.figure()\n",
"ax = fig.add_axes([0,0,1,.3])\n",
"ax.plot(samples)\n",
"ax.set_title(\"GB square wave simulation\")\n",
"plt.xlim(0, first_sample_to_hit_zero)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "04928479-89ea-470c-8f7d-6e25f9157c19",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"RMS value: -23.308472622321517 dB\n"
]
}
],
"source": [
"# If I'm not doing anything fancy, I could just use a simple\n",
"# RMS calculation over all the samples, and determine its dB value.\n",
"\n",
"import numpy as np\n",
"sample_arr = np.array(samples[:first_sample_to_hit_zero])\n",
"rms_value = lambda arr: np.sqrt(np.mean(arr**2))\n",
"db_value = lambda x: 20 * np.log10(x)\n",
"\n",
"print(\n",
" f\"RMS value: {db_value(rms_value(sample_arr))} dB\"\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "50d7d35b-817c-4844-b3d3-374faf9faa68",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Integrated loudness: -22.71423445874779 dB LUFS\n"
]
}
],
"source": [
"# Integrated Loudness readings are considered better, but it's a pain to calculate\n",
"# by myself. Fortunately, this is Python. Someone else has done it for me, so I\n",
"# don't have to.\n",
"\n",
"import pyloudnorm\n",
"\n",
"meter = pyloudnorm.Meter(sample_rate)\n",
"print(\n",
" f\"Integrated loudness: {meter.integrated_loudness(sample_arr)} dB LUFS\"\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "6ef37be5-47cd-4dbb-836f-bdeaa1787c26",
"metadata": {},
"outputs": [],
"source": [
"# Let's smash everything in one function...\n",
"\n",
"sample_rate = 44_100\n",
"\n",
"def compare(peak: float, compare_function):\n",
" # Setup\n",
" global sample_rate\n",
" frequency = 1000\n",
" period = 1/4\n",
" seconds = 10\n",
" envelope_value = 7\n",
" going_up = False\n",
" # ---\n",
" one_cycle = sample_rate // frequency\n",
" no_of_up_samples = int(one_cycle * period)\n",
" for env_value in range(15, 1-1, -1):\n",
" starting_volume = env_value/15\n",
" #######################################################\n",
" if envelope_value == 0:\n",
" change_every = 0\n",
" else:\n",
" change_every = (sample_rate // 64) * envelope_value\n",
" samples = []\n",
" volume = starting_volume\n",
" if envelope_value > 7:\n",
" envelope_value = 7\n",
" elif envelope_value < 0:\n",
" envelope_value = 0\n",
" has_hit_zero_yet = False\n",
" first_sample_to_hit_zero = 0\n",
" for i in range(int(sample_rate * seconds)):\n",
" if change_every != 0:\n",
" if going_up:\n",
" if (i % change_every == 0) and (i != 0) and (volume < 1):\n",
" volume += 1/15\n",
" else:\n",
" if (i % change_every == 0) and (i != 0) and (volume > 0):\n",
" volume -= 1/15\n",
" if volume < 0:\n",
" volume = 0\n",
" elif volume > 1:\n",
" volume = 1\n",
" p = i % one_cycle\n",
" if p < no_of_up_samples:\n",
" samples += [peak * volume]\n",
" else:\n",
" samples += [-peak * volume]\n",
" if going_up:\n",
" if (volume > 14/15) and not has_hit_zero_yet:\n",
" has_hit_zero_yet = True\n",
" first_sample_to_hit_zero = i\n",
" else:\n",
" if (volume == 0) and not has_hit_zero_yet:\n",
" has_hit_zero_yet = True\n",
" first_sample_to_hit_zero = i\n",
" first_sample_to_hit_zero += change_every\n",
" compare_function(samples, first_sample_to_hit_zero, env_value)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "5647cd98-6ba1-4e7d-a7ed-65862640f792",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<tr><td>0f</td><td>-4.30</td><td>-4.30</td><td>1859</td><td>-7.30</td>?<td></tr>\n",
"<tr><td>0e</td><td>-4.90</td><td>-0.60</td><td>1750</td><td>-7.90</td>?<td></tr>\n",
"<tr><td>0d</td><td>-5.54</td><td>-0.65</td><td>1640</td><td>-8.54</td>?<td></tr>\n",
"<tr><td>0c</td><td>-6.24</td><td>-0.70</td><td>1531</td><td>-9.24</td>?<td></tr>\n",
"<tr><td>0b</td><td>-7.00</td><td>-0.76</td><td>1422</td><td>-10.00</td>?<td></tr>\n",
"<tr><td>0a</td><td>-8.29</td><td>-1.29</td><td>1312</td><td>-11.29</td>?<td></tr>\n",
"<tr><td>09</td><td>-9.25</td><td>-0.96</td><td>1203</td><td>-12.25</td>?<td></tr>\n",
"<tr><td>08</td><td>-10.33</td><td>-1.08</td><td>1094</td><td>-13.33</td>?<td></tr>\n",
"<tr><td>07</td><td>-11.56</td><td>-1.22</td><td>984</td><td>-14.56</td>?<td></tr>\n",
"<tr><td>06</td><td>-12.97</td><td>-1.41</td><td>875</td><td>-15.97</td>?<td></tr>\n",
"<tr><td>05</td><td>-15.49</td><td>-2.53</td><td>766</td><td>-18.49</td>?<td></tr>\n",
"<tr><td>04</td><td>-17.68</td><td>-2.18</td><td>656</td><td>-20.68</td>?<td></tr>\n",
"<tr><td>03</td><td>-18.99</td><td>-1.32</td><td>547</td><td>-21.99</td>?<td></tr>\n",
"<tr><td>02</td><td>-23.93</td><td>-4.93</td><td>328</td><td>-26.93</td>?<td></tr>\n",
"<tr><td>01</td><td>-31.58</td><td>-7.65</td><td>219</td><td>-34.58</td>?<td></tr>\n"
]
}
],
"source": [
"# …so I can get results for all the possible starting volumes bar 0\n",
"\n",
"import pyloudnorm\n",
"import numpy as np\n",
"\n",
"meter = pyloudnorm.Meter(sample_rate)\n",
"last_il = 0.0\n",
"\n",
"volume_map = {}\n",
"def something(samples, first_sample_to_hit_zero, env_value):\n",
" global last_il\n",
" # Default block size for pyloudnorm's integrated loudness function is .4 seconds\n",
" # so I need to adjust, otherwise the calculation will throw an error\n",
" if first_sample_to_hit_zero < (sample_rate * .4):\n",
" sample_arr = np.array(samples)\n",
" else:\n",
" # use the entire sample if the time to 0 or max is less than .4 seconds\n",
" sample_arr = np.array(samples[:first_sample_to_hit_zero])\n",
"\n",
" il = meter.integrated_loudness(sample_arr)\n",
" diff = il - last_il\n",
"\n",
" # in ms\n",
" effective_sound_length = first_sample_to_hit_zero/sample_rate*1000\n",
" \n",
" #print(\n",
" # f\"IL with init volume {hex(env_value)[2:].zfill(2)}: {il:.2f} dB LUFS -/+({diff:.2f})\"\n",
" #)\n",
" print(\n",
" f\"<tr><td>{hex(env_value)[2:].zfill(2)}</td><td>{il:.2f}</td><td>{diff:.2f}</td><td>{effective_sound_length:.0f}</td><td>{il-3.0:.2f}</td>?<td></tr>\"\n",
" )\n",
" last_il = il\n",
" \n",
"compare(1, something)\n",
"\n",
"# Now copy and paste the output to the cell below"
]
},
{
"cell_type": "markdown",
"id": "a713a0c1-276a-4e65-96af-99e8f80bedfa",
"metadata": {},
"source": [
"## Determining the mappings\n",
"\n",
"Assumes:\n",
"* **A single note is played to completion.** Does not consider multiple notes played in succession.\n",
"* Using a peak of +1.0 and a trough of -1.0.\n",
"* Values are approximate.\n",
"* Simple square wave emulation, not really accurate to the Game Boy's actual output.\n",
"* The envelope speed used is 7.\n",
"* The sample wave is a 1000 Hz 25% square wave.\n",
"* The mapping is the closest in target integrated volume.\n",
"* The integrated loudness uses `pyloudnorm`'s default based on ITU-R BS.1770-4.\n",
"\n",
"<table>\n",
"<tr><th>GB volume</th><th>Integrated</th><th>Difference</th><th>Effective note length in ms</th><th>Pan law volume target (at -3 dB)</th><th>Mapping (-3 dB)</th></tr>\n",
"<tr><td>0f</td><td>-4.30</td><td>-4.30</td><td>1859</td><td>-7.30</td><td>0b</td><td></td></tr>\n",
"<tr><td>0e</td><td>-4.90</td><td>-0.60</td><td>1750</td><td>-7.90</td><td>0b</td><td></td></tr>\n",
"<tr><td>0d</td><td>-5.54</td><td>-0.65</td><td>1640</td><td>-8.54</td><td>0a</td><td></td></tr>\n",
"<tr><td>0c</td><td>-6.24</td><td>-0.70</td><td>1531</td><td>-9.24</td><td>09</td><td></td></tr>\n",
"<tr><td>0b</td><td>-7.00</td><td>-0.76</td><td>1422</td><td>-10.00</td><td>09</td><td></td></tr>\n",
"<tr><td>0a</td><td>-8.29</td><td>-1.29</td><td>1312</td><td>-11.29</td><td>08</td><td></td></tr>\n",
"<tr><td>09</td><td>-9.25</td><td>-0.96</td><td>1203</td><td>-12.25</td><td>07</td><td></td></tr>\n",
"<tr><td>08</td><td>-10.33</td><td>-1.08</td><td>1094</td><td>-13.33</td><td>06</td><td></td></tr>\n",
"<tr><td>07</td><td>-11.56</td><td>-1.22</td><td>984</td><td>-14.56</td><td>06</td><td></td></tr>\n",
"<tr><td>06</td><td>-12.97</td><td>-1.41</td><td>875</td><td>-15.97</td><td>05</td><td></td></tr>\n",
"<tr><td>05</td><td>-15.49</td><td>-2.53</td><td>766</td><td>-18.49</td><td>04</td><td></td></tr>\n",
"<tr><td>04</td><td>-17.68</td><td>-2.18</td><td>656</td><td>-20.68</td><td>03</td><td></td></tr>\n",
"<tr><td>03</td><td>-18.99</td><td>-1.32</td><td>547</td><td>-21.99</td><td>03</td><td></td></tr>\n",
"<tr><td>02</td><td>-23.93</td><td>-4.93</td><td>328</td><td>-26.93</td><td>02</td><td></td></tr>\n",
"<tr><td>01</td><td>-31.58</td><td>-7.65</td><td>219</td><td>-34.58</td><td>01</td><td></td></tr>\n",
"<tr><td>00</td><td>-Inf</td><td>-</td><td>-</td><td>-</td><td>00</td><td></td></tr>\n",
"</table>\n",
"\n",
"Difference appears to the same no matter the peak level…\n",
"\n",
"Determining the mapping:\n",
"1. Find a hex volume range where the target volume in dB sits between. Ex. -7.30 is between -7.00 (`0b`) and -8.29 (`0a`)\n",
"2. Get the value of the upper limit, ex. in that case would be `0b`."
]
},
{
"cell_type": "markdown",
"id": "6fb897a3-a537-4aef-9be1-ace0eb80dde2",
"metadata": {},
"source": [
"Mapping so far:\n",
"\n",
"```asm\n",
"; for envelope 0\n",
" db 0, 1, 2, 3, 3, 4, 5, 5, 6, 7, 8, 8, 9, 10, 10, 11\n",
"; for envelope 1\n",
" db 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 7, 7, 10, 11, 12, 13\n",
"; for envelope 2\n",
" db 0, 1, 2, 3, 4, 4, 6, 6, 7, 8, 8, 11, 11, 11, 11, 11\n",
"; for envelope 3\n",
" db 0, 1, 2, 2, 4, 4, 5, 7, 7, 7, 7, 9, 9, 9, 11, 11\n",
"; for envelope 4\n",
" db 0, 1, 2, 3, 4, 5, 5, 5, 6, 7, 8, 8, 9, 10, 10, 12\n",
"; for envelope 5\n",
" db 0, 1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 9, 9, 10, 11, 12\n",
"; for envelope 6\n",
" db 0, 1, 2, 3, 3, 4, 5, 6, 6, 7, 8, 9, 9, 10, 11, 11\n",
"; for envelope 7\n",
" db 0, 1, 2, 3, 3, 4, 5, 6, 6, 7, 8, 9, 9, 10, 11, 11\n",
"```"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment