For a theatre show, I wanted to emit a song (on repeat) on FM frequencies, so that an actual radio receipter could play it. It's nice for multiple reasons, one of them being able to decide when the sound is clear or not via analog gears. Yay, parasites!
Turns out I have a Raspberry Pi — bought for 20€ when it came out in 2011 — that's able to use pifmrds, a command line tool that's using the pulse width modulation of the Pi to emit FM radio.
At first, I wanted to send the audio directly from the computer to the raspberry, but I then figured it would be way easier to just have the pi run things itself without having to check.
pi_fm_rds uses the pi GPIO 4 (pin 7) as the transmitter. A 20cm wire on that
pin is a great antenna, and even without it you're able to cover quite good
distances (for us, we needed 50m max).
I installed Debian bullseye on it, because it's known to work with pi_fm_rds.
Basically I downloaded an image and put in the SD card. The, for convenience, I
connected the pi with a network adapter, and made it run an ssh server for me,
but you can do without it.
Basically I had to download a few dependencies and run a make command:
ssh pi
sudo apt-get update
sudo apt-get install -y git build-essential libsndfile1-dev
git clone https://github.com/ChristopheJacquet/PiFmRds.git
cd PiFmRds/src
make
sudo cp pi_fm_rds /usr/local/bin/pi_fm_rds
sudo chmod 755 /usr/local/bin/pi_fm_rdsI decided to use a .wav file to avoid decoding on the pi itself. I
had to make sure it's mono (and not stereo), and increase a bit the output
volume, so that it plays nicely.
Here is how I converted the file from stereo to mono, and nroamlized it, with
ffmpeg:
ffmpeg -i vivaldi.wav \
-af "loudnorm=I=-10:TP=-1.0:LRA=11,alimiter=limit=0.97" \
-ac 1 -ar 44100 -c:a pcm_s16le \
vivaldi-mono-normalized.wavFew things to note:
loudnorm=I=-10sets the target loudnessTP=-1.0+alimiterto avoid clipping-ac 1to convert to mono (otherwise the file plays multiple times slower)-ar 44100 -c:a pcm_s16leto have the proper bitrate.
Lastly, I wanted this to work automatically, and so I created a script that runs on startup.
The config lives in /etc/radio.ini and has just two settings, the file to
play and the frequency to emit on:
file = /home/pi/vivaldi-normalized.wav
frequency = 100.0Then a small script reads that config and feeds the .wav to pi_fm_rds, looping
forever. One subtlety: pi_fm_rds defaults to a 22050 Hz sample rate, so a
44100 Hz file would play at half speed. The script reads the real rate from the
WAV header and passes it along.
Here is /usr/local/bin/play-radio.py:
#!/usr/bin/env python3
import configparser
import os
import sys
import wave
def read_config(path):
"""Read the flat (section-less) radio.ini with configparser."""
parser = configparser.ConfigParser()
with open(path) as f:
# configparser needs a section header; the ini has none, so add one.
parser.read_string("[radio]\n" + f.read())
return parser["radio"]
def main():
config = sys.argv[1] if len(sys.argv) > 1 else "/etc/radio.ini"
if not os.path.isfile(config):
sys.exit(f"Error: config file not found: {config}")
settings = read_config(config)
audio_file = settings.get("file")
freq = settings.get("frequency")
if not audio_file:
sys.exit(f"Error: 'file' not set in {config}")
if not freq:
sys.exit(f"Error: 'frequency' not set in {config}")
if not os.path.isfile(audio_file):
sys.exit(f"Error: audio file not found: {audio_file}")
try:
with wave.open(audio_file, "rb") as w:
rate = w.getframerate()
except (wave.Error, OSError):
print("Warning: could not read WAV sample rate; defaulting to 22050",
file=sys.stderr)
rate = 22050
print(f"Broadcasting '{audio_file}' on {freq} MHz at {rate} Hz (looping)",
flush=True)
env = {**os.environ, "PIFM_SAMPLERATE": str(rate)}
cmd = ["pi_fm_rds", "-freq", freq, "-audio", audio_file]
# pi_fm_rds exits when the file ends; restart it to loop forever.
while True:
os.spawnvpe(os.P_WAIT, cmd[0], cmd, env)
if __name__ == "__main__":
main()Make it executable: sudo chmod +x /usr/local/bin/play-radio.py.
To run it at boot I used a systemd service. It needs to run as root (the GPIO
DMA access requires it), which is the default when no User= is given. This goes
in /etc/systemd/system/radio.service:
[Unit]
Description=FM radio auto-play
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/play-radio.py /etc/radio.ini
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetThen enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable radio.service
sudo systemctl start radio.service