miketeachman/micropython-i2s-examples

Possible to play multiple .wav's simultaneously?

Theagainmen opened this issue · 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.

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:

I hope this will help you to get started !

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

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.

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:
micropython/micropython@0be3b91#r61208899

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

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?