Replies: 6 comments 11 replies
-
|
What processor are you using? Does it have a floating point processor (unit)? I would suggest looking at I2S for the DAC process. See: https://github.com/miketeachman/micropython-i2s-examples Also to get a more accurate time tick difference try:
this will work around overflow in the tick timer. Also https://hackaday.com/2024/08/08/tulip-is-a-micropython-synth-workstation-in-an-esp32/ Also for curiosity see: https://www.youtube.com/watch?v=pktBirYcHD0 https://github.com/Lana-chan/Cardputer-MicroHydra/blob/main/MicroHydra/lib/beeper.py |
Beta Was this translation helpful? Give feedback.
-
|
In case you're not aware, the Tulip Creative Computer is playing in a similar space that might have a lot you can investigate. More specifically, the creator, @bwhitman, has also created the AMY music synthesizer library to power Tulip. |
Beta Was this translation helpful? Give feedback.
-
|
Thanks @pi-mst . Yes, we have spent a lot of time getting synths working in Micropython. Like Circuitpython's |
Beta Was this translation helpful? Give feedback.
-
|
Quick update, using @micropython.native decorator and ulab's numpy trig functions, helped bring the time down to import math
import time
from ulab import numpy as np
from ulab import scipy as sci
# Define note frequencies (middle octave - 4)
BASE_NOTES = {
'C': 262,
'C#': 277,
'D': 294,
'D#': 311,
'E': 330,
'F': 349,
'F#': 370,
'G': 392,
'G#': 415,
'A': 440,
'A#': 466,
'B': 494
}
@micropython.native
def sine_wave(freq,x):
return math.sin(freq * 2 * np.pi * x) # Has frequency of 440Hz
@micropython.native
def saw_wave(freq,x):
value = np.atan(np.tan(freq*np.pi*x))
return value
@micropython.native
def tri_wave(freq,x):
return 0.6366*np.asin(np.sin(freq*2*np.pi*x))
@micropython.native
def sqr_wave(freq:int,x:ptr32):
if (x * freq) % 1 < 0.5:
return 1.0
else:
return -1.0
@micropython.native
def generateWaveTable(sampleRate,duration,func,freq,vol):
n_samples = sampleRate*duration
step = duration / (n_samples - 1)
f_output = []
maxint16 = 2**15-1
for i in range(n_samples):
t = step * i
out = func(freq,t)*vol
f_output.append(int(out*maxint16))
return f_output
@micropython.native
def generateByteWaveTable(sampleRate,n_samples,func,freq,vol):
#n_samples = sampleRate*duration
step = duration / (n_samples - 1)
output = bytearray()
maxint16 = 2**15-1
for i in range(n_samples):
t = step * i
out = func(freq,t)*vol
output += int(out*maxint16).to_bytes(2,'little')
return output
sampleRate = 44100
frequency = BASE_NOTES['C']
duration = 1
wavelenght = 1/frequency
n_samples = int(wavelenght*sampleRate)
waveform = sqr_wave
volume = 1
inittime = time.ticks_ms()
y_bytes = generateByteWaveTable(sampleRate,n_samples,waveform,frequency,volume)
print("Time to produce WaveTable:",time.ticks_diff(time.ticks_ms() , inittime),"ms")
print("Sample Duration:",n_samples/sampleRate*1000," ms") |
Beta Was this translation helpful? Give feedback.
-
|
There is a lot that can be done to improve the code in the original post. Use pre-allocated integer arrays rather than variable size lists. Use integer processing with the Viper code emitter. On RP2 consider the use of DMA to transfer the integer array contents to the I2S DAC (I'm not sure if this is possible). Find faster algorithms! def saw_wave(freq,x):
value = math.atan(math.tan(freq*math.pi*x))
return valueCunning, but slow. It's easy to generate saw, triangle and square waves with integer processing. It may be faster to generate an integer sin with an integer lookup table and interpolation. However the CircuitPython library looks highly developed and is written in C. It depends whether you want an out-of-the-box solution or a project! |
Beta Was this translation helpful? Give feedback.
-
|
The code I posted previously has a mistake in it, the step size is calculated for 1 second where it should calculated for 3.8 ms, but for the proposes of comparing performance i think its ok. I did a bare minimal example with viper optimization and got down to 7 ms import math
import time
# Define note frequencies (middle octave - 4) Hz
BASE_NOTES_HERTZ = {
'C': 262,
'C#': 277,
'D': 294,
'D#': 311,
'E': 330,
'F': 349,
'F#': 370,
'G': 392,
'G#': 415,
'A': 440,
'A#': 466,
'B': 494
}
@micropython.viper
def sqr_wave(half_w_length:int,x:int) -> int:
if x < half_w_length:
return int(32767)
else:
return int(-32767)
@micropython.native
def generateByteWaveTable(sampleRate,func,freq,vol):
w_len = 1/frequency
w_len_us = w_len*1000000
half_w_len = int(round(w_len_us/2))
n_samples = int(w_len*sampleRate)
step = w_len / (n_samples - 1)
step_us = round(step*1000000)
output = bytearray(n_samples*2)
#output = bytearray()
for i in range(n_samples):
t = int(step_us * i)
out = func(half_w_len,t)*vol
out_bytes = out.to_bytes(2,'little')
output[i*2] = out_bytes[0]
output[(i*2)+1] = out_bytes[1]
return output
sampleRate = 44100
frequency = BASE_NOTES_HERTZ['C']
wavelenght = 1/frequency
n_samples = int(wavelenght*sampleRate)
waveform = sqr_wave
volume = 1
inittime = time.ticks_us()
WaveTable_bytes = generateByteWaveTable(sampleRate,waveform,frequency,volume)
print("Time to produce WaveTable:",time.ticks_diff(time.ticks_us(), inittime)/1000,"ms")
print("Wavetable Duration:",(n_samples/sampleRate)*1000," ms")
print(WaveTable_bytes)I plotted the WaveTable on Jupyter notebook, and seems to be correct: As @GitHubsSilverBullet mentioned 1/(n_samples/sampleRate) is not quite the right frequency and needs to be sampled a little longer to match, but I think this serves as a good indication. Its still not fast enough to produce the waveform faster than playing it, Its almost there and can probably be optimized further. However doesn't leave much room for adding effects on top of these simple tones |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
-
Hi, I'm working on a synthesizer project and wanted to write a sound synthesis library for Micropython, I went as far as writing a oscillator in pure python to produce a bytearray to be passed to a DAC over I2S. The idea being that on a dual core microcontroller (eg: pi pico) i could control the parameters of the synthesizer on one core and on a different thread I would continuously play a wavetable on the I2C DAC. I went as far as writing a small test to see if it would work. and it does! but it currently takes more time to produce the wavetable then to actually play it:
Output on a Pi Pico (rp2040)
Are there any tricks I could use to speed this up? or this is the wrong approach? is sending the I2S words directly to the DAC better? I know circuit-python has a quite extensive C library for sound synthesis called synthio but would be nice if I could do this in mycropython
Beta Was this translation helpful? Give feedback.
All reactions