micropython-i2s-examples icon indicating copy to clipboard operation
micropython-i2s-examples copied to clipboard

Possible to play multiple .wav's simultaneously?

Open Theagainmen opened this issue 2 years ago • 5 comments

Hello,

First of thank you for creating this awesome MicroPython library, very useful!

I was wondering if it's possible to play multiple .wav files simultaneously?

Thanks for any help.

Theagainmen avatar Apr 11 '22 19:04 Theagainmen

I assume you would like to mix together the samples from the audio wav files and play using one speaker.

Here is some pseudo-code showing one possible approach, for mixing together audio samples from two wave files:

construct I2S object with sample rate, bit size, etc
define a bytearray buffer 1, 2000 bytes in length
define a bytearray buffer 2, 2000 bytes in length
define a bytearray buffer 3, 2000 bytes in length
open wav file 1
open wav file 2

loop:
  read audio samples from wav file 1 into buffer 1
  read audio samples from wav file 2 into buffer 2
  add the samples from buffers 1 and 2, store results in buffer 3
  write samples from buffer 3 to I2S object using the I2S.write() method

notes:

  • use the I2S.shift() method if volume reduction is needed for any of the sample buffers
  • the wav files should be the same audio frequency
  • for efficient addition of samples, consider writing a function using the Viper emitter
    • https://forum.micropython.org/viewtopic.php?f=2&t=2480&hilit=viper&start=10#p65638
    • https://docs.micropython.org/en/v1.9.3/pyboard/reference/speed_python.html#the-viper-code-emitter
  • read the wav files using the machine.sdcard class which is much faster than the sdcard.py driver
  • if you get gaps in the audio playback (underflow) you may need convert the wav files to a lower sample rate, e.g. 11025 Hz

I hope this will help you to get started !

miketeachman avatar Apr 15 '22 16:04 miketeachman

Exactly this was also my question! But first things first: Thanks for your efforts. Especially the nice examples section is really helpful to get started.

In terms of the mixing of samples I have an additional demand, and I thought about solutions for solving this. I am going to program a sampler for a modular synthesizer, that have 8 triggers (either CV or push buttons) and than plays up to 8 wavs through 1 output via a i2s amplifier (98357A).

However, the problem is, that wavs can be played at any time. So e.g. the first wav is triggered, then after 500ms the second is triggered and so on and so on. Having a potential unlimited overlap of sample but a 'play buffer' of a limited size.

So my thoughts would be, add the samples dynamically into the play buffer at certain positions (when a trigger occurs) and if at the end of the buffer, doing a modulo operation for continuing at the start again (like a ring buffer).

Do you think, this would be the easiest method for doing this? This would also limit the volume of the samples, preventing a possible overflow (say for playing up to 3 samples simultaneously the max volume of all samples have to be 1/3 of max bitrate).

I also saw on other i2s implementations that they support the playback of multiple buffers. Is this part of the i2s protocol and thus could be implemented here too, or is this also coded on top?

Thanks again and I would appreciate any answer or suggestion

limchr avatar May 01 '22 10:05 limchr

I've came to a solution and it's working really good so far. Here it is:

@micropython.viper
def set_buffer(buf:ptr8, start_i:int, end_i:int, value:int):
	for i in range(start_i,end_i):
		buf[i] = value

@micropython.viper
def add_to_buffer(buf1:ptr8, buf2:ptr8, s_i1:int, s_i2:int, l:int):
	for i in range(l):
		buf1[s_i1+i] += buf2[s_i2+i]


# loop through ring buffer and mix together simultaneously playing wav files in play buffer
try:
	while True:
		utils.set_buffer(pbmv,0,pbl,0)
		for i,p in enumerate(trigger.is_playing):
			if p:
				add_l = min(pbl, len(wavs[i].data)-trigger.play_i[i])
				if add_l > 0:
					utils.add_to_buffer(pbmv,wavs[i].data,0,trigger.play_i[i],add_l)
					trigger.play_i[i] += add_l
				else:
					trigger.is_playing[i] = False
					trigger.play_i[i] = 0
		_ = audio_out.write(pbmv)
except (KeyboardInterrupt, Exception) as e:
	print("caught exception {} {}".format(type(e).__name__, e))

The triggers that are triggering samples to be played are set by timer interrupts checking for push button presses or control voltages. The buffer functions I have adapted from the forum thread you posted.

However, it is really strange but if I don't play a sample for like 2,3 seconds and after this time a sample is triggered, I hear a loud noise (like a cracking sound) at the beginning of the sample. All follow up samples don't have this noise, only if I wait longer than a few seconds, this noise appears. Could the reason be the i2s implementation? I checked my hardware setup, and I could not find any issues.

limchr avatar May 02 '22 20:05 limchr

Thank you for sharing your implementation! For the noise problem, I don't have an obvious solution, but I wonder if there is something inside the ESP32 I2S implementation that causes this? I submitted a PR in late 2021, trying to fix a noise problem when underflow happens, but it appears that there is still a problem with the ESP32 I2S implementation. A user reported this: https://github.com/micropython/micropython/commit/0be3b91f11e81ff18a0554c31326ac9df20a2091#r61208899

As a workaround, is it possible to continually write a buffer of zero-value samples when no sample clips need to be played?

miketeachman avatar May 07 '22 16:05 miketeachman

Thanks, the noise issue seemed to be a hardware issue which is solved now. Also I am already writing 0's if no sample is in the queue.

It is working nicely now. However, I still have one hack in it: the add_to_buffer function doesn't care for overflows. Right now I solve it by decreasing the volume of samples, which is not optimal.

Since I'm adding 16bit signed integers within two buffers, I think I had to implement it manually. I thought of, adding the low significant byte, check for an overflow, if so add 1 to the higher byte, check for an overflow, if so just set the result to 0xFF.

Do you may have an idea how to implement this efficiently or do you have a better approach than the described one?

limchr avatar May 08 '22 20:05 limchr