Skip to content

Commit 3190877

Browse files
authored
Add files via upload
1 parent c26b82c commit 3190877

File tree

1 file changed

+157
-0
lines changed

1 file changed

+157
-0
lines changed

Thexecutor/audio.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Audio engine as developed by Transistortestor for Thumby Color and back ported by me with their
2+
# support to the original Thumby.
3+
4+
5+
# Implements raw 4-bit IMA ADPCM using more or less the standard way.
6+
# Files for this were processed as follows:
7+
# 1) Convert to 15625 Hz Mono, volume boosted, and pitch increased in Audacity
8+
# 2) Exported as wav file with IMA ADPCM encoding
9+
# 3) Converted to IMA with SoX (Sound eXchange):
10+
# 4) sox inputfilename outputfilename.ima
11+
12+
# Ported by Ace Geiger 2025
13+
# All comments below are from Transistortestor unless otherwise noted.
14+
15+
16+
#there is a slight tick every time a buffer is filled - I suspect that running the audio loop entirely in RAM will likely eliminate it.
17+
#the only part that still runs from flash is setting the pulse width, but configuring PWM with raw register writes if a hassle so it's been left for now.
18+
19+
import time
20+
import struct
21+
import _thread
22+
import array
23+
24+
bufsize = 800 #enough for 6 frames at 30 FPS at 8 KHz
25+
sampledelay = 125 #microseconds between samples
26+
buf1 = bytearray(bufsize)
27+
buf2 = bytearray(bufsize)
28+
data = None
29+
playing = False
30+
31+
#current buffer, pos in buffer, bufsize, current sample, total samples, buf1NeedsFilling, buf2NeedsFilling
32+
bufstate = array.array("I", [0, 0, bufsize, 0, 0, 1, 1])
33+
34+
IMAindextable = array.array("i", [ #it appears that only ptr32 works with signed numbers in viper
35+
-1, -1, -1, -1, 2, 4, 6, 8,
36+
-1, -1, -1, -1, 2, 4, 6, 8
37+
])
38+
39+
IMAsteptable = array.array("h", [
40+
7, 8, 9, 10, 11, 12, 13, 14, 16, 17,
41+
19, 21, 23, 25, 28, 31, 34, 37, 41, 45,
42+
50, 55, 60, 66, 73, 80, 88, 97, 107, 118,
43+
130, 143, 157, 173, 190, 209, 230, 253, 279, 307,
44+
337, 371, 408, 449, 494, 544, 598, 658, 724, 796,
45+
876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066,
46+
2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358,
47+
5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
48+
15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767
49+
])
50+
51+
@micropython.viper
52+
def audioloop():
53+
from machine import PWM, Pin
54+
pwm = PWM(Pin(28))
55+
pwm.freq(120000) # changed from pwm = PWM(Pin(28), freq=120000) to support microPython 1.19 and original Thumby -ace
56+
setwidth = pwm.duty_u16 #Redefining these reduces clicks. Directly writing to the register would be better, but this works.
57+
#curtime = time.ticks_us
58+
curtime = ptr32(0x40054028) #location of microsecond register, as per RP2040 datasheet and transistortestor (TIMERAWL of TIMER0) -ace
59+
state:ptr32 = ptr32(bufstate)
60+
b1:ptr8 = ptr8(buf1)
61+
b2:ptr8 = ptr8(buf2)
62+
delay:int = int(sampledelay)
63+
#nexttime:int = int(curtime()) + delay
64+
nexttime:int = (curtime[0] & 0x3fffffff) #mask off highest bit so it can't be treated as signed - should be 7 instead of 3 but can't due to viper funkiness
65+
66+
indextable:ptr32 = ptr32(IMAindextable)
67+
steptable:ptr16 = ptr16(IMAsteptable)
68+
prediction:int = 32768 #would normally be 0 and signed, but since duty_u16 is unsigned, the offset is built-in here. Over/underflow checks have been changed accordingly.
69+
index:int = 0
70+
step:int = steptable[index]
71+
delta:int = 0
72+
diff:int = 0
73+
74+
while state[3] < state[4]: #still playing
75+
if state[0]: #second buffer
76+
delta = b2[state[1]]
77+
else: #first buffer
78+
delta = b1[state[1]]
79+
80+
if state[3] & 1: #odd sample
81+
delta &= 0b1111 #NOTE: Some variants of IMA ADPCM swap which half is processed first
82+
state[1] += 1 #increment bufpos
83+
if state[1] >= state[2]: #if end of buffer
84+
state[5+state[0]] = 1 #set flag
85+
state[0] ^= 1 #swap buffers
86+
state[1] = 0 #reset bufpos
87+
else:
88+
delta >>= 4
89+
90+
state[3] += 1 #increment sample number
91+
92+
diff = step >> 3 #calculate next sample
93+
if delta & 0b100: diff += step
94+
if delta & 0b10: diff += (step >> 1)
95+
if delta & 0b1: diff += (step >> 2)
96+
if delta & 0b1000:
97+
prediction -= diff
98+
if prediction < 0: prediction = 0 #cap to valid range (normally -32768 with no offset)
99+
else:
100+
prediction += diff
101+
if prediction > 65535: prediction = 65535 #normally 32767 with no offset
102+
103+
index += indextable[delta] #update state
104+
if index < 0: index = 0
105+
elif index > 88: index = 88
106+
step = steptable[index]
107+
108+
#while int(curtime()) < nexttime: pass
109+
while (curtime[0] & 0x3fffffff) < nexttime or (curtime[0] & 0x3fffffff) - nexttime > 0x1fffffff: pass #wait for next sample, or for overflow
110+
setwidth(prediction) #TODO: Replace this line with raw register writes
111+
nexttime += delay
112+
nexttime &= 0x3fffffff #keep within valid range
113+
114+
setwidth(0)
115+
pwm.deinit()
116+
print("Thread ended")
117+
stop()
118+
119+
120+
def fillbufs():
121+
global bufstate
122+
if bufstate[5]: #5 and 6 are the buffer empty flags
123+
data.readinto(buf1)
124+
bufstate[5] = 0
125+
print("buf1 filled")
126+
if bufstate[6]:
127+
data.readinto(buf2)
128+
bufstate[6] = 0
129+
print("buf2 filled")
130+
131+
132+
validrates = [15625, 12500, 10000, 8000, 6250, 5000, 4000] #not exhaustive - anything that evenly divides 1000000, provided fillbuffs() is called often enough to keep up
133+
def load(f, samplerate, samplecount):
134+
global data, buf1, buf2, bufstate, sampledelay
135+
if not samplerate in validrates:
136+
print("Unsupported sample rate")
137+
return
138+
sampledelay = 1000000//samplerate
139+
data = f
140+
buf1 = bytearray(bufsize)
141+
buf2 = bytearray(bufsize)
142+
#current buffer, pos in buffer, bufsize, current sample, total samples, buf1NeedsFilling, buf2NeedsFilling
143+
bufstate = array.array("I", [0, 0, bufsize, 0, samplecount, 1, 1])
144+
fillbufs()
145+
print("loaded")
146+
147+
148+
def play():
149+
global playing
150+
playing = True
151+
_thread.start_new_thread(audioloop, ())
152+
153+
154+
def stop():
155+
global playing
156+
playing = False
157+
bufstate[3] = bufstate[4] #set current sample to limit

0 commit comments

Comments
 (0)