I2S tone player

RP2040 based microcontroller boards running MicroPython.
Target audience: MicroPython users with an RP2040 boards.
This does not include conventional Linux-based Raspberry Pi boards.
Post Reply
sushyoshi
Posts: 21
Joined: Sat Feb 05, 2022 9:25 am

I2S tone player

Post by sushyoshi » Tue Feb 15, 2022 6:36 am

Hi all,

This is a follow-up to my previous post about I2S Dac

I have been trying to play around with the I2S capabilities to play simple sine wave tones. This is possible, however, I have not been able to successfully add user input to this. Here are some of the things I tried:

1 - Using the I2S micropython examples, they show different ways to output sound through your DAC. There are several examples to play a wav file, either from the SD card or with a file in the flash memory. All of these examples work great! There is an example to play a sine wave tone, which became my main focus. The sine wave example shows how to continuously loop a sine wave at a given frequency, however, if I want to change the frequency of the sound, I need to stop the program and change the values and restart again.

2 - I have been trying to change the frequency of the sine wave with user input, using the example code as a base, but I have not been very successfully. I believe that to be able to do this, the code need to create a new array with the sine wave data on each while True: loop., which is not happening with the example code.

3 - I was wondering if we could just create a small array with sine wave values (1 period), and on the while True: loop we just change the frequency that the wave is repeated.

Maybe my approach to this is not the best one, but I am really interested to know if anyone has been working on this and can help me out?

Thanks.

User avatar
Mike Teachman
Posts: 155
Joined: Mon Jun 13, 2016 3:19 pm
Location: Victoria, BC, Canada

Re: I2S tone player

Post by Mike Teachman » Wed Feb 16, 2022 2:43 pm

Here is one approach to try:

1. create a new function, create_tone(tone_frequency), that returns a bytearray of samples for the requested tone. The functions would contain the code from lines:
https://github.com/miketeachman/micropy ... one.py#L84
to
https://github.com/miketeachman/micropy ... one.py#L98

there would be a return statement at the end, e.g. return samples

2. create two tone arrays:
tone_440 = create_tone(440) # create bytearray of samples for 440Hz tone
tone_1000 = create_tone(1000) # create bytearray of samples for 1000Hz tone

3. In the while loop:
add logic to detect the button push and then switch the tone array when the button is pressed.
something like this:

Code: Select all

tone = tone_440
while True:
   num_written = audio_out.write(tone)

   if button press detected:
       add code to change to the required tone (e.g.  tone=tone_1000 or other tone)
Other forum members may have a more elegant python solution.

sushyoshi
Posts: 21
Joined: Sat Feb 05, 2022 9:25 am

Re: I2S tone player

Post by sushyoshi » Thu Feb 17, 2022 4:01 pm

Thanks for the suggestion.

So the problem that I was having was that the Pico was having difficulty outputting sound at higher frequencies (higher than 550 Hz).

I followed your advice and went on to try different ways to get this to work. I was hoping that if the program accessed a sample array, instead of creating one each time a button is pressed (in the while True loop), it would save more processing power to the sound output. However, after many attempts, this did not improve the sound output and the results were similar.

After that I thought about using the 2nd core of the Pico, and dedicate this core just for outputting sound (num_written = audio_out.write(tone_sample)) . My initial results were successful. Keeping 1 core just for the audio out seems to do the trick.

Here is the code I am working on for anyone interested. I am sure there are a lot of different ways to do this, but for now I believe I am on a good track. The code below is using a tft display, pcf8574 gpio expander with touch buttons as user input buttons

Code: Select all

# Coded in Micropython

# Pinout:

    # PCF8574:
    #  SDA - GP8
    #  SCL - GP9
    
    # ST7735 128x160 TFT LCD Display
    #  SCL - GP10
    #  SDA - GP11
    #   DC - GP12
    #  RES - GP13
    #   CS - GP14
    
    # PCM5102 I2S DAC
    #  BCK - GP16
    #  LCK - GP17
    #  DIN - GP18
    
    # B1_LED - GP21
    # B2_LED - GP22
    
from machine import I2C, I2S, Pin, SPI
import struct
import time
import array
import math
from ST7735 import TFT
from sysfont import sysfont
import pcf8574
import _thread

#======= SPI TFT CONFIGURATION =======
spi = SPI(1, baudrate=20000000, polarity=0, phase=0,
          sck=Pin(10), mosi=Pin(11), miso=None)
tft=TFT(spi,12,13,14)
tft.initr()
tft.rgb(True)
tft.rotation(3)
tft.fill(TFT.BLACK)

# ======= I2C GPIO Expander CONFIGURATION =======
i2c = I2C(0, scl=Pin(9), sda=Pin(8))
pcf1 = pcf8574.PCF8574(i2c, 0x20)    # addresses can be 0x20-0x27
pcf2 = pcf8574.PCF8574(i2c, 0x24)    # addresses can be 0x20-0x27


# ======= I2S CONFIGURATION =======
SCK_PIN = 16
WS_PIN = 17
SD_PIN = 18
I2S_ID = 0
BUFFER_LENGTH_IN_BYTES = 1000


# ======= AUDIO CONFIGURATION =======

TONE_FREQUENCY_IN_HZ = 440
last_TONE_FREQUENCY_IN_HZ = 0
SAMPLE_SIZE_IN_BITS = 16
FORMAT = I2S.MONO  # only MONO supported in this example
SAMPLE_RATE_IN_HZ = 8000 

stop_audio = 0
last_stop_audio = 0

# # allocate a small array of blank samples
silence = bytearray(50)

# ======= 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,
)

       
# ======= TOUCH BUTTONS ARRAYS =======
        
touch = array.array('i', ([0]*12))
last_touch = array.array('i', ([0]*12))


# ======= CREATE SAMPLES FUNCTIONS =======
   
def sine_tone(TONE_FREQUENCY_IN_HZ):

        # create a buffer containing the pure tone samples
    samples_per_cycle = SAMPLE_RATE_IN_HZ // TONE_FREQUENCY_IN_HZ
    sample_size_in_bytes = SAMPLE_SIZE_IN_BITS // 8
    samples = bytearray(samples_per_cycle * sample_size_in_bytes)
    volume_reduction_factor = 32
    srange = pow(2, SAMPLE_SIZE_IN_BITS) // 2 // volume_reduction_factor

    if SAMPLE_SIZE_IN_BITS == 16:
        format = "<h"
    else:  # assume 32 bits
        format = "<l"

    for i in range(samples_per_cycle):
        sample = srange + int((srange - 1) * math.sin(2 * math.pi * i / samples_per_cycle))
        struct.pack_into(format, samples, i * sample_size_in_bytes, sample)

    return (samples)

# ======= CREATE SAMPLES =======

sine_440 = sine_tone(440)
sine_494 = sine_tone(494)
sine_523 = sine_tone(523)
sine_587 = sine_tone(587)
sine_659 = sine_tone(659)
sine_698 = sine_tone(698)
sine_783 = sine_tone(783)

tone_sample = sine_440 # initial sample to be in buffer

# ======= PRINT TONE SAMPLES as int (for debugging purposes) =======

int_sine_440 = [x for x in sine_440]
int_sine_494 = [x for x in sine_494]
int_sine_523 = [x for x in sine_523]
int_sine_587 = [x for x in sine_587]
print("sine_440 = ", int_sine_440)
print("sine_494 = ", int_sine_494)
print("sine_523 = ", int_sine_523)
print("sine_587 = ", int_sine_587)


# ======= SECOND THREAD - AUDIO OUT ENGINE =======

def second_thread():
    
    while True:        
        for t in range(0, 7):
            if touch[t] == 1:                    
                num_written = audio_out.write(tone_sample)  # HERE IS WHERE THE SAMPLE IS PLAYED
                for i in range (1, 6):
                    touch[t+i] = 0                 # makes only 1 touch key is pressed at a time
                    touch[t-i] = 0 

_thread.start_new_thread(second_thread, ())


while True:

# ======= READ TOUCH BUTTONS =======
    for n in range(6, -1, -1):       # range pcf1.pin 6 to 0
        if pcf1.pin(n):
            touch[(n*(-1))+6] = 1    # coverts range from 6 - 0 to touch[0 - 6], then sets value 1
            if touch[(n*(-1))+6] != last_touch[(n*(-1))+6]:  # if current value != than previous             
#                 print("touch ", ((n*(-1))+6), " pressed!")  # the above if causes actions to only             
                
                if touch[0] == 1:    # When a touch button is pressed, a sample array is selected
                    tone_sample = sine_440
                if touch[1] == 1:
                    tone_sample = sine_494
                if touch[2] == 1:
                    tone_sample = sine_523
                if touch[3] == 1:
                    tone_sample = sine_587
                if touch[4] == 1:
                    tone_sample = sine_659
                if touch[5] == 1:
                    tone_sample = sine_698
                if touch[6] == 1:
                    tone_sample = sine_783    
                    
                last_touch[(n*(-1))+6] = touch[(n*(-1))+6]   # be performed once
        else:
            if touch[(n*(-1))+6] == 1:
                touch[(n*(-1))+6] = 0
                last_touch[(n*(-1))+6] = 0
                   

    if pcf2.pin(2):
        stop_audio = 1
        if stop_audio != last_stop_audio:
            audio_out.deinit()               
            print("Done")                      # touch 4 stops the audio engine
            last_stop_audio = stop_audio
            _thread.exit()
Once again, Thanks Mike for the help and all suggestions are helpful to improve the code.

Post Reply