One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Audio

Play tracks, record microphone files, and answer lock screen controls

One.Audio plays local files and remote URLs, records the microphone to AAC files, and answers interruptions and lock screen controls. It offers two playback shapes: a singleton (play, pause, resume, seek, stop) that holds one track at a time, and independent AudioPlayer instances from createPlayer or useAudioPlayer, each with its own position, volume, and rate.

On iOS playback runs on AVPlayer and recording on AVAudioRecorder, with MPNowPlayingSession and the remote command center behind lock screen controls. On Android playback runs on MediaPlayer and recording on MediaRecorder, with audio focus driving interruptions and a MediaSession behind the controls. On web an HTMLAudioElement plays and MediaRecorder plus getUserMedia records.

Usage

import { One } from 'one'
await One.Audio.play('https://example.com/episode.m4a')
const status = await One.Audio.getPlaybackStatus()
if (status.state === 'playing') {
await One.Audio.pause()
await One.Audio.seek(30_000)
await One.Audio.resume()
}
await One.Audio.stop()

The singleton plays one URI at a time and reports its state on every call, so a screen can drive transport buttons straight from the returned status.

How it works

The singleton play() builds a player for the URI, replacing any previous one, and switches the shared audio session to the playback category on iOS. Every transport call resolves with the playback status: the state (idle, loading, playing, paused, ended, or failed), the URI, the position, the duration when finite, and the error when failed. A remote source can report loading until enough data arrives; seek needs a ready item and rejects while loading or failed. resume restarts an ended item from the beginning.

Pass { positionMs, paused } to play to load a track at a position without hearing its opening. With either option set, play prepares the item, seeks exactly to the position, and resolves only after the seek lands: paused when paused is true, otherwise started from the position. The status reports loading until then, and on iOS can read loading for a moment after a started load resolves while the player begins. A pause or resume during the load changes whether it starts, seek rejects with E_AUDIO_STATE, and a new play or stop rejects the pending load with E_AUDIO_STATE. A position past the end loads at the end, where the status can read ended and resume restarts from the beginning. A load error rejects and leaves the status failed; on web a source that cannot seek, such as a server without range requests, fails the same way rather than starting at 0. Without options play behaves as before. Starting playback replaces the previous player; stop a paused player before recording. All native work runs through the main thread on both platforms, and every failure rejects with an Error whose code holds the stable E_AUDIO_* value.

Recording writes AAC in an .m4a file under the app cache on native. Request the recording permission first; playback alone needs none. Set native.app.audio.microphone to the purpose string before prebuild: it becomes NSMicrophoneUsageDescription on iOS and stamps RECORD_AUDIO into the Android manifest. Recording and playback cannot run together. The recorder pauses on interruption and reports paused, so resume after the interruption ends. Files in cache can be evicted, so move a keeper to permanent storage with One.FileSystem.move.

watchInterruptions reports when another session takes over: the type is began or ended, and shouldResume is the system’s recommendation, not an automatic action. One pauses its own player or recorder on began; resume only after ended, per your own policy. On iOS the notice comes from AVAudioSession on the main queue; on Android it comes from audio focus loss. watchRemoteCommands reports play, pause, and seek from the system controls, with the position on seek, after your screen registers metadata with setNowPlayingInfo. Artwork must be an existing local file:// image, so download remote art to cache first. stop or a new play clears the metadata and handlers; clearNowPlayingInfo removes them while the player keeps playing.

The system controls belong to one owner at a time: the singleton or one player instance. player.setNowPlayingInfo(info) makes that player the owner with the given metadata, taking over from the singleton or another player without clearing the controls in between. Remote play, pause, and seek then drive only the owner, and watchRemoteCommands events carry its playerId; events from the singleton have none. Each owner releases only its own claim: player.clearNowPlayingInfo(), player.remove(), and a hook unmount release a player’s claim, while Audio.clearNowPlayingInfo, stop, and a new play release only the singleton’s. Once ownership has moved, the old owner’s cleanup leaves the new owner alone, and nothing falls back to the singleton when a player releases. player.replace keeps the claim and its metadata, so set new metadata when the track changes. Interruptions pause every player as before and the controls show the owner paused. One fades nothing for you: to crossfade, start the next player, ramp both volumes, move the controls to the next player, and remove the old one. On Android the background service follows the owner, so an owning player keeps playing in the background like the singleton. On web a newer claim or release wins over a slower one still decoding its artwork, which then rejects.

