You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
# 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
+
importtime
20
+
importstruct
21
+
import_thread
22
+
importarray
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
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
+
whilestate[3] <state[4]: #still playing
75
+
ifstate[0]: #second buffer
76
+
delta=b2[state[1]]
77
+
else: #first buffer
78
+
delta=b1[state[1]]
79
+
80
+
ifstate[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
+
ifstate[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
+
ifdelta&0b100: diff+=step
94
+
ifdelta&0b10: diff+= (step>>1)
95
+
ifdelta&0b1: diff+= (step>>2)
96
+
ifdelta&0b1000:
97
+
prediction-=diff
98
+
ifprediction<0: prediction=0#cap to valid range (normally -32768 with no offset)
99
+
else:
100
+
prediction+=diff
101
+
ifprediction>65535: prediction=65535#normally 32767 with no offset
102
+
103
+
index+=indextable[delta] #update state
104
+
ifindex<0: index=0
105
+
elifindex>88: index=88
106
+
step=steptable[index]
107
+
108
+
#while int(curtime()) < nexttime: pass
109
+
while (curtime[0] &0x3fffffff) <nexttimeor (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
+
deffillbufs():
121
+
globalbufstate
122
+
ifbufstate[5]: #5 and 6 are the buffer empty flags
123
+
data.readinto(buf1)
124
+
bufstate[5] =0
125
+
print("buf1 filled")
126
+
ifbufstate[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
+
defload(f, samplerate, samplecount):
134
+
globaldata, buf1, buf2, bufstate, sampledelay
135
+
ifnotsamplerateinvalidrates:
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
0 commit comments