Audio
Play a file, a file:// URI or an http(s) URL with play(), pause() and seek_to(seconds), and change loop and volume at any time. use_audio_player(source, loop, volume) makes the player inside a component, follows your arguments on every render and frees it on unmount. The native player is made on the first play() or seek_to(), so a player that never plays costs nothing. Sound pauses while the app is in the background, iOS plays with the silent switch on and mixes with other apps, and the web page and desktop window play through an HTML audio element.
- pyxpo-audio
- v0.1.0a1
- Native: Swift + Kotlin
- Like expo-audio

Install
Install the package into the same environment as pyxpo.
Add audio to modules in your app config, then import it in your code.
[tool.pyxpo]
modules = ["audio"]import pyxpo_audio as AudioNeeds Python 3.9+ and pyxpo>=0.1.0a2.
This module has native code. After you add it, run pyxpo run or pyxpo build again so it is compiled into the app. Hot reload alone is not enough.
Platforms
Quick start
import os
import pyxpo_audio as Audio
from pyxpo import Button, Text, View, component
CHIME = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "chime.wav")
@component
def Chime():
player = Audio.use_audio_player(CHIME, loop=True, volume=0.8)
return View(
Text("Playing" if player.playing else "Paused"),
Button("Pause" if player.playing else "Play",
lambda: player.pause() if player.playing else player.play()),
Button("Restart", lambda: player.seek_to(0)),
)Permissions
pyxpo adds these to Info.plist and AndroidManifest.xml for you when the module is listed.
| iOS | None. No prompt. |
|---|---|
| Android | INTERNET (granted at install) for URLs, added by the module. |
API reference
Players
source is a path, a file:// URI, a pathlib.Path, an http(s) URL, expo's {"uri": ...}, or None for a player that plays nothing. Any other type, or an empty string, raises ValueError. Put files you ship next to your Python code.
use_audio_player(source, *, loop=False, volume=1.0)→ AudioPlayerA hook. loop and volume follow the arguments on every render. A new source makes a new player and removes the old one. Freed on unmount.
create_audio_player(source, *, loop=False, volume=1.0)→ AudioPlayerFor use outside a component. Call remove() when done, or the native player stays alive until the app ends.
AudioPlayer
playing is what you last asked for, not a live status: a sound that does not loop stops at its end while playing stays True, and setting it does not re-render. Keep your own use_state flag when a label must change.
play()Starts or resumes. After a non-looping sound ended, starts it over.
pause()Pauses where it is.
seek_to(seconds)Jump to a time, a number >= 0. Makes the native player if there is none yet.
loop→ boolTrue repeats forever. Settable.
volume→ float0 to 1. Settable. Anything else, a bool too, raises ValueError.
playing→ boolWhat was last asked for. Read only.
uri, idThe resolved source and the player's number.
remove()Stops the sound and frees the native player. The player does nothing afterwards.
Types
Every name also has its expo spelling, such as Audio.useAudioPlayer and player.seekTo.
Source
| Key | Type | Description |
|---|---|---|
str or Path | file | A path or a file:// URI. A Path is resolved to an absolute path. |
http(s):// URL | stream | Streams on iOS with AVPlayer. Needs a network. |
{"uri": ...} | dict | The expo shape. |
None | empty | A player with no sound. All calls do nothing. |
Errors
| Key | Type | Description |
|---|---|---|
ValueError | bad argument | A bad source, a volume outside 0 to 1, or a negative or non-number seek_to. The message names it. |
ModuleNotLinkedError | pyxpo.module_api | Built without "audio" in modules, and play() was called. Add it and rebuild. |
no sound, no error | native | A file that cannot be found or decoded does not raise. Check the path first with os.path.exists. |
Examples
Recipe: a sound effect and background music
Looping music that follows a "music on" switch, and a one-shot effect on a button. pop.seek_to(0) before play() lets the effect play again when pressed fast.

import os
import pyxpo_audio as Audio
from pyxpo import Button, Text, View, component, use_state
HERE = os.path.dirname(os.path.abspath(__file__))
MUSIC = os.path.join(HERE, "assets", "music.mp3")
POP = os.path.join(HERE, "assets", "pop.wav")
@component
def Game():
music_on, set_music = use_state(True)
music = Audio.use_audio_player(MUSIC, loop=True, volume=0.4)
pop = Audio.use_audio_player(POP)
if music_on and not music.playing:
music.play()
if not music_on and music.playing:
music.pause()
def hit():
pop.seek_to(0)
pop.play()
return View(
Text("Music is %s" % ("on" if music_on else "off")),
Button("Toggle music", lambda: set_music(not music_on)),
Button("Pop", hit),
)A player you own
Outside a component, make and free the player yourself.
import pyxpo_audio as Audio
player = Audio.create_audio_player("https://example.com/beep.mp3", loop=False, volume=1.0)
player.play()
# ...
player.remove()Testing
Unit tests run on the desktop host. Replace the bridge call with a fake and check what the player sent. Value checks need no fake: create_audio_player("x", volume=2) raises ValueError.
import pyxpo_audio as Audio
def test_play_creates_then_plays(monkeypatch):
sent = []
monkeypatch.setattr(Audio, "call", lambda *a, **k: sent.append(a))
monkeypatch.setattr(Audio, "require_linked", lambda *a, **k: None)
player = Audio.create_audio_player("song.wav", loop=True, volume=0.5)
player.play()
assert sent[0][:2] == ("Audio", "create") and sent[0][3:] == ("song.wav", True, 0.5)
assert sent[1][:2] == ("Audio", "play")
assert player.playingGood to know
- It only plays. Recording is not part of this package.
- Several players can play at once; each one is independent.
- It does not play in the background or on the lock screen. It pauses and plays on when the app returns.
- Formats are whatever the platform's player supports, such as WAV, MP3 and AAC on both phones.
- No module options and no config plugin. The only setup is the modules entry.