This project will run on a Raspberry Pi and will play the chorus from "In the End" by Linkin Park (mp3 not included) when the song's intro is played on a connected MIDI device. In 2020, I demoed a version of the project on my Twitter that printed to the console, then I later created a full-featured version.
You can watch a demo of the project in the YouTube video below.
First, set up a virtual enviornment:
python3 -m venv .venv
Once the environment is created, activate it:
source .venv/bin/activate
Once activated, install the dependencies. If there are errors during this step, check the resolving dependency conflicts section below.
pip install -r requirements/development.txt -r requirements/production.txt
Next create a project .env
file:
cp .env.example .env
The script may pick a different default midi interface than the one which was intended.
If this script does not fire when the correct notes are played, try the following steps to ensure that the the application is listening to the correct midi interface.
Activate the Python virtual environment if it is not already activated in your current terminal session:
source .venv/bin/activate
Once the virtual environment is activated, begin an interactive Python session:
python
In the interactive Python session, run the following commands:
import mido
mido.get_input_names()
These commands will return a list of audio interfaces such as the one below:
# Example output:
['Midi Through:Midi Through Port-0 14:0', 'Scarlett 18i8 USB:Scarlett 18i8 USB MIDI 1 20:0']
To manually select an audio interface, copy the full name of the audio interface and set the AUDIO_INTERFACE
value to that string in .env
:
# .env file
AUDIO_INTERFACE="Scarlett 18i8 USB:Scarlett 18i8 USB MIDI 1 20:0"
The audio file is not provided as a part of this repository, so you'll need to provide your own. Save the file into the main project directory, then set its value in .env
using the AUDIO_FILE
variable.
To extract stems from an audio file, you can use Spleeter.
Install Git:
sudo apt install git -y
After that, run the following commands to clone the repo into the home directory:
cd ~
git clone https://github.com/tylerlwsmith/pi-in-the-end.git
Once cloned, change into the directory and install the production dependencies. Because this is the only Python application the Pi will be running, we will opt to install the dependencies globally.
cd pi-in-the-end
pip install requirements/production.txt
If you are not using the default audio driver for output, follow Jeff Geerling's instructions to set a different audio device.
To adjust the ouptut volume, grab the name of the device you'd like to change the volume of with the following command:
amixer scontrols
It may return something like Simple mixer control 'PCM',0
. You can target the device name in single quotes to change the volume of the output with the following command (more info on Stack Overflow):
amixer set PCM 100% # 100% means full volume.
Alternatively, you can user alsamixer
and edit the volume through the TUI:
alsamixer
To set the default audio device, you can use raspi-config
:
sudo raspi-config
Next create, a systemd
unit file by running the following command:
sudo vi /etc/systemd/system/linkinpark.service
Paste the following into the new linkinpark.service
file, replace <username>
with the desired user by running :%s/<username>/username/g
, and save and quit with wq!
:
[Unit]
Description=Linkin Park MIDI Playback Service
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/<username>/pi-in-the-end
User=<username>
Restart=always
ExecStart=/home/<username>/pi-in-the-end/in_the_end.py
[Install]
WantedBy=multi-user.target
Once created, run the following commands to activate the service:
sudo systemctl daemon-reload
sudo systemctl enable linkinpark.service
sudo systemctl start linkinpark.service
sudo systemctl status linkinpark.service
This is a proof-of-concept implementation is not intented be put into production, so it has several potential issues that are not addressed:
- There is no way to specify what audio interface plays the audio clip within the code: that happens at the operating system level.
- Linux may change the ID of desired output audio interface.
- If the midi device is disconnected while the app is running, the application has no way of reconnecting when the device is plugged back in.
- The MIDI device name that the
mido
library sees may change between system boots. - If the midi device is disconnected when
systemd
starts the app, the service will go into a rapid boot loop. - Multiple midi channels on the same interface will all be collapsed into a single stream of notes.
- The current implementation only supports listening on one device.
All of these issues are fixable, but are not worth expanding this demo project's scope to mitigate.
After installing the project requirements in a virtual environment, use the following command to run the project's tests:
pytest
This repo uses pip-tools
to manage top-level dependencies and generate the appropriate requirements files. This approach was taken from pip-tools section of Python Application Dependency Management by Hynek Schlawack.
To add a dependency, add it to either development.in
or production.in
in the requirements/
directory, then run the following commands:
pip-compile -o requirements/production.txt requirements/production.in
pip-compile -o requirements/development.txt requirements/development.in
This will add the new dependency, but not upgrade any existing dependencies. To upgrade existing dependencies to their latest versions, run one of the following commands:
# Upgrade all packages.
pip-compile --upgrade -o requirements/production.txt requirements/production.in
# Upgrade single package.
pip-compile --upgrade-package mido -o requirements/production.txt requirements/production.in
Once you have made the desired changes to your requirements files, sync the virtual environment:
pip-sync requirements/production.txt requirements/development.txt
Sometimes, the development
and production
requirements files can't have the same transitive dependency, but require a different version. In this case, you must manually resolve the dependency conflict and ensure that both development
and production
use the same version of the transitive dependency.
This project uses Black for formatting.