This repository provides two independent installer scripts that configure a Raspberry Pi to rebroadcast audio as an FM radio transmission using PiFmRds on GPIO 4.
| Script | Audio source | Discovery |
|---|---|---|
a2dp2fm.sh |
Bluetooth A2DP (phone/tablet) | Bluetooth pairing |
airplay2fm.sh |
AirPlay / RAOP (iPhone, Mac, iPad) | Wi-Fi / Bonjour |
Both scripts share the same FM transmitter hardware (GPIO 4 antenna, PiFmRds), LED feedback system, and RDS support. They cannot run simultaneously — the FM transmitter is exclusive. Use uninstall.sh to switch between them or remove either one cleanly.
raspberry-pi · bluetooth · a2dp · airplay · raop · shairport-sync · fm-transmitter · rds · pi-fm-rds · headless-install · systemd · tts
- Fully automated provisioning – Installs all required packages, builds PiFmRds, and configures systemd units in one pass.
- RDS support – Mirrors track metadata (Artist, Title, Album) into the RDS PS/RT fields so compatible radios display what's playing.
- LED status feedback – Reconfigures the Raspberry Pi ACT LED to provide visual cues for the current state.
- Offline-friendly boot – Disables the "wait for network" delay so the Pi completes startup even without network connectivity.
- Configurable frequency – Set FM frequency, step size, and min/max range at install time; change them later by editing
/etc/default/bt2fmor/etc/default/airplay2fm.
- Headless Bluetooth pairing – Keeps the adapter powered, discoverable, and pairable after every boot; no screen or keyboard required.
- A2DP audio pipeline – Captures audio via BlueALSA and feeds it into PiFmRds.
- Volume-key frequency control – Monitors Bluetooth Absolute Volume so the phone's volume buttons shift the FM frequency while playback is paused.
- TTS station announcements – After each frequency change, announces "moving to X" on the old frequency (so listeners know where to retune), then confirms on the new one, using
flite. - AVRCP metadata to RDS – Pushes track info from the connected phone into RDS PS/RT fields.
- Zero-config AirPlay discovery – Advertises itself on the local network via Avahi/Bonjour; appears instantly in iOS Control Center and macOS audio output.
- AirPlay audio pipeline – Uses
shairport-syncwith its pipe backend;soxwraps the raw PCM in a WAV container and feeds it into PiFmRds. - Metadata to RDS – Reads shairport-sync's metadata pipe to populate RDS PS/RT fields with the playing track.
- Web tuner UI – A built-in HTTP server on port 8750 serves a mobile-friendly page showing the current frequency, now-playing track, and play state, with Up/Down step buttons and a direct-entry frequency field. Changes persist across restarts.
- Volume-key frequency control (opt-in,
--vol-tune) – Press the sender's volume buttons while playback is paused to shift the FM frequency. Rapid presses are batched: ~3 s after the last press, the net change applies in one move (3 up-clicks = +0.6 MHz at the default 0.2 step), announced on the old frequency and confirmed on the new one. Sender volume is restored via the DACP back-channel when playback resumes. - FM carrier on demand – The transmitter runs only while audio is playing; carrier is off when idle.
- Raspberry Pi with 40-pin header (tested with Pi 3 / 4 / Zero 2 W).
- ⛔ Pi 5 and Pi 500 are NOT supported. PiFmRds generates FM through the SoC clock generator on GPIO4; on the Pi 5/500, GPIO is routed through the RP1 I/O chip and this method cannot work. The installers detect these boards and refuse to run (
A2DP2FM_FORCE_INSTALL=1overrides, but the transmitter will not function). - Bluetooth path: Bluetooth adapter (onboard or USB) supported by BlueZ.
- AirPlay path: Wi-Fi connection on the same network as the sending device.
- Short piece of wire (~10–20 cm) connected to GPIO 4 (pin 7) as the FM antenna.
- Nearby FM radio to receive the transmission.
⚠️ Regulatory notice: Broadcasting FM radio may be regulated in your region. Use low power, short antennas, and comply with local laws.
The 40-pin GPIO header layout is identical across all supported models: Pi Zero, Pi Zero W, Pi Zero 2 W, Pi 2B, Pi 3A+/3B/3B+, Pi 4B, Pi 5, and Pi 400. The only physical difference is that Pi Zero (original and W) ships with unpopulated header holes — you need to solder a 2×20 pin header before use.
The original Pi 1 Model A and B used a 26-pin header (a subset of this layout). Those boards are not supported.
The standard two-column layout, with pin numbers in the centre:
┌────── USB / Ethernet end ──────┐
│ │
3V3 (1) (2) 5V
GPIO2 (3) (4) 5V
GPIO3 (5) (6) GND
GPIO4 (7) (8) GPIO14 ← (7) FM ANTENNA — attach wire here
GND (9) (10) GPIO15
GPIO17 (11) (12) GPIO18
GPIO27 (13) (14) GND
GPIO22 (15) (16) GPIO23
3V3 (17) (18) GPIO24
GPIO10 (19) (20) GND
GPIO9 (21) (22) GPIO25
GPIO11 (23) (24) GPIO8
GND (25) (26) GPIO7
GPIO0 (27) (28) GPIO1
GPIO5 (29) (30) GND
GPIO6 (31) (32) GPIO12
GPIO13 (33) (34) GND
GPIO19 (35) (36) GPIO16
GPIO26 (37) (38) GPIO20
GND (39) (40) GPIO21
│ │
└──────── SD card end ───────────┘
Pin 1 (3V3) is the corner nearest the SD card slot on most models. On Pi Zero boards the SD card is directly adjacent to pin 1 at the same end of the header.
Pins used by this project:
| Physical pin | BCM GPIO | Role |
|---|---|---|
| 7 | GPIO4 | FM transmit output — connect antenna wire |
| 6, 9, 14, 20, 25 … | GND | Ground (any GND pin works) |
The ACT LED is an on-board LED controlled via /sys/class/leds/led0; it is not a header pin.
Connect a 10–20 cm insulated wire to physical pin 7 (GPIO4). Ground is provided through the Pi's internal circuits; a separate ground wire is not required for basic operation, but connecting one to any GND pin can improve signal quality.
Wire type and length:
-
Type: any insulated solid-core hookup wire (20–24 AWG / ~0.5 mm), or — easiest — a female-ended jumper (Dupont) wire pushed straight onto pin 7. No soldering required.
-
Length: pick for the coverage you need — longer radiates further, so stay short where regulations are strict:
Length Typical usable range 10–20 cm (4–8") same room / adjacent room — polite default ~30 cm (12") most of a typical house ~75 cm (29.5") quarter-wave whole house + yard — only where legal The quarter-wave length is frequency-dependent (λ/4 ≈ 69–85 cm across 87.7–107.9 MHz; ~85 cm at 87.9). House construction matters more than exact length — wood/drywall barely attenuates FM, while brick, plaster on metal lath, or foil-backed insulation can eat it. Elevation is free range: place the Pi high and central before reaching for a longer wire.
-
Placement: run the wire vertically and away from the board; make sure it touches no other pin. Bare wire ends should not contact the Pi's case or other metal.
On the Pi 400/500 the GPIO header is on the rear and mirrored compared to a regular Pi: viewed from behind the keyboard, pin 1 is in the top row at the right end (nearest the SD slot), so pin 7 is the 4th pin from the right in the top row. The installer's board diagram shows this orientation.
💡 When either installer finishes, it auto-detects your Pi model (Zero family, full-size boards, Pi 400/500, or a generic fallback) and prints a terminal diagram of that board with the antenna pin — GPIO4, physical pin 7 — highlighted, along with antenna wire guidance. Run with
--dry-runto see the diagram without installing anything.
- Attach a 10–20 cm wire to GPIO 4 (pin 7).
- Boot the Pi headless with network access.
- Run the installer:
sudo bash a2dp2fm.sh --freq 87.9 - Pair your phone with the Pi (name:
raspberrypi), enable Media Audio. - Tune a radio to 87.9 MHz and start playing audio.
- Attach a 10–20 cm wire to GPIO 4 (pin 7).
- Boot the Pi on the same Wi-Fi network as your iPhone or Mac.
- Run the installer:
sudo bash airplay2fm.sh --freq 87.9 --name "My Pi Radio" - Open iOS Control Center → AirPlay → select My Pi Radio.
- Tune a radio to 87.9 MHz and start playing audio.
The installers are standalone single files — everything they deploy is embedded, so you can fetch one straight from GitHub instead of cloning:
# Bluetooth pathway
curl -fsSLO https://raw.githubusercontent.com/identd113/A2DP2FM/main/a2dp2fm.sh
sudo bash a2dp2fm.sh --freq 87.9
# AirPlay pathway
curl -fsSLO https://raw.githubusercontent.com/identd113/A2DP2FM/main/airplay2fm.sh
sudo bash airplay2fm.sh --freq 87.9 --name "My Pi Radio"
# Uninstaller (download it — the interactive menu needs a real stdin)
curl -fsSLO https://raw.githubusercontent.com/identd113/A2DP2FM/main/uninstall.sh
sudo bash uninstall.shNotes:
curl -Ooverwrites an existing file of the same name in the current directory; use-o othername.shto avoid that.- The Pi still needs network access during install (apt + GitHub clones).
- Re-installing resets the frequency config: the installer rewrites
/etc/default/bt2fm/airplay2fmfrom its command-line flags every run. If you've changed the frequency since installing, pass it explicitly (check first withgrep FREQ /etc/default/bt2fm).
Both paths use the same antenna and GPIO 4. Do not run both installers and then start both services — they will fight over the FM transmitter.
Both scripts target Raspberry Pi OS (Debian-based). They require:
sudoaccess (the scripts exit if not run as root).- Network connectivity during install for
apt-getand GitHub clones. - Python 3 (installed automatically).
All required packages are installed automatically.
Testing in a container (offline/CI):
The easiest way is the Docker harness, which builds an ARMv7 Raspberry Pi-like image and runs the full install test inside it (works on Apple Silicon and amd64 hosts):
./tests/run-in-docker.shOr run the installer manually against the stubbed system utilities:
A2DP2FM_STUB_LOG_DIR=$(mktemp -d) \
PATH="$(pwd)/tests/bin:$PATH" \
A2DP2FM_GIT_CLONE_CMD="$(pwd)/tests/bin/git-clone-stub" \
SUDO_USER=pi sudo ./a2dp2fm.sh --freq 99.1Both installers detect the Raspberry Pi OS/Debian codename and are validated on Trixie, Bookworm, and Bullseye. Both /boot/config.txt and /boot/firmware/config.txt are updated, covering all recent boot layouts.
sudo bash a2dp2fm.sh [--freq 87.9] [--step 0.2] [--min 87.7] [--max 107.9]| Flag | Default | Description |
|---|---|---|
--freq |
87.9 |
FM frequency (MHz) at boot |
--step |
0.2 |
Step size (MHz) for volume-key frequency changes |
--min |
87.7 |
Lower frequency bound (MHz) |
--max |
107.9 |
Upper frequency bound (MHz) |
--dry-run |
— | Preview what would be installed |
--verbose |
— | Extra logging |
What the installer does:
- Installs Bluetooth, audio, and PiFmRds build dependencies. Prefers a packaged BlueALSA (
bluealsa/bluez-alsa) and falls back to building Arkq/bluez-alsa from source. - Clones and builds PiFmRds into
$HOME/PiFmRds. - Writes runtime defaults to
/etc/default/bt2fm. - Deploys helper scripts under
/usr/local/bin/and/usr/local/sbin/. - Creates and enables systemd units for all services.
- Configures the ACT LED for software control.
sudo bash airplay2fm.sh [--freq 87.9] [--name "Pi FM Radio"] [--step 0.2] [--min 87.7] [--max 107.9] [--vol-tune]| Flag | Default | Description |
|---|---|---|
--freq |
87.9 |
FM frequency (MHz) |
--name |
Pi FM Radio |
AirPlay device name shown in iOS/macOS |
--step |
0.2 |
Step size (MHz) for Up/Down tuning (web UI and vol-key) |
--min / --max |
87.7 / 107.9 |
Frequency bounds |
--vol-tune |
off | Enable volume-rocker frequency control (disabled by default) |
--dry-run |
— | Preview what would be installed |
--verbose |
— | Extra logging |
What the installer does:
- Installs Avahi, OpenSSL, ALSA, sox, and PiFmRds build dependencies. Uses the apt
shairport-syncpackage when available; builds from source (mikebrady/shairport-sync) if the package lacks pipe-backend support. - Writes
/etc/shairport-sync.confconfiguring the pipe audio backend, session control, and metadata pipe. - Registers
/etc/tmpfiles.d/airplay2fm.confso systemd recreates the FIFOs at every boot. - Clones and builds PiFmRds (skipped if already present from a Bluetooth install).
- Writes runtime defaults to
/etc/default/airplay2fm. - Deploys helper scripts under
/usr/local/bin/. - Creates and enables systemd units for all services.
- Pair your phone with the Pi (default hostname
raspberrypi). Ensure Media Audio (A2DP) is enabled. - Tune a nearby FM radio to the configured frequency.
- Start playing audio on the phone.
- Press volume down or up while playback is paused to shift the FM frequency. Rapid presses are batched — ~3 s after the last press, the net change applies in one move (3 ups = +0.6 MHz at the default step). During playback, volume buttons work normally.
- Each frequency change flashes the LED three times, announces the move on the old frequency, then confirms on the new one.
- RDS displays Artist / Title / Album on compatible radios.
- Ensure your iPhone, iPad, or Mac is on the same Wi-Fi network as the Pi.
- Open Control Center → AirPlay and select your Pi's name.
- Tune a nearby FM radio to the configured frequency.
- Start playing audio — the FM carrier comes on automatically.
- Pause or stop playback to silence the transmitter; RDS resets to the device name.
- To change frequency: open
http://<pi-hostname>:8750/in any browser on the same network. The page shows the current frequency, now-playing info, and play state. Tap Down or Up to step by 0.2 MHz, or type a frequency directly and tap Set. Changes take effect immediately (the pipeline restarts with the new frequency) and persist through reboots. - (Optional,
--vol-tuneonly) To change frequency with the volume rocker: pause, then press volume up/down right away (iOS only routes the rocker to AirPlay for a few seconds after pausing). Presses are batched — ~3 s after the last press, the net change applies in one move (3 ups = +0.6 MHz), with an LED flash and TTS announcement. Sender volume is restored automatically on resume.
| State | Bluetooth | AirPlay |
|---|---|---|
| Slow blink | Discoverable / pairing mode | shairport-sync running, waiting for stream |
| Double blink ~2 s | Device connected, not streaming | — |
| Solid on | Streaming active | Streaming active |
| Three quick flashes | Frequency changed | Frequency changed |
| Service | Role |
|---|---|
bt-setup.service |
Powers on adapter, sets discoverable/pairable at boot |
bt-agent.service |
Headless Bluetooth pairing agent |
bt2fm.service |
A2DP audio → PiFmRds pipeline |
bt-volume-freqd.service |
Volume-key frequency controller |
avrcp-rds.service |
AVRCP metadata → RDS fields |
led-statusd.service |
ACT LED status for Bluetooth state |
bluealsa.service |
BlueALSA daemon (A2DP capture) |
| Service | Role |
|---|---|
shairport-sync.service |
AirPlay receiver; writes raw PCM to audio FIFO |
airplay2fm.service |
Reads audio FIFO → PiFmRds pipeline |
airplay-rds.service |
shairport-sync metadata → RDS fields |
led-airplay-statusd.service |
ACT LED status for AirPlay state |
Use systemctl status <service> or sudo journalctl -u <service> to inspect any service.
A dedicated script detects what is installed and offers interactive removal:
sudo bash uninstall.shIt scans for both installations, displays their current state, then asks what to remove:
1) Uninstall Bluetooth A2DP -> FM (a2dp2fm)
2) Uninstall AirPlay -> FM (airplay2fm)
3) Uninstall both
q) Quit
Non-interactive flags:
sudo bash uninstall.sh --bt # Bluetooth only
sudo bash uninstall.sh --airplay # AirPlay only
sudo bash uninstall.sh --all # both
sudo bash uninstall.sh --all --yes # both, skip confirmationThe script handles shared resources (PiFmRds, ledctl.sh, ACT LED dtparam config) intelligently — they are removed only when the last remaining install is being uninstalled.
FREQ=87.9
STEP=0.2
FMIN=87.7
FMAX=107.9After editing, restart affected services:
sudo systemctl restart bt2fm.service bt-volume-freqd.serviceFREQ=87.9
STEP=0.2
FMIN=87.7
FMAX=107.9
AP_NAME=Pi FM RadioTo change the FM frequency:
sudo sed -i 's/^FREQ=.*/FREQ=88.5/' /etc/default/airplay2fm
sudo systemctl restart airplay2fm.serviceTo rename the AirPlay device (also requires updating /etc/shairport-sync.conf):
sudo sed -i 's/^AP_NAME=.*/AP_NAME=New Name/' /etc/default/airplay2fm
sudo sed -i 's/name = .*/name = "New Name";/' /etc/shairport-sync.conf
sudo systemctl restart shairport-sync.service- No FM audio – Verify the antenna wire is on GPIO 4 (pin 7) and that PiFmRds is running (
pgrep pi_fm_rds). Checksudo journalctl -u <pipeline service>for errors. - LED does not respond – Reboot once after installation to apply the ACT LED dtparam change. Test manually:
sudo /usr/local/bin/ledctl.sh on.
- Phone cannot connect – Check
bt-setup.serviceis active and the Pi is discoverable (bluetoothctl show). Remove stale pairings:bluetoothctl remove <MAC>then re-pair. - Frequency changes do not trigger – Confirm your phone supports Bluetooth Absolute Volume. Monitor:
sudo journalctl -u bt-volume-freqd.service. - RDS text missing – Verify your radio supports RDS. Check
/run/rds_ctlactivity andsudo journalctl -u avrcp-rds.service. - Audio stutter – Edit
/usr/local/bin/bt2fm.shand lowerarecordto-r 32000. - BlueALSA diagnostics – Run
bluealsactl pcm-listto inspect PCM devices. The daemon binary isbluealsadon newer installs.
- Device does not appear in AirPlay list – Confirm
shairport-sync.serviceis active and that Avahi is running (systemctl status avahi-daemon). Both the Pi and the sending device must be on the same Wi-Fi network. - Audio connects but no FM output – Confirm
airplay2fm.serviceis active. The audio FIFO/run/airplay_audiomust exist; check withls -la /run/airplay_audio. If missing,sudo systemctl restart airplay2fm.servicerecreates it. The pipeline iscat FIFO | sox | pi_fm_rds, so also verifysoxis installed (command -v sox). - RDS not updating with track info – Check
sudo journalctl -u airplay-rds.service. Confirm the metadata FIFO/run/airplay_metadataexists. shairport-sync must be configured withmetadata.enabled = "yes"in/etc/shairport-sync.conf. - shairport-sync fails to start – Run
shairport-sync -vfor a config parse error. Ensure/etc/shairport-sync.confis valid and the pipe paths exist. - FIFOs missing after reboot – Run
sudo systemd-tmpfiles --create /etc/tmpfiles.d/airplay2fm.confto recreate them immediately. If the file is missing, re-runsudo bash airplay2fm.sh(it is idempotent).
If the uninstall.sh script is unavailable, stop and remove services manually.
Bluetooth:
sudo systemctl disable --now bt2fm.service bt-volume-freqd.service \
avrcp-rds.service led-statusd.service bt-setup.service bt-agent.service
sudo rm -f /usr/local/bin/{bt2fm.sh,fm_announce.sh,bt-volume-freqd.sh,avrcp_rds.py,led-statusd.sh}
sudo rm -f /usr/local/sbin/{bt-agent-wrapper.sh,bt-setup-bluetooth.sh}
sudo rm -f /etc/default/bt2fm /run/rds_ctl
sudo rm -f /etc/systemd/system/{bt2fm,bt-volume-freqd,avrcp-rds,led-statusd,bt-agent,bt-setup,bluealsa}.service
sudo systemctl daemon-reloadAirPlay:
sudo systemctl disable --now airplay2fm.service airplay-rds.service \
led-airplay-statusd.service shairport-sync.service
sudo rm -f /usr/local/bin/{airplay2fm.sh,airplay-rds.py,led-airplay-statusd.sh}
sudo rm -f /etc/default/airplay2fm /etc/tmpfiles.d/airplay2fm.conf
sudo rm -f /run/airplay_audio /run/airplay_metadata
sudo sed -i '/^snd-aloop$/d' /etc/modules
sudo rm -f /etc/systemd/system/{airplay2fm,airplay-rds,led-airplay-statusd}.service
sudo systemctl daemon-reloadShared (after removing both):
sudo rm -f /usr/local/bin/ledctl.sh /run/rds_ctl
sudo rm -rf ~/PiFmRds
# Remove ACT LED overrides and reboot
sudo sed -i '/dtparam=act_led_trigger=none/d;/dtparam=act_led_activelow=off/d' \
/boot/firmware/config.txt /boot/config.txt 2>/dev/null || true
sudo rebootThis repository inherits the license of the installer scripts. Review each script header and upstream projects (PiFmRds, shairport-sync, bluez-alsa) for their respective licenses.