Skip to content

Instantly share code, notes, and snippets.

@todbot
Created August 15, 2024 00:20
Show Gist options
  • Save todbot/046b1c8243002ef76c5e2fabae84ba0e to your computer and use it in GitHub Desktop.
Save todbot/046b1c8243002ef76c5e2fabae84ba0e to your computer and use it in GitHub Desktop.
My sound piece attempting to recreate the THX "Deep Note" sound, with display
# derpnote2-rp2350.py - My sound piece attempting to recreate the THX "Deep Note" sound, with display
# 14 Aug 2024 - @todbot / Tod Kurt
#
import asyncio
import board, audiopwmio, audiomixer, audiocore, synthio, random, time
import busio, displayio, terminalio, vectorio
import keypad
import neopixel
import adafruit_displayio_ssd1306
from adafruit_display_text import bitmap_label as label
import ulab.numpy as np
myfont = terminalio.FONT
SAMPLE_RATE = 22_000
BUFFER_SIZE = 4096
led = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.1)
keys = keypad.Keys( (board.D6,), value_when_pressed=False)
audio = audiopwmio.PWMAudioOut(board.D9)
mixer = audiomixer.Mixer(voice_count=1, channel_count=1, sample_rate=SAMPLE_RATE, buffer_size=BUFFER_SIZE)
synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE)
audio.play(mixer)
mixer.voice[0].play(synth)
displayio.release_displays()
i2c = busio.I2C(board.D21, board.D20, frequency=800_000)
dw, dh = 128, 64
display_bus = displayio.I2CDisplay(i2c, device_address=0x3d)
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=dw, height=dh, rotation=0)
maingroup = displayio.Group()
display.root_group = maingroup
boxpal = displayio.Palette(2)
boxpal[1] = 0xffffff # the only color we got
boxouter = vectorio.Rectangle(pixel_shader=boxpal, color_index=1, width=dw-1, height=dh-1, x=0,y=0)
boxinner = vectorio.Rectangle(pixel_shader=boxpal, color_index=0, width=dw-8, height=dh-8, x=4,y=4)
box = displayio.Group()
box.append(boxouter)
box.append(boxinner)
maingroup.append(box)
text1 = label.Label(myfont, scale=3, text="~THX", x=25, y=dh+20)
maingroup.append(text1)
# create waves used by Deep Note
SAMPLE_SIZE = 256
SAMPLE_AMP = 30_000
wave_saw = np.linspace(SAMPLE_AMP, -SAMPLE_AMP, num=SAMPLE_SIZE, dtype=np.int16)
wave_noise = np.array([random.randint(-SAMPLE_AMP, SAMPLE_AMP) for i in range(SAMPLE_SIZE)], dtype=np.int16)
my_wave = wave_saw
# standard linear interpolate
def lerp(a, b, t): return (1-t)*a + t*b
# Deep Note MIDI note names to note numbers
# A2 D1 D2 D3 A3 D4 A4 D5 A5 D6 F#6
# 45 26 38 50 57 62 69 74 81 86 90
# from https://www.phpied.com/webaudio-deep-note-part-4-multiple-sounds/
note_start = 45 # A2
notes_deepnote = ( 26, 38, 50, 57, 62, 69, 74, 81,
62, 69, 74, 81, 86, 86, 90, 90 );
# Deep Note lifecycle
stage1_time = 1 # static random chaos
stage2_time = 3 # moving random chaos
stage3_time = 8 # converge on big chord
stage4_time = 5 # hold on big chord
time_steps = 100 # iterations per stage
num_oscs = 12
notes = [None] * num_oscs
lfos = [None] * num_oscs
notesS1 = [random.uniform(note_start, note_start+12) for _ in range(num_oscs)]
notesS2 = [random.uniform(note_start+30, note_start) for _ in range(num_oscs)]
notesS3 = notes_deepnote[0:num_oscs]
amp_env = synthio.Envelope(attack_time=0.5, release_time=3, sustain_level=0.75, attack_level=0.75)
for i in range(num_oscs):
lfos[i] = synthio.LFO(rate=0.001,
scale=random.uniform(0.07,0.05),
phase_offset=random.random(),
waveform=wave_noise)
notes[i] = synthio.Note( synthio.midi_to_hz(notesS1[i]),
waveform=my_wave,
envelope=amp_env, bend=lfos[i])
do_deepnote = False
async def deepnote_loop():
global do_deepnote
while True:
if not do_deepnote:
#print("waiting")
await asyncio.sleep(0.01)
continue
# stage 1 is static random chaos (as set above with random LFOs)
print("starting stage 1")
synth.press(notes)
await asyncio.sleep(stage1_time)
#print("debug wait")
#time.sleep(10)
# stage 2 is moving chaos, where oscs move randomly towards a random destination pitch over a (random) time
print("starting stage 2")
for t in range(time_steps):
for i in range(num_oscs):
notes[i].frequency = lerp( synthio.midi_to_hz(notesS1[i]),
synthio.midi_to_hz(notesS2[i]), t/time_steps)
lfos[i].scale = lfos[i].scale * 0.97
await asyncio.sleep(stage2_time/time_steps)
# stage 3 is converge on big chord
print("starting stage 3")
for t in range(time_steps):
for i in range(num_oscs):
lfos[i].scale = max(lfos[i].scale * 0.99, 0.001)
notes[i].frequency = lerp( synthio.midi_to_hz(notesS2[i]),
synthio.midi_to_hz(notesS3[i]), t/time_steps)
await asyncio.sleep(stage3_time/time_steps)
#print(notes[i].frequency, lfos[i].scale, t/time_steps)
print("starting stage 4")
await asyncio.sleep(stage4_time)
synth.release_all()
do_deepnote = False
async def display_updater():
global do_deepnote
while True:
#box.hidden = True
text1.y = dh+40
y = text1.y
desty = 35
f = 0.996
while not do_deepnote:
await asyncio.sleep(0.05)
while y > desty+0.01:
y = f * y + (1-f) * desty # simple filter
text1.y = int(y)
await asyncio.sleep(0.01)
await asyncio.sleep(1)
box.hidden = False
while do_deepnote:
await asyncio.sleep(0.01)
async def key_watcher():
global do_deepnote
while True:
if key := keys.events.get():
print("KEY")
if key.pressed:
do_deepnote = True
await asyncio.sleep(0.01)
async def main():
await asyncio.gather(
asyncio.create_task( deepnote_loop() ),
asyncio.create_task( display_updater() ),
asyncio.create_task( key_watcher() ),
)
asyncio.run(main())
@todbot
Copy link
Author

todbot commented Aug 15, 2024

video demo:

derpnote2_rp2350.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment