A P2P voice intercom app for Whisplay HAT, designed for real-time voice broadcasting between multiple Whisplay devices.
Core flow:
- Runs as a
whisplay-daemonapp - Discovers online devices through Tailscale
MagicDNS, using thewhisplay-talk-hostname prefix - While one device holds the talk button, microphone audio is compressed and streamed to all online peers over TCP
- Other devices play the audio in real time, highlight the active speaker, and show a receive icon in the status box
- While idle, the screen shows the device list with online state and heartbeat latency
- Header:
Shows the
WhisplayTalktitle plus VPN, Wi-Fi signal, and battery status icons - Status card: Shows the current app state, the local device name, and a talk icon on the right while receiving audio
- Device list:
Keeps showing the peer list even while talking or receiving, with online / offline markers and heartbeat latency such as
kitchen (42ms) - Active speaker highlight: Highlights the device currently speaking in yellow
- Footer:
Shows the current action hint such as
Hold button to talk,Release to stop, orListening...
The project currently uses the following design:
- Discovery:
Polls
tailscale status --jsonfor devices whose hostname starts withwhisplay-talk-, then probes the app TCP port on each device before marking it online and recording heartbeat latency - Transport:
All devices listen on fixed TCP port
24680for audio streams - Audio:
Uses
arecord/aplay, with default 16kHz / 16-bit / mono capture,Opusvoice encoding, a small receive jitter buffer, and one-frame redundant resend - Display:
Uses Pillow to render a 240x280 UI into the framebuffer provided by
whisplay-daemon, including header VPN / Wi-Fi / battery icons and a live peer list - Input:
Uses
whisplay-daemonbutton events for push-to-talk
whisplay-talk/
├── main.py
├── application.py
├── config.py
├── audio/
├── display/
├── hardware/
├── network/
├── install.sh
├── run.sh
├── requirements.txt
└── .env.template
git clone <this-repo>
cd whisplay-talk
bash install.shinstall.sh will:
- Install Python / ALSA utils / curl /
libopus0 - Create a
venv - Install
Pillowandpython-dotenv - Download the
NotoSansSC-Bold.ttffont - Auto-register the app if
whisplay-daemonis detected
Every device must join the same Tailscale tailnet before whisplay-talk can discover peers.
Install Tailscale on Raspberry Pi:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale upAfter sudo tailscale up, open the login URL shown in the terminal and complete the device login in your browser.
You can verify the connection with:
tailscale statusIf Tailscale is not installed, not logged in, or not running, the app will show a matching reminder on screen.
First copy the config file:
cp .env.template .envImportant settings:
WHISPLAY_TALK_DEVICE_PREFIXDefault:whisplay-talk-WHISPLAY_TALK_DEVICE_NAMEIf empty, the system hostname is used. If it does not already have the prefix, the prefix is added automatically.WHISPLAY_TALK_TCP_PORTDefault:24680WHISPLAY_TALK_APP_HEARTBEAT_TIMEOUT_MSDefault3000, timeout for peer online probing and latency measurementWHISPLAY_TALK_APP_HEARTBEAT_FAILS_BEFORE_OFFLINEDefault5, number of consecutive failed heartbeat probes allowed before a peer is marked offlineALSA_INPUT_DEVICERecording device. If unset, the app auto-detectswhisplaysound,wm8960soundcard, or compatible Whisplay cards before falling back todefaultALSA_OUTPUT_DEVICEPlayback device. If unset, the app auto-detectswhisplaysound,wm8960soundcard, or compatible Whisplay cards before falling back todefaultAUDIO_CODECDefaultopus, recommended for current real-time talkbackAUDIO_FRAME_MSDefault40, which lowers packet rate and usually helps continuity on weaker linksAUDIO_REDUNDANCY_FRAMESDefault1, which resends the previous compressed frame to help recover a single lost packetAUDIO_OPUS_BITRATEDefault16000, tuned for mono intercom voice with stronger continuityAUDIO_OPUS_COMPLEXITYDefault6, still light enough for Raspberry Pi while improving encode quality a bitAUDIO_OPUS_PACKET_LOSS_PERCDefault15, hints expected network loss to the Opus encoderAUDIO_OPUS_ENABLE_FECDefault1, enables Opus in-band forward error correctionWHISPLAY_TALK_RECEIVE_PREBUFFER_FRAMESDefault24, roughly one second of audio at the current 40ms Opus frame size
Peer discovery is based on Tailscale MagicDNS hostnames. Devices are only considered talk peers when their hostname starts with whisplay-talk-.
Recommended naming pattern:
whisplay-talk-kitchenwhisplay-talk-room1whisplay-talk-office
The UI strips the whisplay-talk- prefix when showing device names, so whisplay-talk-kitchen is displayed as kitchen.
The recommended way to rename devices is directly in the Tailscale admin console, by editing each device name to match the whisplay-talk-<name> pattern.
For example:
whisplay-talk-kitchenwhisplay-talk-room1
After renaming a device in Tailscale, wait for the updated MagicDNS name to propagate to peers.
WHISPLAY_TALK_DEVICE_NAME can still be used as a local override when needed.
Also make sure all devices have joined the same Tailscale tailnet.
bash run.shIf whisplay-daemon is running on the system, it is recommended to launch Talk from the daemon app list.
For systems that do not use whisplay-daemon, you can configure boot startup with:
bash startup.shstartup.sh installs a systemd service for this app. If it detects whisplay-daemon, it exits without making changes.
- While idle: The screen shows the device list, including self, online / offline markers, and peer heartbeat latency
- If Tailscale is not installed: The screen shows an install reminder
- If Tailscale is installed but not logged in or not running: The screen shows the matching login/start hint
- While holding the button:
The local device enters
Speakingand stops local playback to avoid echo - After releasing the button: Sending stops and an end packet is broadcast
- When remote audio is received:
The device enters
Receiving, plays audio, shows who is speaking, and displays the talk icon on the right side of the status box
The current implementation uses a small custom packet header over a TCP stream:
- magic:
WT01 - type:
1 - flags:
1 = start,2 = end - sender name
- stream id
- sequence
- codec id
- compressed audio payload, typically
Opus - optional redundant payload for the previous frame
This makes it easy to evolve later toward:
- Unicast priority
- Push-to-talk arbitration
- Half-duplex / full-duplex strategy
- Stronger packet loss handling
This is still an MVP, so a few practical limitations remain:
- The transport is still a custom TCP framing layer, not a standard voice/media protocol stack
- There is no explicit channel lock or arbitration yet; overlapping talk attempts are not coordinated
- Peer identity is still derived from the Tailscale hostname prefix, not from a separate nickname or contact system
- The best experience still assumes
whisplay-daemon;startup.shonly helps boot the app on systems without the daemon, it does not recreate the daemon UI/runtime model
This project is licensed under the GPL-3.0 license. See LICENSE.



