An iOS & Android app that receives OSC commands (or plain text via UDP/TCP) over Wi-Fi to simulate realistic incoming phone calls and SMS messages on stage. Designed for theater productions where an actor's phone needs to ring or receive messages on cue.
A show control system (like QLab) sends an OSC command over the local network → the iPhone receives it and triggers a native phone call or SMS notification — indistinguishable from the real thing.
QLab / OSC Sender ──UDP──▶ iPhone / Android (TheaterPhone App)
├── /call → Native incoming call
├── /sms → Native notification + chat view
├── /hangup → End call remotely
├── /vibrate → Vibration pulse or pattern
├── /audio → Play imported sound file
└── /ping → Responds with /pong (alive check)
- Native incoming calls — CallKit on iOS, full-screen notification on Android — works on lock screen
- SMS notifications — native notifications on both platforms, tap to open iMessage-style chat
- Audio playback — import sound files and trigger them by name
- Two communication modes — OSC (binary, UDP) or Plain Text (UDP + TCP) — switchable in Settings
- Background mode — listener stays active when the phone is locked
- Ping/Pong — QLab can check if the app is running before sending commands
- Vibration control — trigger vibration remotely
- Cross-platform — identical command set for iOS and Android, use the same QLab cues for both
| Command | Arguments | Description |
|---|---|---|
/call |
<Name> [Number] |
Trigger incoming call |
/hangup |
— | End current call remotely |
/sms |
<Sender> <Text> |
Send SMS notification |
/vibrate |
[single|pattern|stop] |
Vibration only (default: single) |
/audio |
<Name> | stop |
Play imported sound file / stop playback |
/ping |
— | App responds with /pong on port 9001 |
Switch to Plain Text mode in Settings for simpler integrations that don't support OSC binary protocol.
| Command | Arguments | Description |
|---|---|---|
call |
<Name> [Number] |
Trigger incoming call |
hangup |
— | End current call remotely |
sms |
<Sender> <Text> |
Send SMS notification |
vibrate |
[single|pattern|stop] |
Vibration only (default: single) |
audio |
<Name> | stop |
Play imported sound file / stop playback |
ping |
— | App responds with pong ready on port 9001 |
Use quotes for arguments with spaces: call "Mom" "+1 555 1234567"
# OSC mode
/call "Mom" "+1 555 1234567" → Incoming call from Mom
/sms "Max" "Where are you?" → SMS notification from Max
/audio doorbell → Plays the sound named "doorbell"
/audio stop → Stops current audio playback
/hangup → Ends the active call
# Plain Text mode (same commands without /)
call "Mom" "+1 555 1234567"
sms "Max" "Where are you?"
audio doorbell
hangup
- iOS: iPhone with iOS 17+, Mac with Xcode 15+
- Android: Phone with Android 8.0+ (API 26), Android Studio
- Both devices on the same Wi-Fi network
- OSC-capable show control software (QLab, ETC Eos, etc.), a plain text sender, or the included Python test script
- Clone this repository
- Open
TheaterPhone/TheaterPhone.xcodeprojin Xcode - Select your iPhone as the build target
- Build and run (⌘R)
- Allow notifications when prompted
- Note the IP address shown on the lock screen
Tip: Set your preferred ringtone and message tone in iPhone Settings → Sounds & Haptics before the show.
- Clone this repository
- Open the
TheaterPhoneAndroid/folder in Android Studio - Wait for Gradle sync to complete
- Connect your Android phone via USB (enable USB debugging)
- Build and run (
▶️ ) - Allow notification permissions when prompted
- Note the IP address shown on the lock screen
Tip: Disable battery optimization for TheaterPhone in Android Settings → Apps → TheaterPhone → Battery → Unrestricted. This prevents the OS from killing the background listener during a show.
Import sound files (mp3, wav, aiff, m4a) to play them on cue — great for doorbell rings, ambient sounds, or custom effects.
- Open Settings (gear icon) → Audio Library
- Tap Add Sound → select a file from your iPhone
- Give it a name (e.g.
doorbell) - Trigger it via OSC:
/audio doorbellor Plain Text:audio doorbell - Stop playback:
/audio stop
Sound names are case-insensitive. Files are stored within the app and persist across restarts.
Use the included Python script to send commands from any computer:
# Interactive mode (supports OSC, Plain Text UDP, Plain Text TCP)
python3 osc_send.py
# Direct OSC commands
python3 osc_send.py call "Mom" "+1 555 1234567"
python3 osc_send.py sms "Max" "Break a leg!"
python3 osc_send.py hangup
python3 osc_send.py audio doorbell
python3 osc_send.py ping
# Plain Text mode (UDP)
python3 osc_send.py --plain call "Mom"
python3 osc_send.py --plain audio doorbell
# Plain Text mode (TCP)
python3 osc_send.py --plain --tcp sms "Max" "Hello!"Edit TARGET_IP in osc_send.py to match your iPhone's IP address (shown in the app). In interactive mode, use ip <address> to change it on the fly, and o/p/t to switch between OSC, Plain Text UDP, and Plain Text TCP.
- Copy
TheaterPhone.qlabnetworkto/Applications/QLab.app/Contents/Resources/NetworkDeviceDescriptions - In QLab: create a Network Cue → select "TheaterPhone" as the device
- Choose the action (Call, Hang Up, SMS, Vibrate, Ping) and fill in the parameters
The included AppleScript cues check if the app is running before sending commands. If the app doesn't respond, a fallback cue is triggered instead (e.g., a sound effect from speakers).
Setup:
- Create your phone cues with cue numbers:
phone_call,phone_sms,phone_hangup - Create fallback cues:
fallback_call,fallback_sms,fallback_hangup - Create a Script Cue and paste the content of the matching script from
QLab Script Cues/:smart_call.applescript— for call cuessmart_sms.applescript— for SMS cuessmart_hangup.applescript— for hangup cues
- Set
phoneIPin the script to your iPhone's IP address
Each script sends /ping, waits 2 seconds for /pong, then triggers either phone_<action> or fallback_<action>.
├── TheaterPhone/ # iOS app (Xcode / Swift / SwiftUI)
│ └── TheaterPhone/
│ ├── TheaterPhoneApp.swift
│ ├── Models/
│ │ ├── CallState.swift
│ │ └── SMSState.swift
│ ├── Services/
│ │ ├── OSCManager.swift # OSC + Plain Text listener
│ │ ├── SoundLibraryManager.swift
│ │ ├── CallKitService.swift
│ │ ├── NotificationService.swift
│ │ ├── AudioService.swift
│ │ └── BackgroundService.swift
│ └── Views/
│ ├── ContentView.swift
│ ├── LockScreenView.swift
│ ├── ActiveCallView.swift
│ ├── CallEndedView.swift
│ ├── SMSConversationView.swift
│ └── SettingsView.swift
│
├── TheaterPhoneAndroid/ # Android app (Kotlin / Jetpack Compose)
│ └── app/src/main/java/com/theaterphone/
│ ├── service/
│ │ ├── OscListenerService.kt # Foreground Service (UDP/TCP)
│ │ ├── OscParser.kt # OSC binary protocol parser
│ │ ├── PlainTextParser.kt # Plain text tokenizer
│ │ └── CommandDispatcher.kt # Routes commands to managers
│ ├── call/
│ │ ├── CallManager.kt # Call state machine
│ │ ├── CallNotificationHelper.kt # Full-screen call notification
│ │ └── IncomingCallActivity.kt # Lock screen call UI
│ ├── sms/
│ │ ├── SmsManager.kt # SMS state + messages
│ │ └── SmsNotificationHelper.kt
│ ├── audio/
│ │ ├── AudioService.kt # Vibration + tones
│ │ └── SoundLibraryManager.kt # Audio file import + playback
│ └── ui/screen/
│ ├── LockScreen.kt
│ ├── ActiveCallScreen.kt
│ ├── CallEndedScreen.kt
│ ├── SmsConversationScreen.kt
│ └── SettingsScreen.kt
│
├── QLab Script Cues/ # AppleScript fallback cues
├── TheaterPhone.qlabnetwork # QLab 5 network device preset
├── osc_send.py # Python test tool (OSC + Plain Text)
└── osc_ping_test.py # Ping/Pong debug script
- CallKit — iOS natively handles incoming call UI on the lock screen
- Background Audio — A silent audio loop keeps the app process alive
- Local Notifications — SMS messages appear as native iOS notifications
The app requests audio and voip background modes in Info.plist.
- Foreground Service — A persistent notification keeps the listener running
- Wake Lock — Prevents the CPU from sleeping while waiting for commands
- Full-Screen Intent — Incoming calls show over the lock screen via high-priority notification
Note: Some manufacturers (Samsung, Xiaomi, Huawei) aggressively kill background services. Disable battery optimization for TheaterPhone before the show.
Reading about Stage Caller was the inspiration for tackling this project.
MIT — see LICENSE