packages

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
An audio player screen on Android and iOS.
An audio player screen on Android and iOS.

Install

Install the package into the same environment as pyxpo.

$pip install pyxpo-audio

Add audio to modules in your app config, then import it in your code.

pyproject.toml
[tool.pyxpo]
modules = ["audio"]
python
import pyxpo_audio as Audio

Needs 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

iOSiOS 15+. AVAudioPlayer for files (gapless loops), AVPlayer for URLs. Plays with the silent switch on.
AndroidAPI 24+. MediaPlayer. A play() asked while the file prepares starts when it is ready.
Desktop previewAn HTML audio element. Sound plays after the first tap on the window.
WebAn HTML audio element. Browsers play sound only after the first tap on the page.

Quick start

app/index.py
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.

iOSNone. No prompt.
AndroidINTERNET (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)→ AudioPlayer

A 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)→ AudioPlayer

For 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→ bool

True repeats forever. Settable.

volume→ float

0 to 1. Settable. Anything else, a bool too, raises ValueError.

playing→ bool

What was last asked for. Read only.

uri, id

The 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

KeyTypeDescription
str or PathfileA path or a file:// URI. A Path is resolved to an absolute path.
http(s):// URLstreamStreams on iOS with AVPlayer. Needs a network.
{"uri": ...}dictThe expo shape.
NoneemptyA player with no sound. All calls do nothing.

Errors

KeyTypeDescription
ValueErrorbad argumentA bad source, a volume outside 0 to 1, or a negative or non-number seek_to. The message names it.
ModuleNotLinkedErrorpyxpo.module_apiBuilt without "audio" in modules, and play() was called. Add it and rebuild.
no sound, no errornativeA 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.

The audio controls on Android and iOS.
The audio controls on Android and iOS.
python
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.

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

python
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.playing

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