miketeachman/micropython-i2s-examples

Having trouble porting play_tone.py to non-blocking

lerouxb opened this issue · 3 comments

When running the play_tone.py example I can hear a perfect sine wave. But then if I minimally change it to be non-blocking I just hear a buzzing sound. I must be missing something silly, but I just can't spot it. Maybe an official non-blocking minimal example that just plays a tone exactly like the blocking one would be a nice sanity check to have?

Here's my blocking code which is just the example stripped down to just the ESP32 parts and gpio pins changed to what I'm using:

import os
import math
import struct
import time
import micropython
from machine import I2S
from machine import Pin

def make_tone(rate, bits, frequency):
    # create a buffer containing the pure tone samples
    samples_per_cycle = rate // frequency
    sample_size_in_bytes = bits // 8
    samples = bytearray(samples_per_cycle * sample_size_in_bytes)
    volume_reduction_factor = 32
    range = pow(2, bits) // 2 // volume_reduction_factor
    
    if bits == 16:
        format = "<h"
    else:  # assume 32 bits
        format = "<l"
    
    for i in range(samples_per_cycle):
        sample = range + int((range - 1) * math.sin(2 * math.pi * i / samples_per_cycle))
        struct.pack_into(format, samples, i * sample_size_in_bytes, sample)
        
    return samples

# ======= I2S CONFIGURATION =======
SCK_PIN = 18
WS_PIN = 23
SD_PIN = 19
I2S_ID = 0
BUFFER_LENGTH_IN_BYTES = 2000
# ======= I2S CONFIGURATION =======

# ======= AUDIO CONFIGURATION =======
TONE_FREQUENCY_IN_HZ = 440
SAMPLE_SIZE_IN_BITS = 16
FORMAT = I2S.MONO  # only MONO supported in this example
SAMPLE_RATE_IN_HZ = 44100
# ======= AUDIO CONFIGURATION =======

audio_out = I2S(
    I2S_ID,
    sck=Pin(SCK_PIN),
    ws=Pin(WS_PIN),
    sd=Pin(SD_PIN),
    mode=I2S.TX,
    bits=SAMPLE_SIZE_IN_BITS,
    format=FORMAT,
    rate=SAMPLE_RATE_IN_HZ,
    ibuf=BUFFER_LENGTH_IN_BYTES,
)

samples = make_tone(SAMPLE_RATE_IN_HZ, SAMPLE_SIZE_IN_BITS, TONE_FREQUENCY_IN_HZ)

print("==========  START PLAYBACK ==========")
try:
    while True:
        num_written = audio_out.write(samples)

except (KeyboardInterrupt, Exception) as e:
    print("caught exception {} {}".format(type(e).__name__, e))

# cleanup
audio_out.deinit()
print("Done")

And then here's the same thing with just the few changes that I think are necessary to turn that non-blocking:

import os
import math
import struct
import time
import micropython
from machine import I2S
from machine import Pin

def make_tone(rate, bits, frequency):
    # create a buffer containing the pure tone samples
    samples_per_cycle = rate // frequency
    sample_size_in_bytes = bits // 8
    samples = bytearray(samples_per_cycle * sample_size_in_bytes)
    volume_reduction_factor = 32
    range = pow(2, bits) // 2 // volume_reduction_factor
    
    if bits == 16:
        format = "<h"
    else:  # assume 32 bits
        format = "<l"
    
    for i in range(samples_per_cycle):
        sample = range + int((range - 1) * math.sin(2 * math.pi * i / samples_per_cycle))
        struct.pack_into(format, samples, i * sample_size_in_bytes, sample)
        
    return samples

# ======= I2S CONFIGURATION =======
SCK_PIN = 18
WS_PIN = 23
SD_PIN = 19
I2S_ID = 0
BUFFER_LENGTH_IN_BYTES = 2000
# ======= I2S CONFIGURATION =======

# ======= AUDIO CONFIGURATION =======
TONE_FREQUENCY_IN_HZ = 440
SAMPLE_SIZE_IN_BITS = 16
FORMAT = I2S.MONO  # only MONO supported in this example
SAMPLE_RATE_IN_HZ = 44100
# ======= AUDIO CONFIGURATION =======

audio_out = I2S(
    I2S_ID,
    sck=Pin(SCK_PIN),
    ws=Pin(WS_PIN),
    sd=Pin(SD_PIN),
    mode=I2S.TX,
    bits=SAMPLE_SIZE_IN_BITS,
    format=FORMAT,
    rate=SAMPLE_RATE_IN_HZ,
    ibuf=BUFFER_LENGTH_IN_BYTES,
)

samples = make_tone(SAMPLE_RATE_IN_HZ, SAMPLE_SIZE_IN_BITS, TONE_FREQUENCY_IN_HZ)

def i2s_callback(arg):
    num_written = audio_out.write(samples)

audio_out.irq(i2s_callback) 

print("==========  START PLAYBACK ==========")
num_written = audio_out.write(samples)
try:
    while True:
        pass

except (KeyboardInterrupt, Exception) as e:
    print("caught exception {} {}".format(type(e).__name__, e))

# cleanup
audio_out.deinit()
print("Done")

I studied the micropython documentation and all the other examples and I think that should just work, yet it doesn't.

Any ideas welcome.

Btw I did try and dramatically lower the sample rate (16000) and increase the buffer size (20000) and it made no difference. Slightly different buzzing, but still no tone.

I'm running with the latest master branch code of micropython and v5.0.4 of esp-idf on a ESP32-PICO-KIT V4, so admittedly no PSRAM.

Oh and this is with Adafruit's MAX98357 module. Not that I think that would matter because as I said - the blocking version works fine.

I've got an idea on the problem you see. On the ESP32 port non-blocking mode is implemented using a FreeRTOS task. This task shares processor time with the main MicroPython task. I think every task gets a 10ms time slice. The issue may arise when the MicroPython task runs for 10ms and the I2S transmit buffer runs out of samples because samples is quite small in size. That would create the sound you hear. To test this theory try making samples larger. e.g. samples = samples * 100. Try different multipliers to see if the problem goes away.

Thanks! That does make sense and seems to help. I had to also increase the ibuf value before it went away entirely. I suppose that also makes sense because ibuf should probably be bigger than the samples buffer size. I'm guessing my buffer should be at least 10ms long, then? And the 440hz single cycle was only 200 samples. at 44100hz that's only 4.5ms.

I was already not sure if I want to be going the non-blocking way or the asyncio way and I think this is making me lean towards the latter for my use case. Although.. in reality I won't be sending single cycles of pure sine waves, so it shouldn't matter too much if I have to tweak the buffer size. For anything that has to respond quickly to user input you should probably safely make the buffer size (in terms of milliseconds) close to the control loop size? With 30 or 60 hertz being 16.6 to 33.3ms. I'm just thinking out loud while I try and map this out in my brain 😆

(Just for fun I'm trying to see if it would be viable to write a chiptunesque basic synthesizer entirely in micropython without having to put the audio engine parts in c and then only controlling the node graph (or whatever) from micropython. I'm probably gonna hit a wall, but like I said - just having fun.)

Edit: Nevermind. Immediate brick wall. Floating point performance is so slow even at 240MHz it cannot even increment an oscillator phase fast enough to do a square wave synth 😆