bastibe / SoundCard

A Pure-Python Real-Time Audio Library
https://soundcard.readthedocs.io
BSD 3-Clause "New" or "Revised" License
680 stars 69 forks source link

Cannot play multiple sounds concurrently #108

Closed Lawmate closed 4 years ago

Lawmate commented 4 years ago

Hi

Firstly, I'm pretty new to using python, so sorry for any obvious mistakes. I am trying to play 4 files simultaneously from my Mac, each going to a different channel on an audio interface (Motu 828ES). When I run

speakers.play(datafr,sampleratefr,[0],0) speakers.play(datafl,sampleratefl,[1],0) speakers.play(datarr,sampleraterr,[2],0) speakers.play(datarl,sampleraterl,[3],0)

each file is played sequentially. They are played through the correct channel, but not at the same time. I have also tried using multiprocessing with the following code ` def func1(): speakers.play(datafr,sampleratefr,[0],0) def func2(): speakers.play(datafl,sampleratefl,[1],0) def func3(): speakers.play(datarr,sampleraterr,[2],0) def func4(): speakers.play(datarl,sampleraterl,[3],0)

for x in range(1): if name == 'main': p1 = Process(target=func1) p2 = Process(target=func2) p3 = Process(target=func3) p3 = Process(target=func4) p1.start() p2.start() p3.start() p4.start() p1.join() p2.join() p3.join() p4.join()`

but while this plays together, there is around 0.2s delay between the different channels.

I am simply playing the sound files, no recording or other processing, so I'm not too sure where the delay latency is coming from.

Thanks

bastibe commented 4 years ago

Python can only do one thing at a time. The play function in particular only returns when the sound is (almost) finished playing. Thus a multiple calls to play play one after another.

In your case, you can play to all channels simultaneously by passing all four channels to one play call.

Lawmate commented 4 years ago

I see, thanks for the reply.

Lawmate commented 4 years ago

for anyone else in the same position, I did the following: Read 4 files into separate arrays. Each channel is a mono WAV file

dataBMC1, sampleratechBMC1 = sf.read(fileBMCfr)
dataBMC2, sampleratechBMC2 = sf.read(fileBMCfl)
dataBMC3, sampleratechBMC3 = sf.read(fileBMCrr)
dataBMC4, sampleratechBMC4 = sf.read(fileBMCrl)

Then I combined the 4 arrays, rotated the array through 270 degrees, then flipped it vertically: (There's probably a better way to do this, but it worked fine for me)

dataBMCFull = np.flip(np.rot90(np.array([dataBMC1,dataBMC2,dataBMC3,dataBMC4]),3),1)

My files are quite large so I did the following to speed things up by getting rid of used variables

del dataBMC1, dataBMC2, dataBMC3, dataBMC4
gc.collect()

then you can use the play function

with speakers.player(samplerate=sampleratechBMC1, channels=[0,1,2,3] ) as sp:
    sp.play(dataBMCFull)

It takes a while to load up for large files but will play smoothly and without slowing down

bastibe commented 4 years ago

Two tips:

You can probably replace np.flip(np.rot90(data)) with data.T, which is a shorthand for np.transpose(data).

Also, you might want to loop through short blocks of data instead of one big chunk, i.e.

idx = 0
blocklen = 1024 # just an example length
with speakers.player(...) as sp:
    while idx + blocklen < len(dataBMC1):
        data = np.array([dataBMC1[idx:idx+blocklen], dataBMC2[idx:idx+blocklen], 
                         dataBMC3[idx:idx+blocklen], dataBMC4[idx:idx+blocklen]]).T
        sp.play(data)
Chum4k3r commented 4 years ago

A better way is to elaborate the new nSamples x nChannels array before the while loop, which avoids memory allocation during audio enqueuing

idx = 0

# horizontal stack of arrays
data = np.hstack(dataBMC1, dataBMC2, 
                         dataBMC3, dataBMC4)

blocksize = 1024 # just an example length
with speakers.player(...) as sp:
    while idx < len(dataBMC1):
        sp.play(data[idx : idx + blocksize])
        idx += blocksize
bastibe commented 4 years ago

If you want to avoid the allocation, it would be better to preallocate one block, and copy data to the block as needed. Pre-allocating all blocks in advance can become extremely memory-intensive very quickly.