For playback with another app in front, set native.app.audio.background: true and rerun prebuild. On iOS this adds audio to the background modes and playback activates the playback category before the app backgrounds. On Android it stamps a mediaPlayback foreground service that runs while audio plays.

Player instances are independent of each other and of the singleton. The source is a URI string, { uri, headers, name }, or null for a player with no media yet; numeric asset ids throw, so bundle audio as a file and pass its URI. Options are { updateInterval, crossOrigin }: the status poll interval in milliseconds (default 500, driving addListener and useAudioPlayerStatus) and the web-only CORS mode. useAudioPlayer creates one player, replaces its source when the source value changes, and removes it on unmount. useAudioPlayerStatus returns the current status and re-renders on every update. Time reads in seconds. setPlaybackRate changes speed without reloading: 0 to 2 on iOS and web, 0.1 to 2 on Android, else E_AUDIO_RATE. Pitch quality is iOS only. Source headers apply on iOS and Android and are ignored on web.

On web, URI inputs use browser HTTP, HTTPS, data, or blob URLs, and autoplay policy can reject playback without a user gesture. A stopped recording returns its encoded byte length and a blob URL in the browser’s own codec; release it with URL.revokeObjectURL when finished. The Media Session API supplies metadata and remote commands when present. OS audio interruptions have no browser equivalent, so watchInterruptions keeps an inert remover there.

API reference

