wrap pausable / play-rate-able audio in a React hook
This commit is contained in:
parent
99180e63ce
commit
08dc096792
2 changed files with 79 additions and 87 deletions
|
|
@ -13,6 +13,7 @@ import { RaisedEvent } from "@/statecharts/runtime_types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import "./Microwave.css";
|
import "./Microwave.css";
|
||||||
|
import { useAudioContext } from "./useAudioContext";
|
||||||
|
|
||||||
export type MagnetronState = "on" | "off";
|
export type MagnetronState = "on" | "off";
|
||||||
export type DoorState = "open" | "closed";
|
export type DoorState = "open" | "closed";
|
||||||
|
|
@ -56,116 +57,49 @@ const imgs = {
|
||||||
open: { off: imgSmallOpenedOff, on: imgSmallOpenedOn },
|
open: { off: imgSmallOpenedOff, on: imgSmallOpenedOn },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const BUTTON_HEIGHT = 18;
|
const BUTTON_HEIGHT = 18;
|
||||||
const BUTTON_WIDTH = 60;
|
const BUTTON_WIDTH = 60;
|
||||||
|
|
||||||
const BUTTON_X0 = 412;
|
const BUTTON_X0 = 412;
|
||||||
const BUTTON_X1 = BUTTON_X0 + BUTTON_WIDTH;
|
|
||||||
|
|
||||||
const START_X0 = BUTTON_X0;
|
const START_X0 = BUTTON_X0;
|
||||||
const START_Y0 = 234;
|
const START_Y0 = 234;
|
||||||
const START_X1 = BUTTON_X1;
|
|
||||||
const START_Y1 = START_Y0 + BUTTON_HEIGHT;
|
|
||||||
|
|
||||||
const STOP_X0 = BUTTON_X0;
|
const STOP_X0 = BUTTON_X0;
|
||||||
const STOP_Y0 = 211;
|
const STOP_Y0 = 211;
|
||||||
const STOP_X1 = BUTTON_X1;
|
|
||||||
const STOP_Y1 = STOP_Y0 + BUTTON_HEIGHT;
|
|
||||||
|
|
||||||
const INCTIME_X0 = BUTTON_X0;
|
const INCTIME_X0 = BUTTON_X0;
|
||||||
const INCTIME_Y0 = 188;
|
const INCTIME_Y0 = 188;
|
||||||
const INCTIME_X1 = BUTTON_X1;
|
|
||||||
const INCTIME_Y1 = INCTIME_Y0 + BUTTON_HEIGHT;
|
|
||||||
|
|
||||||
const DOOR_X0 = 26;
|
const DOOR_X0 = 26;
|
||||||
const DOOR_Y0 = 68;
|
const DOOR_Y0 = 68;
|
||||||
const DOOR_WIDTH = 353;
|
const DOOR_WIDTH = 353;
|
||||||
const DOOR_HEIGHT = 217;
|
const DOOR_HEIGHT = 217;
|
||||||
|
|
||||||
const ctx = new AudioContext();
|
|
||||||
|
|
||||||
function fetchAudioBuffer(url: string): Promise<AudioBuffer> {
|
|
||||||
return fetch(url).then(res => {
|
|
||||||
return res.arrayBuffer();
|
|
||||||
}).then(buf => {
|
|
||||||
return ctx.decodeAudioData(buf);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using the Web Audio API was the only way I could get the 'microwave running' sound to properly play gapless in Chrome.
|
|
||||||
function playAudioBufer(buf: AudioBuffer, loop: boolean, speed: number): AudioCallbacks {
|
|
||||||
const src = ctx.createBufferSource();
|
|
||||||
src.buffer = buf;
|
|
||||||
|
|
||||||
const lowPass = ctx.createBiquadFilter();
|
|
||||||
lowPass.type = 'highpass';
|
|
||||||
lowPass.frequency.value = 20; // let's not blow up anyone's speakers
|
|
||||||
|
|
||||||
src.connect(lowPass);
|
|
||||||
lowPass.connect(ctx.destination);
|
|
||||||
|
|
||||||
if (loop) src.loop = true;
|
|
||||||
src.start();
|
|
||||||
return [
|
|
||||||
() => src.stop(),
|
|
||||||
(speed: number) => {
|
|
||||||
// instead of setting playback rate to 0 (which browsers seem to handle as if playback rate was set to 1, we just set it to a very small value, making it "almost paused")
|
|
||||||
// combined with the lowpass filter above, this should produce any audible results.
|
|
||||||
src.playbackRate.value = (speed===0) ? 0.00001 : speed;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
type AudioCallbacks = [
|
|
||||||
() => void,
|
|
||||||
(speed: number) => void,
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Magnetron({state: {timeDisplay, bell, magnetron}, speed, callbacks}: MicrowaveProps) {
|
export function Magnetron({state: {timeDisplay, bell, magnetron}, speed, callbacks}: MicrowaveProps) {
|
||||||
const [door, setDoor] = useState<DoorState>("closed");
|
const [door, setDoor] = useState<DoorState>("closed");
|
||||||
|
|
||||||
const [soundsPlaying, setSoundsPlaying] = useState<AudioCallbacks[]>([]);
|
const [playSound, preloadAudio] = useAudioContext(speed);
|
||||||
const [bufRunningPromise] = useState(() => fetchAudioBuffer(sndRunning));
|
|
||||||
const [bufBellPromise] = useState(() => fetchAudioBuffer(sndBell));
|
|
||||||
|
|
||||||
// a bit hacky: when the bell-state changes to true, we play the bell sound...
|
|
||||||
useEffect(() => {
|
|
||||||
if (bell) {
|
|
||||||
bufBellPromise.then(buf => {
|
|
||||||
const cbs = playAudioBufer(buf, false, speed);
|
|
||||||
setSoundsPlaying(sounds => [...sounds, cbs]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [bell]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (magnetron === "on") {
|
|
||||||
const stop = bufRunningPromise.then(buf => {
|
|
||||||
const cbs = playAudioBufer(buf, true, speed);
|
|
||||||
setSoundsPlaying(sounds => [...sounds, cbs]);
|
|
||||||
return () => {
|
|
||||||
cbs[0]();
|
|
||||||
setSoundsPlaying(sounds => sounds.filter(cbs_ => cbs_ !== cbs));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => stop.then(stop => {
|
|
||||||
stop();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return () => {};
|
|
||||||
}, [magnetron])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
soundsPlaying.forEach(([_, setSpeed]) => setSpeed(speed));
|
|
||||||
}, [soundsPlaying, speed])
|
|
||||||
|
|
||||||
// preload(imgSmallClosedOff, {as: "image"});
|
// preload(imgSmallClosedOff, {as: "image"});
|
||||||
preload(imgSmallClosedOn, {as: "image"});
|
preload(imgSmallClosedOn, {as: "image"});
|
||||||
preload(imgSmallOpenedOff, {as: "image"});
|
preload(imgSmallOpenedOff, {as: "image"});
|
||||||
preload(imgSmallOpenedOn, {as: "image"});
|
preload(imgSmallOpenedOn, {as: "image"});
|
||||||
|
|
||||||
|
preloadAudio(sndRunning);
|
||||||
|
preloadAudio(sndBell);
|
||||||
|
|
||||||
|
// a bit hacky: when the bell-state changes to true, we play the bell sound...
|
||||||
|
useEffect(() => {
|
||||||
|
if (bell) {
|
||||||
|
playSound(sndBell, false);
|
||||||
|
}
|
||||||
|
}, [bell]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (magnetron === "on") {
|
||||||
|
const stopSoundRunning = playSound(sndRunning, true);
|
||||||
|
return () => stopSoundRunning();
|
||||||
|
}
|
||||||
|
return () => {};
|
||||||
|
}, [magnetron])
|
||||||
|
|
||||||
|
|
||||||
const openDoor = () => {
|
const openDoor = () => {
|
||||||
setDoor("open");
|
setDoor("open");
|
||||||
callbacks.doorOpened();
|
callbacks.doorOpened();
|
||||||
|
|
|
||||||
58
src/App/Plant/Microwave/useAudioContext.ts
Normal file
58
src/App/Plant/Microwave/useAudioContext.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { memoize } from "@/util/util";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
// I was trying to get the 'microwave running' sound to play gapless on Chrome, and the Web Audio API turned out to be the only thing that worked properly. It has some nice bonus features as well, such as setting the playback rate, and audio filters.
|
||||||
|
|
||||||
|
// The result is a simple Web Audio API wrapper for React:
|
||||||
|
|
||||||
|
export function useAudioContext(speed: number) {
|
||||||
|
const [{ctx,hipass}] = useState(() => {
|
||||||
|
const ctx = new AudioContext();
|
||||||
|
const hipass = ctx.createBiquadFilter();
|
||||||
|
hipass.type = 'highpass';
|
||||||
|
hipass.frequency.value = 20; // let's not blow up anyone's speakers
|
||||||
|
hipass.connect(ctx.destination);
|
||||||
|
return {
|
||||||
|
ctx,
|
||||||
|
hipass,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [sounds, setSounds] = useState<AudioBufferSourceNode[]>([]);
|
||||||
|
|
||||||
|
const url2AudioBuf: (url:string) => Promise<AudioBuffer> = memoize((url: string) => {
|
||||||
|
return fetch(url)
|
||||||
|
.then(res => res.arrayBuffer())
|
||||||
|
.then(buf => ctx.decodeAudioData(buf));
|
||||||
|
});
|
||||||
|
|
||||||
|
function play(url: string, loop: boolean) {
|
||||||
|
const srcPromise = url2AudioBuf(url)
|
||||||
|
.then(audioBuf => {
|
||||||
|
const src = ctx.createBufferSource();
|
||||||
|
src.buffer = audioBuf;
|
||||||
|
src.connect(hipass);
|
||||||
|
src.playbackRate.value = speed;
|
||||||
|
src.loop = loop;
|
||||||
|
src.start();
|
||||||
|
setSounds(sounds => [...sounds, src]);
|
||||||
|
return src;
|
||||||
|
});
|
||||||
|
// return callback to stop playing
|
||||||
|
return () => srcPromise.then(src => src.stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (speed !== 0) {
|
||||||
|
sounds.forEach(src => {
|
||||||
|
src.playbackRate.value = speed;
|
||||||
|
});
|
||||||
|
ctx.resume();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ctx.suspend();
|
||||||
|
}
|
||||||
|
}, [speed]);
|
||||||
|
|
||||||
|
return [play, url2AudioBuf] as [(url: string, loop: boolean) => ()=>void, (url:string)=>void];
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue