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.
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.
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.
| Methods | Type | Description |
|---|---|---|
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) => () => void | Hear 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) => () => void | Hear 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) => AudioPlayer | An independent player. Both arguments default; remove it by hand. |
| Player | Type | Description |
|---|---|---|
idstringThe native player id. | string | The native player id. |
currentTimenumberPosition in seconds. | number | Position in seconds. |
durationnumberLength in seconds, 0 while unknown. | number | Length in seconds, 0 while unknown. |
playingbooleanWhether sound is coming out right now. | boolean | Whether sound is coming out right now. |
pausedbooleanWhether the player is paused. | boolean | Whether the player is paused. |
isLoadedbooleanWhether media is loaded. | boolean | Whether media is loaded. |
isBufferingbooleanWhether the player is waiting on data. | boolean | Whether the player is waiting on data. |
isAudioSamplingSupportedbooleanAlways false; sampling is not covered. | boolean | Always false; sampling is not covered. |
loopbooleanRestart the track at the end. | boolean | Restart the track at the end. |
mutedbooleanSilence this player. | boolean | Silence this player. |
volumenumberGain from 0 to 1. | number | Gain from 0 to 1. |
playbackRatenumberThe current speed. | number | The current speed. |
shouldCorrectPitchbooleanKeep the pitch at changed speeds. Default true. | boolean | Keep the pitch at changed speeds. Default true. |
play() => voidPlay, loading first when needed. | () => void | Play, loading first when needed. |
pause() => voidPause this player. | () => void | Pause 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) => void | Swap 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. | () => void | Release the player and its controls claim. Idempotent. |
getStatus() => AudioStatusRead the status synchronously. | () => AudioStatus | Read the status synchronously. |
setPlaybackRate(rate: number, pitchCorrectionQuality?: PitchCorrectionQuality) => voidSet the speed without reloading. | (rate: number, pitchCorrectionQuality?: PitchCorrectionQuality) => void | Set 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 ) => AudioPlayerSubscription | Status on the update interval, plus one initial emit. |
removeAllListeners() => voidStop the status polling. | () => void | Stop the status polling. |
| Player status | Type | Description |
|---|---|---|
currentTimenumberPosition in seconds. | number | Position in seconds. |
didJustFinishbooleanSet on a natural end, cleared by play, seek, or replace. | boolean | Set on a natural end, cleared by play, seek, or replace. |
durationnumberLength in seconds. | number | Length in seconds. |
errorstring | nullA load or playback failure, else null. | string | null | A load or playback failure, else null. |
idstringThe native player id. | string | The native player id. |
isBufferingbooleanWhether the player is waiting on data. | boolean | Whether the player is waiting on data. |
isLoadedbooleanWhether media is loaded. | boolean | Whether media is loaded. |
loopbooleanWhether the track restarts at the end. | boolean | Whether the track restarts at the end. |
mutebooleanMirrors the muted property. | boolean | Mirrors the muted property. |
playbackRatenumberThe current speed. | number | The current speed. |
playingbooleanWhether sound is coming out right now. | boolean | Whether sound is coming out right now. |
shouldCorrectPitchbooleanWhether pitch is held at changed speeds. | boolean | Whether pitch is held at changed speeds. |
| Player source | Type | Description |
|---|---|---|
uristringThe audio file or stream URL. | string | The 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. | string | A label for your own bookkeeping. |
| Player options | Type | Description |
|---|---|---|
updateIntervalnumberStatus poll interval in milliseconds. Default 500. | number | Status poll interval in milliseconds. Default 500. |
crossOrigin"anonymous" | "use-credentials"The CORS mode. Web only. | "anonymous" | "use-credentials" | The CORS mode. Web only. |
| Play options | Type | Description |
|---|---|---|
positionMsnumberStart here, in milliseconds. Default 0. | number | Start here, in milliseconds. Default 0. |
pausedbooleanStay paused at the position until resume. | boolean | Stay paused at the position until resume. |
| Playback status | Type | Description |
|---|---|---|
stateAudioPlaybackStateIdle, loading, playing, paused, ended, or failed. | AudioPlaybackState | Idle, loading, playing, paused, ended, or failed. |
uristringThe playing URI. | string | The playing URI. |
positionMsnumberPosition in milliseconds. | number | Position in milliseconds. |
durationMsnumberLength in milliseconds, when finite. | number | Length in milliseconds, when finite. |
errorstringThe failure, when the state is failed. | string | The failure, when the state is failed. |
| Recording status | Type | Description |
|---|---|---|
stateAudioRecordingStateIdle, recording, or paused. | AudioRecordingState | Idle, recording, or paused. |
uristringThe file being written. | string | The file being written. |
durationMsnumberRecorded audio so far, excluding pauses. | number | Recorded audio so far, excluding pauses. |
| Recording result | Type | Description |
|---|---|---|
uristringThe finished file. | string | The finished file. |
durationMsnumberRecorded audio, excluding pauses. | number | Recorded audio, excluding pauses. |
sizenumberThe file size in bytes. | number | The file size in bytes. |
| Now playing | Type | Description |
|---|---|---|
titlestringThe track title on the controls. | string | The track title on the controls. |
artiststringThe artist line. | string | The artist line. |
albumTitlestringThe album line. | string | The album line. |
artworkUristringA local file image for the artwork. | string | A local file image for the artwork. |
| Interruption | Type | Description |
|---|---|---|
typeAudioInterruptionTypeBegan or ended. | AudioInterruptionType | Began or ended. |
shouldResumebooleanThe system recommendation to resume. | boolean | The system recommendation to resume. |
| Remote command | Type | Description |
|---|---|---|
typeAudioRemoteCommandTypePlay, pause, or seek. | AudioRemoteCommandType | Play, pause, or seek. |
positionMsnumberThe target on seek. | number | The target on seek. |
playerIdstringThe owning player, absent for the singleton. | string | The owning player, absent for the singleton. |
| Subscription | Type | Description |
|---|---|---|
remove() => voidStop this listener. | () => void | Stop this listener. |
| iOS | Android | Web | |
|---|---|---|---|
| Engine | AVPlayer | MediaPlayer | Audio element |
| Recorder | AAC file in cache | AAC file in cache | Blob URL |
| Interrupt | Session notice | Audio focus | None, inert |
| Controls | Lock screen session | Media session | Media Session |
| Background | Audio mode | Foreground service | Tab must stay open |
| Mic setup | Usage text | Runtime permission | Browser prompt |
play replaces the
current track. Use player instances for overlapping or independent tracks.One.FileSystem.move.remove throws.
The hooks remove on unmount; hand-created players need one remove call.play
called outside a user action.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.
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.
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.
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.
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.