Ever wanted to have multiple different sound files playing on different output devices attached to a host computer? Say you’re writing a DJing application where you want one mix for headphones and one for the speakers. Or you’re doing some sort of kiosk or art installation where you have many sets of speakers that need to all be playing their own sound file but the whole thing needs to be synchronized. This would even be cool for something like an escape room.
The ladder example is where I needed this bit of code. I’ve been working with interdisciplinary artist Sara Dittrich on a few projects recently and she asked if I could come up with a way to play 8 different mono sound files on 8 different loudspeakers. Here’s a video of the whole setup in action, and an explanation of the project:
I’ve wrapped up all of the code for the art installation project, and that can be found in a github repo here. It includes the startup functionality etc. If you’re interested in recreating the video above, that repo would be a good starting place. The following is a list of the parts used to make that build happen:
Multi-Audio Example
It is worth it to give a simple example of how to play multiple files on multiple audio devices using python. I couldn’t find an examples on how to do this online and had to spend some time experimenting to make it all come together. Hopefully this saves you the trouble.
To install sounddevice on my Raspberry Pi, I had to run the following commands:
1 2 |
sudo apt-get install python3-pip python3-numpy libportaudio2 libsndfile1 libffi-dev python3 -m pip install sounddevice soundfile |
For this example, let’s say there are 4 audio files in the same directory as multi.py
, so the directory looks like this:
1 2 3 4 5 6 |
multi_audio/ ├── 1.wav ├── 2.wav ├── 3.wav ├── 4.wav └── multi.py |
The code is based on the sounddevice library for python, whose documentation is pretty sparse. This script will find the audio files, and then play them on as many devices as there are attached. For example, if you have 3 sound devices it will play 1.wav
, 2.wav
and 3.wav
on devices 1-3. If you have any questions, feel free to ask:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
""" multi.py, uses the sounddevice library to play multiple audio files to multiple output devices at the same time Written by Devon Bray (dev@esologic.com) """ import sounddevice import soundfile import threading import os DATA_TYPE = "float32" def load_sound_file_into_memory(path): """ Get the in-memory version of a given path to a wav file :param path: wav file to be loaded :return: audio_data, a 2D numpy array """ audio_data, _ = soundfile.read(path, dtype=DATA_TYPE) return audio_data def get_device_number_if_usb_soundcard(index_info): """ Given a device dict, return True if the device is one of our USB sound cards and False if otherwise :param index_info: a device info dict from PyAudio. :return: True if usb sound card, False if otherwise """ index, info = index_info if "USB Audio Device" in info["name"]: return index return False def play_wav_on_index(audio_data, stream_object): """ Play an audio file given as the result of `load_sound_file_into_memory` :param audio_data: A two-dimensional NumPy array :param stream_object: a sounddevice.OutputStream object that will immediately start playing any data written to it. :return: None, returns when the data has all been consumed """ stream_object.write(audio_data) def create_running_output_stream(index): """ Create an sounddevice.OutputStream that writes to the device specified by index that is ready to be written to. You can immediately call `write` on this object with data and it will play on the device. :param index: the device index of the audio device to write to :return: a started sounddevice.OutputStream object ready to be written to """ output = sounddevice.OutputStream( device=index, dtype=DATA_TYPE ) output.start() return output if __name__ == "__main__": def good_filepath(path): """ Macro for returning false if the file is not a non-hidden wav file :param path: path to the file :return: true if a non-hidden wav, false if not a wav or hidden """ return str(path).endswith(".wav") and (not str(path).startswith(".")) cwd = os.getcwd() sound_file_paths = [ os.path.join(cwd, path) for path in sorted(filter(lambda path: good_filepath(path), os.listdir(cwd))) ] print("Discovered the following .wav files:", sound_file_paths) files = [load_sound_file_into_memory(path) for path in sound_file_paths] print("Files loaded into memory, Looking for USB devices.") usb_sound_card_indices = list(filter(lambda x: x is not False, map(get_device_number_if_usb_soundcard, [index_info for index_info in enumerate(sounddevice.query_devices())]))) print("Discovered the following usb sound devices", usb_sound_card_indices) streams = [create_running_output_stream(index) for index in usb_sound_card_indices] running = True if not len(streams) > 0: running = False print("No audio devices found, stopping") if not len(files) > 0: running = False print("No sound files found, stopping") while running: print("Playing files") threads = [threading.Thread(target=play_wav_on_index, args=[file_path, stream]) for file_path, stream in zip(files, streams)] try: for thread in threads: thread.start() for thread, device_index in zip(threads, usb_sound_card_indices): print("Waiting for device", device_index, "to finish") thread.join() except KeyboardInterrupt: running = False print("Stopping stream") for stream in streams: stream.abort(ignore_errors=True) stream.close() print("Streams stopped") print("Bye.") |
Here are some more photos of the build:
Thx! your job he inpire me.
i work in a escape game in france
i made a equivalent system in C++ with 3D sound gestion
you can use 4 sound card too
you can add sound in a config file and with a client in RAW like putty send a json in the 3216 port
launch the sound, make it move with dopler effect etc..
it’s always in WIP but if you want to take look …
https://github.com/hyndruide/Multi-audio ( i need to clean the repo to 🙂 )
Oh amazing! Thanks for sharing. I thought this could be good for escape rooms 🙂
Great work, I need to do something like this, so this resource is saving me some time!
How can I best talk with you in more detail?
email! dev(at)esologic.com
Could you suggest if i have to play these three sound on different device with setting initial interval that each of the sound file should run?
Thank you very much, I was looking for something like this ! Works almost perfectly, however I noticed that it sounds like you have to divide the samplerate by the number of usb devices to get the play duration right… Is that just me ?
No, it depends more on the file. You should be able to inspect a given wav file to determine it’s sample rate, then use that in the code. For this example we had given soundfiles so we could define this as a constant. Try this: https://stackoverflow.com/questions/43490887/check-audios-sample-rate-using-python
Hey Devon!
I’m Aga. I’m an artist currently working on a project that requires a similar set up.
First of all great job! I’m very impressed with how clean your work is.
I have a question about controlling the sound output. In my piece it is crucial that the sound loops play on their own and only overlap sporadically. I would like to crete two different situations that will interchange randomly. Firstly, I would like for one speaker to play while others are silent and sedondly to sometimes play different sounds simultaneously on several speakers.
Each speaker would have an assigned and distinct sound loop (it will be fragments of interviews and each speaker will effectively represent one person). So the sounds would not migrate between the 12 speakers.
I am wondering if using the set up with the usb to jack solution will allow me to asign each speaker its ‘identity’ and programme the interaction between them? Some sort of order of the sounds. Either random or pre-designed – im interested in both.
Thank you for any kind of pointers!
All the best,
Aga
Yeah that would be totally possible. You could get creative with joining the audio playback threads.
This is the existing implementation:
for thread in threads:
thread.start()
for thread, device_index in zip(threads, usb_sound_card_indices):
print("Waiting for device", device_index, "to finish")
thread.join()
If you wanted the files to playback in a sequence, you’d use:
for thread in threads:
thread.start()
thread.join()
That way, you’d never get overlap.
Meh, formatting didn’t really work. Send me an email if things are still confusing, dev@esologic.com
Please add the libffi-dev dependency for CFFI.
Added, thank you Hiram!
Hi Devon,
I work with Raspberrypi 4 Debian and with Python 3.7.3
I downloaded your code from github-esologic.
In Python / Run Module / the code works very well!
With input in the terminal
$ python3 pear.py ./test
and with the “crontab” routine
crontab -e
@reboot screen -dmS pear / bin / bash /home/pi/pear/runpear.sh
does it not work!
Did I forget something?
What should I do?
best martin
Hi! There shouldn’t be any spaces in the
/bin/bash
part of the crontab line.It should be exactly:
@reboot screen -dmS pear /bin/bash /home/pi/pear/runpear.sh
hi Devon,
thank you for the fast answer!
@reboot screen -dmS pear /bin/bash /home/pi/pear/runpear.sh
or
@reboot screen -dmS pear /bin/bash/home/pi/pear/runpear.sh
doesnt work!
I forgot to install “screen”. But it still doesn’t work! Do you have to install anything else?
Hi Devon,
I started the first attempt with
pi@raspberrypi:~ $ python3 pear.py ./test
bash: $: Command not found
pi@raspberrypi:~ $
and then with
pi@raspberrypi:~ $ python3 /home/pi/pear-master/pear.py ./test
Traceback (most recent call last):
File “/home/pi/pear-master/pear.py”, line 88, in
args = parser.parse_args()
File “/usr/lib/python3.7/argparse.py”, line 1758, in parse_args
args, argv = self.parse_known_args(args, namespace)
File “/usr/lib/python3.7/argparse.py”, line 1790, in parse_known_args
namespace, args = self._parse_known_args(args, namespace)
File “/usr/lib/python3.7/argparse.py”, line 1999, in _parse_known_args
stop_index = consume_positionals(start_index)
File “/usr/lib/python3.7/argparse.py”, line 1955, in consume_positionals
take_action(action, args)
File “/usr/lib/python3.7/argparse.py”, line 1848, in take_action
argument_values = self._get_values(action, argument_strings)
File “/usr/lib/python3.7/argparse.py”, line 2378, in _get_values
value = self._get_value(action, arg_string)
File “/usr/lib/python3.7/argparse.py”, line 2411, in _get_value
result = type_func(arg_string)
File “/home/pi/pear-master/pear.py”, line 39, in dir_path
raise NotADirectoryError(path)
NotADirectoryError: ./test
pi@raspberrypi:~ $
something doesn’t seem to be working!
best martin
Alright — so, the problem is with
runpear.sh
. You have to modify it to add the location of your sound files.#!/bin/bash
echo "waiting for usb sound devices to initialize"
python3 /home/pi/pear/wait_devices_init.py
echo "waiting sound devices have initialized, running pear"
python3 /home/pi/pear/pear.py /mnt/usb
Instead of
/mnt/usb
you need to change this to the path of your wav files.Hi Devon
It works!!
Thank you so much!
all the best
martin
🙂
Hi Devon,
Another question: have you had the experience that when using more than one Sound Card a high humming sound occurs?
if I run the sound installation with one Sound Card, this phenomenon does not occur!
What can be the reason?
best martin
Already tried to wrap aluminum foil around but the sound was still there.
It is not due to the sound cards but to the amplifiers. I use one power supply for the Pi and one for the amplifiers. Apparently there is a conflict.
Hi Devon
Because my SD card crashed, I set up a new system. Your program (multi.py) works perfectly in Python.
But if I start the autostart program (pear.py) in Python, the error message appears:
usage: pear.py [-h] dir
pear.py: error: the following arguments are required: dir
Also crontab no longer works:
@reboot screen -dmS pear /bin/bash /home/pi/pear/runpear.sh
I changed runpear.sh:
#! / bin / bash
echo “waiting for usb sound devices to initialize”
python3 /home/pi/pear/wait_devices_init.py
echo “waiting sound devices have initialized, running pear”
python3 /home/pi/pear/pear.py /home/pi/pear/test
Can you help me?
best martin
Hi Devon,
After a system crash the Autostart Crontab (Autostart) no longer works
@reboot screen -dmS pear / bin / bash /home/pi/pear/runpear.sh
runpear.sh
#!/bin/bash
echo “waiting for usb sound devices to initialize”
python3 /home/pi/pear/wait_devices_init.py
echo “waiting sound devices have initialized, running pear”
python3 /home/pi/pear/pear.py ./home/pi/pear/test
what can be there?
martin
Hi Devon, .bashrc was the solution! All works fine! best martin
Great! Glad you got it worked out. If I were to re-do the startup functionality I’d make this a systemd service. Something to consider if you run into more issues.
Hi Devon,
I am setting up your system on my model railroad for various sounds. It is working well so far. A couple of questions:
– I see a signal out of one 3.5mm jack on each of 4 plugable modules. Should there be any signal on the other channel or is that only if the wav file has stereo content?
– can multiple wav files be sent to one channel? ala pygame which overlays the sounds all on one channel
Cool design,
Allen
Hiya Allen what a cool usecase!
1. The green 3.5mm jack is for audio output, the red/pink one is for audio input, which isn’t used in this project.
2. I haven’t tried playing multiple audio files on top of eachother out of the same device before. When audio data is loaded into memory by
load_sound_file_into_memory
, the format is a 2D numpy array. I’d bet if you added two arrays together (maybe multiply?) before playing them you might be able to get what you want. Let me know if you get anything working with this.Got it Devon. Thanks for the info. I am studying the sounddevice library as well as numpy and will try some tests.
Regards,
Allen
Hi Devon,
Merry Christmas to you.
I would like to say that your project save a lot of my time. Really appreciate with what you did.
So far on my side, everything goes well and I am very happy with the results. However, I got a small issue.
I found out that it is only able to handle up to 8 audio cards. Is there a way to make it to 10 audio cards?
Regards,
Johnny
Very interesting. Cool to see someone that has tried out more cards than me. What happens when you plug in 10 cards? Like what doesn’t work?
Thanks for the reply. I had actually sent you an email few days ago. Maybe is easier for us to discuss over there.
What an amazing setup. I have to a ask though. Could this not of been done through a cheap mobile phone or even Bluetooth 5? Look forward to talking.
We needed simultaneous output, so having multiple bt devices paired could work for sequential output (in theory, I haven’t tried this) but not for parallel output given my understanding of the underlaying technology.
Interesting that bt doesn’t allow simultaneous tracks. I know for example on some of the latest phones you can plug more then one Bluetooth device, so two people can listen to the same track. I guess when you mean output that’s what’s the sound cards are their to do, and perhaps limitation is on soundcard and not Bluetooth. I’m gonna look into this and will report back.
* sequential I mean
Yep let me know what you find.
Hi Devon, love your work! Is there a way to do this but with stereo output? Have you even seen a setup with stereo? I’d like to be able to play multiple tracks simultaneously through different headphones with high quality stereo sound. It would need to run continuously in a gallery for a long period of time, similar to this work.
You should be able to use this as is with stereo wav files, I only use one file per channel here because the actual recordings didn’t need both channels. You could send an email if you have more questions.
Hi Devon,
What if I have hardware buttons connected to Raspberry pi. I need to set up a museum installation.
If ‘button_1’ is pressed, play ‘1.wav’ on ‘audiocard_1’; if ‘button_1’ is pressed again Pause.
If ‘button_2’ is pressed, play ‘2.wav’ on ‘audiocard_2’; if ‘button_2’ is pressed again Pause.
etc…
This is a non-commercial project.
Hey send me an email!
It appears that not all USB hubs will work for this application. I’ve built something like this project and have found that some hubs may distort multi voice playback. I’m no USB engineer, but started a thread asking about possible issues over on revolt.com https://www.eevblog.com/forum/projects/differences-between-usb-controller-chips/new/#new
Can the two channels on these amplifiers be Bridged?
Hi Devon,
Thanks firstly for sharing such a great project. I am attempting to recreated it for use in a multidisciplinary workshop. I am a complete novice to using a Pi and to programming however i’ve been steadily working through your instructions and some extra youtube tutorials. Alas, i am still not getting it to work and so I seek your assitance.
I’ve run the script in Thonny to see why it’s not working and it’s come up with this:
“usage: pear.py [-h] dir
pear.py: error: the following arguments are required: dir”
I am assuming somethings not right with the directories, but i have no idea how to resolve that..
Also I don’t quite understand how the multi.py comes into it either..
Thanks in advance,
Liam
I think this is because Thonny isn’t providing the command line arguments. Try running the script directly from the command line.
You could copy the code in
__main__
to it’s own function in the file, and then import/use it that way.First I want to thank the author again for sharing this… also wonder if you guys could make it work with a raspberry pi 4b, can not succesfully add more than 2 usb cards, may it be a hub problem? I can edit code to enable the earphones as an extra output, but yet no more than 2 USB cards.
Hey! Nothing about this setup is specific to any type of raspberry pi, so it should work on 4b.
Hi Devon,
Thanks for sharing such a wonderful resource. I am new to this and I have been trying to make this work.
I am getting the response
“No usb devices found”
A comment on the YouTube video of this project mentions a similar error
Can you recommend something about this error…
Thanks in advance
K