MethodsTypeDescription
getRecordingPermissionStatus() => Promise<AudioRecordingPermission>Undetermined, granted, or denied, without prompting.
() => Promise<AudioRecordingPermission>Undetermined, granted, or denied, without prompting.
requestRecordingPermission() => Promise<AudioRecordingPermission>Ask for microphone access for recording.
() => Promise<AudioRecordingPermission>Ask for microphone access for recording.
play(uri: string, options?: AudioPlayOptions) => Promise<AudioPlaybackStatus>Play one URI on the singleton, replacing any current track. Options load at a position, optionally paused.
(uri: string, options?: AudioPlayOptions) => Promise<AudioPlaybackStatus>Play one URI on the singleton, replacing any current track. Options load at a position, optionally paused.
getPlaybackStatus() => Promise<AudioPlaybackStatus>The singleton state, URI, position, and duration.
() => Promise<AudioPlaybackStatus>The singleton state, URI, position, and duration.
pause() => Promise<AudioPlaybackStatus>Pause the singleton and report its status.
() => Promise<AudioPlaybackStatus>Pause the singleton and report its status.
resume() => Promise<AudioPlaybackStatus>Resume the singleton, or restart an ended item.
() => Promise<AudioPlaybackStatus>Resume the singleton, or restart an ended item.
seek(positionMs: number) => Promise<AudioPlaybackStatus>Move the singleton to a millisecond position.
(positionMs: number) => Promise<AudioPlaybackStatus>Move the singleton to a millisecond position.
stop() => Promise<void>Stop the singleton and clear its metadata.
() => Promise<void>Stop the singleton and clear its metadata.
startRecording() => Promise<AudioRecordingStatus>Start recording the microphone to a cache file.
() => Promise<AudioRecordingStatus>Start recording the microphone to a cache file.
getRecordingStatus() => Promise<AudioRecordingStatus>The recorder state, file URI, and elapsed time.
() => Promise<AudioRecordingStatus>The recorder state, file URI, and elapsed time.
pauseRecording() => Promise<AudioRecordingStatus>Pause the recorder without ending the file.
() => Promise<AudioRecordingStatus>Pause the recorder without ending the file.
resumeRecording() => Promise<AudioRecordingStatus>Resume a paused recorder.
() => Promise<AudioRecordingStatus>Resume a paused recorder.
stopRecording() => Promise<AudioRecordingResult>End the recording and report its file.
() => Promise<AudioRecordingResult>End the recording and report its file.
watchInterruptions(onEvent: (event: AudioInterruptionEvent) => void) => () => voidHear began and ended. Returns a stop function.
(onEvent: (event: AudioInterruptionEvent) => void) => () => voidHear began and ended. Returns a stop function.
setNowPlayingInfo(info: AudioNowPlayingInfo) => Promise<void>Make the singleton the owner of the system media controls.
(info: AudioNowPlayingInfo) => Promise<void>Make the singleton the owner of the system media controls.
clearNowPlayingInfo() => Promise<void>Release the controls if the singleton owns them, keep playing.
() => Promise<void>Release the controls if the singleton owns them, keep playing.
watchRemoteCommands(onEvent: (event: AudioRemoteCommandEvent) => void) => () => voidHear play, pause, and seek. Returns a stop function.
(onEvent: (event: AudioRemoteCommandEvent) => void) => () => voidHear play, pause, and seek. Returns a stop function.
createPlayer(source: AudioSource, options: AudioPlayerOptions) => AudioPlayerAn independent player. Both arguments default; remove it by hand.
(source: AudioSource, options: AudioPlayerOptions) => AudioPlayerAn independent player. Both arguments default; remove it by hand.
PlayerTypeDescription
idstringThe native player id.
stringThe native player id.
currentTimenumberPosition in seconds.
numberPosition in seconds.
durationnumberLength in seconds, 0 while unknown.
numberLength in seconds, 0 while unknown.
playingbooleanWhether sound is coming out right now.
booleanWhether sound is coming out right now.
pausedbooleanWhether the player is paused.
booleanWhether the player is paused.
isLoadedbooleanWhether media is loaded.
booleanWhether media is loaded.
isBufferingbooleanWhether the player is waiting on data.
booleanWhether the player is waiting on data.
isAudioSamplingSupportedbooleanAlways false; sampling is not covered.
booleanAlways false; sampling is not covered.
loopbooleanRestart the track at the end.
booleanRestart the track at the end.
mutedbooleanSilence this player.
booleanSilence this player.
volumenumberGain from 0 to 1.
numberGain from 0 to 1.
playbackRatenumberThe current speed.
numberThe current speed.
shouldCorrectPitchbooleanKeep the pitch at changed speeds. Default true.
booleanKeep the pitch at changed speeds. Default true.
play() => voidPlay, loading first when needed.
() => voidPlay, loading first when needed.
pause() => voidPause this player.
() => voidPause this player.
seekTo( seconds: number, toleranceMillisBefore?: number, toleranceMillisAfter?: number ) => Promise<void>Seek in seconds. Tolerances are iOS only.
( seconds: number, toleranceMillisBefore?: number, toleranceMillisAfter?: number ) => Promise<void>Seek in seconds. Tolerances are iOS only.
replace(source: AudioSource) => voidSwap the media, clearing any error.
(source: AudioSource) => voidSwap the media, clearing any error.
setNowPlayingInfo(info: AudioNowPlayingInfo) => Promise<void>Make this player the owner of the system media controls.
(info: AudioNowPlayingInfo) => Promise<void>Make this player the owner of the system media controls.
clearNowPlayingInfo() => Promise<void>Release the controls if this player owns them.
() => Promise<void>Release the controls if this player owns them.
remove() => voidRelease the player and its controls claim. Idempotent.
() => voidRelease the player and its controls claim. Idempotent.
getStatus() => AudioStatusRead the status synchronously.
() => AudioStatusRead the status synchronously.
setPlaybackRate(rate: number, pitchCorrectionQuality?: PitchCorrectionQuality) => voidSet the speed without reloading.
(rate: number, pitchCorrectionQuality?: PitchCorrectionQuality) => voidSet the speed without reloading.
addListener( event: "playbackStatusUpdate", listener: (status: AudioStatus) => void ) => AudioPlayerSubscriptionStatus on the update interval, plus one initial emit.
( event: "playbackStatusUpdate", listener: (status: AudioStatus) => void ) => AudioPlayerSubscriptionStatus on the update interval, plus one initial emit.
removeAllListeners() => voidStop the status polling.
() => voidStop the status polling.
Player statusTypeDescription
currentTimenumberPosition in seconds.
numberPosition in seconds.
didJustFinishbooleanSet on a natural end, cleared by play, seek, or replace.
booleanSet on a natural end, cleared by play, seek, or replace.
durationnumberLength in seconds.
numberLength in seconds.
errorstring | nullA load or playback failure, else null.
string | nullA load or playback failure, else null.
idstringThe native player id.
stringThe native player id.
isBufferingbooleanWhether the player is waiting on data.
booleanWhether the player is waiting on data.
isLoadedbooleanWhether media is loaded.
booleanWhether media is loaded.
loopbooleanWhether the track restarts at the end.
booleanWhether the track restarts at the end.
mutebooleanMirrors the muted property.
booleanMirrors the muted property.
playbackRatenumberThe current speed.
numberThe current speed.
playingbooleanWhether sound is coming out right now.
booleanWhether sound is coming out right now.
shouldCorrectPitchbooleanWhether pitch is held at changed speeds.
booleanWhether pitch is held at changed speeds.
Player sourceTypeDescription
uristringThe audio file or stream URL.
stringThe audio file or stream URL.
headersRecord<string, string>Request headers. Native only.
Record<string, string>Request headers. Native only.
namestringA label for your own bookkeeping.
stringA label for your own bookkeeping.
Player optionsTypeDescription
updateIntervalnumberStatus poll interval in milliseconds. Default 500.
numberStatus poll interval in milliseconds. Default 500.
crossOrigin"anonymous" | "use-credentials"The CORS mode. Web only.
"anonymous" | "use-credentials"The CORS mode. Web only.
Play optionsTypeDescription
positionMsnumberStart here, in milliseconds. Default 0.
numberStart here, in milliseconds. Default 0.
pausedbooleanStay paused at the position until resume.
booleanStay paused at the position until resume.
Playback statusTypeDescription
stateAudioPlaybackStateIdle, loading, playing, paused, ended, or failed.
AudioPlaybackStateIdle, loading, playing, paused, ended, or failed.
uristringThe playing URI.
stringThe playing URI.
positionMsnumberPosition in milliseconds.
numberPosition in milliseconds.
durationMsnumberLength in milliseconds, when finite.
numberLength in milliseconds, when finite.
errorstringThe failure, when the state is failed.
stringThe failure, when the state is failed.
Recording statusTypeDescription
stateAudioRecordingStateIdle, recording, or paused.
AudioRecordingStateIdle, recording, or paused.
uristringThe file being written.
stringThe file being written.
durationMsnumberRecorded audio so far, excluding pauses.
numberRecorded audio so far, excluding pauses.
Recording resultTypeDescription
uristringThe finished file.
stringThe finished file.
durationMsnumberRecorded audio, excluding pauses.
numberRecorded audio, excluding pauses.
sizenumberThe file size in bytes.
numberThe file size in bytes.
Now playingTypeDescription
titlestringThe track title on the controls.
stringThe track title on the controls.
artiststringThe artist line.
stringThe artist line.
albumTitlestringThe album line.
stringThe album line.
artworkUristringA local file image for the artwork.
stringA local file image for the artwork.
InterruptionTypeDescription
typeAudioInterruptionTypeBegan or ended.
AudioInterruptionTypeBegan or ended.
shouldResumebooleanThe system recommendation to resume.
booleanThe system recommendation to resume.
Remote commandTypeDescription
typeAudioRemoteCommandTypePlay, pause, or seek.
AudioRemoteCommandTypePlay, pause, or seek.
positionMsnumberThe target on seek.
numberThe target on seek.
playerIdstringThe owning player, absent for the singleton.
stringThe owning player, absent for the singleton.
SubscriptionTypeDescription
remove() => voidStop this listener.
() => voidStop this listener.

