Skip to content

Commit

Permalink
Add Webaudio backend, very preliminary
Browse files Browse the repository at this point in the history
  • Loading branch information
solstice23 committed Oct 18, 2024
1 parent 94b7743 commit 75b938c
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 7 deletions.
133 changes: 133 additions & 0 deletions src/contexts/AudioBackend/AudioPlayback.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { createContext, useState, useRef, useContext, useEffect, useCallback, forwardRef, useImperativeHandle } from "react";



export const AudioPlayback = forwardRef((props, ref) => {
const {
type = 'html', // 'html' | 'webaudio'
} = props;

return (
type === 'html' ?
<HTMLAudio ref={ref} {...props}/> :
<WebAudio ref={ref} {...props}/>
)
});


const HTMLAudio = forwardRef((props, ref) => {
return (
<audio
ref={ref}
style={{display: 'none'}}
{ ...props }
></audio>
)
});

// TODO: entirely refactor this and related components
// Warning: WIP!
// TODO: fix playback speed related bugs
const WebAudio = forwardRef((props, ref) => {
const {
onTimeUpdate = () => {},
onPlay = () => {},
onPause = () => {},
onEnded = () => {},
onLoadedMetadata = () => {},
} = props;

// Simulate the behavior of HTMLAudioElement and expose, but with WebAudio API

const audioContextRef = useRef(new (window.AudioContext || window.webkitAudioContext)());

const durationRef = useRef(0);
const playingRef = useRef(false);

const rawArrayBufferRef = useRef(null);
const bufferRef = useRef(null);
const sourceRef = useRef(null);
const startTimeRef = useRef(0);
const pausedAtRef = useRef(0);
const playbackRateRef = useRef(1);

useImperativeHandle(ref, () => ({
set buffer(buffer) {
console.log("Set buffer");
rawArrayBufferRef.current = buffer;
// TODO: stop the previous audio
},
async load() {
const buffer = await audioContextRef.current.decodeAudioData(rawArrayBufferRef.current);
bufferRef.current = buffer;
durationRef.current = buffer.duration;
console.log("duration", buffer.duration);
onLoadedMetadata({ target: { duration: buffer.duration } });
pausedAtRef.current = 0;
console.log("Loaded");
},
play() {
console.log("Play");
if (playingRef.current) return;
if (!bufferRef.current) return;
if (audioContextRef.current.state === 'suspended') {
audioContextRef.current.resume();
}
sourceRef.current = audioContextRef.current.createBufferSource();
sourceRef.current.buffer = bufferRef.current;
sourceRef.current.connect(audioContextRef.current.destination);
sourceRef.current.playbackRate.value = playbackRateRef.current;
sourceRef.current.start(0, pausedAtRef.current / playbackRateRef.current);
playingRef.current = true;
startTimeRef.current = audioContextRef.current.currentTime - pausedAtRef.current / playbackRateRef.current;
onPlay();
},
pause() {
if (!playingRef.current) return;
if (!sourceRef.current) return;
sourceRef.current.stop();
pausedAtRef.current = (audioContextRef.current.currentTime - startTimeRef.current) * playbackRateRef.current;
playingRef.current = false;
onPause();
},
get currentTime() {
if (!playingRef.current) return pausedAtRef.current;
return (audioContextRef.current.currentTime - startTimeRef.current) * playbackRateRef.current;
},
set currentTime(value) {
console.log("Set currentTime", value);
if (playingRef.current) {
console.log("Playing");
sourceRef.current.stop();
sourceRef.current = audioContextRef.current.createBufferSource();
sourceRef.current.buffer = bufferRef.current;
sourceRef.current.playbackRate.value = playbackRateRef.current;
sourceRef.current.connect(audioContextRef.current.destination);
sourceRef.current.start(0, value);
startTimeRef.current = audioContextRef.current.currentTime - value / playbackRateRef.current;
}
pausedAtRef.current = value;
onTimeUpdate({ target: { currentTime: value } });
},
get duration() {
return durationRef.current;
},
get paused() {
return !playingRef.current;
},
set playbackRate(value) {
playbackRateRef.current = value;
if (sourceRef.current) {
startTimeRef.current = audioContextRef.current.currentTime - pausedAtRef.current / playbackRateRef.current;
sourceRef.current.playbackRate.value = value;
}
},
get playbackRate() {
return playbackRateRef.current;
}

}), []);

return null;

});
11 changes: 8 additions & 3 deletions src/contexts/PlayStateContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createContext, useState, useRef, useContext, useEffect, useCallback } f
import { MapPackContext } from "./MapPackContext";
import { BeatmapsContext } from "./BeatmapsContext";
import { SettingsContext } from "./SettingsContext";
import { AudioPlayback } from "./AudioBackend/AudioPlayback";

export const PlayStateContext = createContext(null);

Expand Down Expand Up @@ -34,8 +35,12 @@ export const PlayStateProvider = ({children}) => {
const audioFile = zipFile?.[audioFileName];
if (!audioFile) throw new Error("No audio file found in beatmap");
const audioBuffer = await audioFile.async("arraybuffer");

// two loading ways, first one for html audio, second one for webaudio
playerRef.current.src = URL.createObjectURL(new Blob([audioBuffer]));
playerRef.current.load();
playerRef.current.buffer = audioBuffer;

await playerRef.current.load();
playerRef.current.play();
}

Expand Down Expand Up @@ -104,7 +109,7 @@ export const PlayStateProvider = ({children}) => {
getPreciseTime
}}>
{children}
<audio
<AudioPlayback
ref={playerRef}
style={{display: 'none'}}
onTimeUpdate={(e) => {
Expand All @@ -114,7 +119,7 @@ export const PlayStateProvider = ({children}) => {
onPause={() => _setPlaying(false)}
onEnded={() => _setPlaying(false)}
onLoadedMetadata={(e) => _setDuration(e.target.duration)}
></audio>
></AudioPlayback>
</PlayStateContext.Provider>
)
}
8 changes: 4 additions & 4 deletions src/modules/ControlBar/ProgressBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ function ProgressBarSlider({startTween, stopTween, extendTween}) {

const [dragging, setDragging] = useState(false);

const present = getPreciseTime() / (duration * 1000) * 100;
const percent = getPreciseTime() / (duration * 1000) * 100;

const getDurationByEvent = useCallback((e) => {
const rect = sliderRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const present = Math.min(1, Math.max(0, x / rect.width));
return present * duration;
const percent = Math.min(1, Math.max(0, x / rect.width));
return percent * duration;
}, [duration]);

const seek = (time) => {
Expand Down Expand Up @@ -147,7 +147,7 @@ function ProgressBarSlider({startTween, stopTween, extendTween}) {
<div
className="progress-bar-slider-handle"
ref={handleRef}
style={{left: `${present}%`}}
style={{left: `${percent}%`}}
/>
</div>
)
Expand Down

0 comments on commit 75b938c

Please sign in to comment.