Skip to content

Instantly share code, notes, and snippets.

@almet
Last active May 28, 2026 19:52
Show Gist options
  • Select an option

  • Save almet/fcbcb5679bb29102cc352f7ab210ac6f to your computer and use it in GitHub Desktop.

Select an option

Save almet/fcbcb5679bb29102cc352f7ab210ac6f to your computer and use it in GitHub Desktop.
Transmitting FM radio with a Raspberry Pi

Transmitting FM radio with a Raspberry Pi

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).

Flashing the pi

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.

Building pi_fm_rds

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_rds

Preparing the file

I 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.wav

Few things to note:

  • loudnorm=I=-10 sets the target loudness
  • TP=-1.0 + alimiter to avoid clipping
  • -ac 1 to convert to mono (otherwise the file plays multiple times slower)
  • -ar 44100 -c:a pcm_s16le to have the proper bitrate.

Install the auto-play service

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.0

Then 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.target

Then enable and start it:

sudo systemctl daemon-reload
sudo systemctl enable radio.service
sudo systemctl start radio.service
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment