Skip to content

Instantly share code, notes, and snippets.

@smontlouis
Created December 16, 2024 14:46
Show Gist options
  • Save smontlouis/a29ac563e5a9c3b429d805e07a09f553 to your computer and use it in GitHub Desktop.
Save smontlouis/a29ac563e5a9c3b429d805e07a09f553 to your computer and use it in GitHub Desktop.
expo-av audio fade
import {
AVPlaybackStatus,
AVPlaybackStatusError,
AVPlaybackStatusSuccess,
Audio,
} from 'expo-av'
import { Platform } from 'react-native'
const FADE_DURATION = 600
const isPlaybackStatusSuccess = (
s: AVPlaybackStatus
): s is AVPlaybackStatusSuccess => {
return (s as AVPlaybackStatusError)?.error === undefined
}
export class FadingLooper {
uri: string
sound1: Audio.Sound
sound2: Audio.Sound
isInitialized: boolean
isCurrentlyPlaying: boolean
isFading: boolean
soundIsPlaying: 'sound1' | 'sound2'
volume: number
private fadeOutPromise: Promise<void> | null = null
private lastAction: 'play' | 'pause' | null = null
private actionTimestamp: number = 0
constructor(uri: string, volume: number) {
this.uri = uri
this.sound1 = new Audio.Sound()
this.sound2 = new Audio.Sound()
this.isInitialized = false
this.isCurrentlyPlaying = false
this.isFading = false
this.soundIsPlaying = 'sound1'
// round to 1 decimal places
this.volume = Math.round(volume * 10) / 10
}
async init() {
try {
await this.sound1.loadAsync({ uri: this.uri })
await this.sound2.loadAsync({ uri: this.uri })
await this.sound1.setProgressUpdateIntervalAsync(200)
await this.sound2.setProgressUpdateIntervalAsync(200)
await this.sound1.setVolumeAsync(this.volume)
await this.sound2.setVolumeAsync(this.volume)
this.sound1.setOnPlaybackStatusUpdate(
this.handlePlaybackStatusUpdate.bind(
this,
this.sound1,
this.sound2,
'sound1'
)
)
this.sound2.setOnPlaybackStatusUpdate(
this.handlePlaybackStatusUpdate.bind(
this,
this.sound2,
this.sound1,
'sound2'
)
)
this.isInitialized = true
} catch (error) {
console.warn('FadingLooper init error:', error)
}
}
play = async () => {
try {
const now = Date.now()
this.lastAction = 'play'
this.actionTimestamp = now
if (!this.isInitialized) {
await this.init()
}
// Attendre que le fadeOut soit terminé avant de jouer
if (this.fadeOutPromise) {
await this.fadeOutPromise
}
// Vérifier si une autre action n'a pas été demandée entre temps
if (this.lastAction !== 'play' || this.actionTimestamp !== now) {
return
}
if (!this.isCurrentlyPlaying) {
await this[this.soundIsPlaying].playAsync()
this.isCurrentlyPlaying = true
}
} catch (error) {
console.warn('FadingLooper play error:', error)
}
}
setVolume = async (volume: number) => {
try {
const status = (await this[
this.soundIsPlaying
].getStatusAsync()) as AVPlaybackStatusSuccess
if ((!this.isFading || this.isInitialized) && status.isLoaded) {
await this.sound1.setVolumeAsync(volume)
await this.sound2.setVolumeAsync(volume)
}
this.volume = volume
} catch (error) {
console.warn('FadingLooper setVolume error:', error)
}
}
pause = async () => {
try {
const now = Date.now()
this.lastAction = 'pause'
this.actionTimestamp = now
const status = (await this[
this.soundIsPlaying
].getStatusAsync()) as AVPlaybackStatusSuccess
if (status.isPlaying) {
this.fadeOutPromise = this.fadeOutAndPause(
this[this.soundIsPlaying],
now
)
await this.fadeOutPromise
this.fadeOutPromise = null
this.isCurrentlyPlaying = false
}
} catch (error) {
console.warn('FadingLooper pause error:', error)
}
}
fadeOutAndPause = async (sound: Audio.Sound, timestamp: number) => {
try {
if (Platform.OS === 'android') {
await sound.pauseAsync()
return
}
const fadeSteps = 10
const stepDuration = FADE_DURATION / fadeSteps
const volumeStep = this.volume / fadeSteps
for (let i = fadeSteps; i > 0; i--) {
// Vérifier si une nouvelle action n'a pas été demandée
if (this.actionTimestamp !== timestamp) {
return
}
const newVolume = volumeStep * i
await sound.setVolumeAsync(newVolume)
await new Promise((resolve) => setTimeout(resolve, stepDuration))
}
// Vérifier une dernière fois si l'action est toujours valide
if (this.actionTimestamp === timestamp) {
await sound.pauseAsync()
}
} catch (error) {
console.warn('FadingLooper fadeOutAndPause error:', error)
}
}
destroy = async () => {
try {
this.sound1.unloadAsync()
this.sound2.unloadAsync()
} catch (error) {
console.warn('FadingLooper destroy error:', error)
}
}
fade = async ({
name,
sound,
fromVolume,
toVolume,
}: {
name: 'sound1' | 'sound2'
sound: Audio.Sound
fromVolume: number
toVolume: number
}) => {
try {
if (Platform.OS === 'android') {
if (fromVolume > toVolume) {
await sound.setVolumeAsync(0)
await sound.stopAsync()
} else {
await sound.setVolumeAsync(toVolume)
}
return
}
const fadeSteps = 10
const stepDuration = FADE_DURATION / fadeSteps
const volumeDiff = toVolume - fromVolume
const volumeStep = volumeDiff / fadeSteps
for (let i = 0; i <= fadeSteps; i++) {
const newVolume = fromVolume + volumeStep * i
try {
await sound.setVolumeAsync(newVolume)
await new Promise((resolve) => setTimeout(resolve, stepDuration))
} catch (e) {
console.log('error for', name, newVolume, toVolume)
}
}
if (toVolume === 0) {
await sound.stopAsync()
}
} catch (error) {
console.warn('FadingLooper fade error:', error)
}
}
handlePlaybackStatusUpdate = async (
currentSound: Audio.Sound,
nextSound: Audio.Sound,
soundName: 'sound1' | 'sound2',
status: AVPlaybackStatus
) => {
if (!isPlaybackStatusSuccess(status) || !this.isCurrentlyPlaying) {
return
}
if (!status.durationMillis) {
return
}
if (
status.positionMillis > status.durationMillis - FADE_DURATION &&
!this.isFading
) {
this.isFading = true
const nextSoundName = soundName === 'sound1' ? 'sound2' : 'sound1'
this.soundIsPlaying = nextSoundName
try {
await Promise.all([
this.fade({
name: soundName,
sound: currentSound,
fromVolume: this.volume,
toVolume: 0,
}),
(async () => {
await nextSound.setVolumeAsync(0)
await nextSound.setPositionAsync(0)
await nextSound.playAsync()
return this.fade({
name: nextSoundName,
sound: nextSound,
fromVolume: 0,
toVolume: this.volume,
})
})(),
])
this.isFading = false
} catch (error) {
console.error('Fade error:', error)
this.isFading = false
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment