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.
I2S tone player
- Mike Teachman
- Posts: 155
- Joined: Mon Jun 13, 2016 3:19 pm
- Location: Victoria, BC, Canada
Re: I2S tone player
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:
Other forum members may have a more elegant python solution.
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)
Re: I2S tone player
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
Once again, Thanks Mike for the help and all suggestions are helpful to improve the code.
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()