Platform differences

iOSAndroidWeb
EngineAVPlayerMediaPlayerAudio element
RecorderAAC file in cacheAAC file in cacheBlob URL
InterruptSession noticeAudio focusNone, inert
ControlsLock screen sessionMedia sessionMedia Session
BackgroundAudio modeForeground serviceTab must stay open
Mic setupUsage textRuntime permissionBrowser prompt

Limits and gotchas

Examples

Play an episode with a speed toggle

import { Button, Text } from 'react-native'
import { useAudioPlayer, useAudioPlayerStatus } from 'one'
function Episode({ uri }: { uri: string }) {
const player = useAudioPlayer(uri)
const status = useAudioPlayerStatus(player)
function toggleSpeed() {
player.setPlaybackRate(status.playbackRate === 1 ? 1.5 : 1)
}
return (
<>
<Button
title={status.playing ? 'Pause' : 'Play'}
onPress={() => (status.playing ? player.pause() : player.play())}
/>
<Button title={`${status.playbackRate}x speed`} onPress={toggleSpeed} />
<Text>
{Math.floor(status.currentTime)}s of {Math.floor(status.duration)}s
</Text>
</>
)
}

The hook owns the player lifetime: it creates the player, reloads it when the URI changes, and removes it on unmount. The status hook re-renders the screen on every poll tick, so transport UI stays current.

