Created
August 15, 2024 00:20
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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()) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
video demo:
derpnote2_rp2350.mp4