Record a memo and keep it

import { useState } from 'react'
import { Button, Text } from 'react-native'
import { One } from 'one'
function MemoRecorder({ keepDir }: { keepDir: string }) {
const [kept, setKept] = useState('')
async function record() {
const permission = await One.Audio.requestRecordingPermission()
if (permission !== 'granted') return
await One.Audio.startRecording()
}
async function finish() {
const recorded = await One.Audio.stopRecording()
const keptUri = `${keepDir}/memo.m4a`
await One.FileSystem.move(recorded.uri, keptUri)
setKept(keptUri)
}
return (
<>
<Button title="Record" onPress={record} />
<Button title="Stop and keep" onPress={finish} />
{kept ? <Text>Kept {kept}</Text> : null}
</>
)
}

Recording starts only after the grant, and stopping returns the cache file at once. Moving it out of cache before anything else makes the memo survive eviction.

Drive lock screen controls

import { useEffect } from 'react'
import { One } from 'one'
function useNowPlaying(trackUri: string, title: string, artist: string) {
useEffect(() => {
async function setup() {
await One.Audio.play(trackUri)
await One.Audio.setNowPlayingInfo({ title, artist })
}
setup()
const stopWatching = One.Audio.watchRemoteCommands((event) => {
void One.Audio.getPlaybackStatus().then((status) => {
console.log(event.type, status.state, status.positionMs)
})
})
return () => {
stopWatching()
void One.Audio.stop()
}
}, [trackUri, title, artist])
}

Metadata goes up after playback starts, since the controls attach to the current player. The remote listener only observes; the handlers already drove the player, so the screen just syncs its own state.

Swap recordings at the same position

import { One } from 'one'
async function switchRecording(uri: string) {
const { positionMs } = await One.Audio.getPlaybackStatus()
await One.Audio.play(uri, { positionMs, paused: true })
await One.Audio.resume()
}

The new recording is ready at the old position before it makes a sound, so the opening never plays. A second switch before the first finishes rejects the first with E_AUDIO_STATE.

Crossfade with lock screen controls

import { One, type AudioPlayer } from 'one'
async function crossfade(from: AudioPlayer, uri: string, title: string) {
const to = One.Audio.createPlayer(uri)
to.volume = 0
await to.seekTo(from.currentTime).catch(() => {})
to.play()
await to.setNowPlayingInfo({ title })
for (let step = 1; step <= 20; step++) {
await new Promise((resolve) => setTimeout(resolve, 100))
to.volume = step / 20
from.volume = 1 - step / 20
}
from.remove()
return to
}

Moving the controls before the fade keeps the lock screen card up the whole time, and removing the old player afterwards leaves the new owner in place. seekTo needs a loaded player, so production code waits for isLoaded before seeking.

Edit this page on GitHub.