use-audio-hooks
v1.0.2
Published
This package expose `useAudioPlayer` and `useAudioRecorder`, as well as several components: `Audio`, `AudioRecorder`, and `AudioPlayer` that use those hooks, as well as the underlying styled components that compose those components.
Downloads
1
Readme
Use Audio Hooks
This package expose useAudioPlayer
and useAudioRecorder
, as well as several components: Audio
, AudioRecorder
, and AudioPlayer
that use those hooks, as well as the underlying styled components that compose those components.
useAudioPlayer
function useAudioPlayer(config: PlayerConfigInterface) {
const {
audioFile,
canvas,
handleStartPlaying,
handlePlaying,
handleFinishPlaying,
} = config;
const [audio, setAudio] = useState<HTMLAudioElement | null>(null);
const [audioCtx, setAudioCtx] = useState<AudioContext | null>(null);
const [
audioSrcNode,
setAudioSrcNode,
] = useState<MediaElementAudioSourceNode | null>(null);
const [duration, setDuration] = useState();
const [clickedTime, setClickedTime] = useState();
const [curTime, setCurTime] = useState();
const [src, setSrc] = useState<string | null>(null);
const [playing, setPlaying] = useState(false);
const play = () => {
if (!audio) return;
setPlaying(true);
audio.play();
handleStartPlaying && handleStartPlaying();
};
const pause = () => {
if (!audio) return;
setPlaying(false);
audio.pause();
};
const togglePlaying = () => {
if (!audio) return;
if (audio.paused) {
play();
} else {
if (!audio.ended && audio.readyState > 2) {
pause();
}
}
};
useEffect(() => {
if (!audio) return;
// state setters wrappers
const setAudioData = () => {
setDuration(audio.duration);
setCurTime(audio.currentTime);
};
const setAudioTime = () => {
setCurTime(audio.currentTime);
handlePlaying && handlePlaying(audio.currentTime);
};
// DOM listeners: update React state on DOM events
audio.addEventListener('loadeddata', setAudioData);
audio.addEventListener('ended', () => {
setPlaying(false);
handleFinishPlaying && handleFinishPlaying();
});
audio.addEventListener('timeupdate', setAudioTime);
if (clickedTime && clickedTime !== curTime) {
audio.currentTime = clickedTime;
setClickedTime(null);
}
// effect cleanup
return () => {
audio.removeEventListener('loadeddata', setAudioData);
audio.removeEventListener('ended', () => {
setPlaying(false);
handleFinishPlaying && handleFinishPlaying();
});
audio.removeEventListener('timeupdate', setAudioTime);
};
});
useEffect(() => {
const newSrc = URL.createObjectURL(audioFile.file);
setSrc(newSrc);
setPlaying(false);
return () => {
URL.revokeObjectURL(newSrc);
if (audioSrcNode && (audioCtx && audioCtx.state !== 'closed')) {
try {
console.log('trying');
audioSrcNode.disconnect();
setAudioSrcNode(null);
audioCtx.close();
setAudioCtx(null);
} catch (error) {
console.log(error);
}
}
};
}, [audioFile]);
useEffect(() => {
if (!src) return;
const newAudio = new Audio(src);
setAudio(newAudio);
}, [src]);
useEffect(() => {
if (!audio) return;
const newContext = new (window.AudioContext ||
(window as any).webkitAudioContext)();
setAudioCtx(newContext);
return () => {
if (audioCtx && audioCtx.state !== 'closed') {
try {
audioCtx.close();
setAudioCtx(null);
} catch (error) {
console.log(error);
}
}
};
}, [audio]);
useEffect(() => {
if (!audio || !audioCtx) return;
const newSource = audioCtx.createMediaElementSource(audio);
setAudioSrcNode(newSource);
return () => {
if (
audioSrcNode &&
audioSrcNode.context &&
audioSrcNode.context.state === 'closed'
) {
audioSrcNode && audioSrcNode.disconnect();
}
};
}, [audioCtx]);
useEffect(() => {
if (!canvas || !audio || !audioCtx || !audioSrcNode) return;
visualization(canvas, audioCtx, audioSrcNode, config.visualizer, true);
});
return {
curTime,
duration,
playing,
togglePlaying,
setClickedTime,
};
}
useAudioRecorder
function useAudioRecorder(config: RecorderConfigInterface) {
const {
canvas,
handleStartRecording,
handleRecording,
setFiles,
handleFinishRecording,
} = config;
const [audioCtx, setAudioCtx] = useState<AudioContext | null>(null);
const [
audioInput,
setAudioInput,
] = useState<MediaStreamAudioSourceNode | null>(null);
const [audioNode, setAudioNode] = useState<ScriptProcessorNode | null>(null);
const [bufferSize /* setBufferSize */] = useState(4096);
const [, /* blob */ setBlob] = useState<Blob | null>(null);
const [duration, setDuration] = useState<number>(0);
const [microphone, setMicrophone] = useState<MediaStream | null>(null);
const [recordedData, setRecordedData] = useState<Float32Array[]>([]);
const [recording, setRecording] = useState(false);
const [recordingLength, setRecordingLength] = useState(0);
const [sampleRate, setSampleRate] = useState<number | null>(null);
const [startDate, setStartDate] = useState<Date | null>(null);
const toggleRecording = () => {
recording ? stop() : start();
};
const processInWebWorker = (_function: any) => {
const workerURL = URL.createObjectURL(
new Blob(
[
_function.toString(),
`;this.onmessage = function(e) {${_function.name}(e.data);}`,
],
{
type: 'application/javascript',
}
)
);
const worker = new Worker(workerURL) as WorkerWithUrl;
worker.workerURL = workerURL;
return worker;
};
function exportWav(config: any, callback: any) {
function inlineWebWorker(config: any, cb: any) {
function joinBuffers(channelBuffer: any, count: any) {
const result = new Float64Array(count);
let offset = 0;
const lng = channelBuffer.length;
for (let i = 0; i < lng; i++) {
const buffer = channelBuffer[i];
result.set(buffer, offset);
offset += buffer.length;
}
return result;
}
function writeUTFBytes(view: any, offset: any, string: string) {
const lng = string.length;
for (let i = 0; i < lng; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
const firstData = config.data.slice(0);
const data = joinBuffers(firstData, config.recordingLength);
const dataLength = data.length;
// create wav file
const buffer = new ArrayBuffer(44 + dataLength * 2);
const view = new DataView(buffer);
writeUTFBytes(view, 0, 'RIFF'); // RIFF chunk descriptor/identifier
view.setUint32(4, 44 + dataLength * 2, true); // RIFF chunk length
writeUTFBytes(view, 8, 'WAVE'); // RIFF type
writeUTFBytes(view, 12, 'fmt '); // format chunk identifier, FMT sub-chunk
view.setUint32(16, 16, true); // format chunk length
view.setUint16(20, 1, true); // sample format (raw)
view.setUint16(22, 1, true); // mono (1 channel)
view.setUint32(24, config.sampleRate, true); // sample rate
view.setUint32(28, config.sampleRate * 2, true); // byte rate (sample rate * block align)
view.setUint16(32, 2, true); // block align (channel count * bytes per sample)
view.setUint16(34, 16, true); // bits per sample
writeUTFBytes(view, 36, 'data'); // data sub-chunk identifier
view.setUint32(40, dataLength * 2, true); // data chunk length
// write the PCM samples
let index = 44;
for (let i = 0; i < dataLength; i++) {
view.setInt16(index, data[i] * 0x7fff, true);
index += 2;
}
if (cb) {
return cb({
buffer: buffer,
view: view,
});
}
self.postMessage({
buffer: buffer,
view: view,
});
}
const webWorker = processInWebWorker(inlineWebWorker);
webWorker.onmessage = function messageFn(event) {
callback(event.data.buffer, event.data.view);
// release memory
URL.revokeObjectURL(webWorker.workerURL);
};
webWorker.postMessage(config);
}
const stopRecording = (callback: (blob: Blob) => void) => {
setRecording(false);
if (!audioInput || !audioNode || !audioCtx || !microphone) return;
microphone.getTracks().forEach(t => t.stop());
audioInput.disconnect();
audioNode.disconnect();
audioCtx.close();
setMicrophone(null);
setAudioInput(null);
setAudioCtx(null);
setAudioNode(null);
setStartDate(null);
setSampleRate(null);
exportWav(
{
sampleRate,
recordingLength,
data: recordedData,
},
function totallyNecessaryName(buffer: Buffer, view: DataView) {
console.log(buffer);
const newBlob = new Blob([view], { type: 'audio/wav' });
setBlob(newBlob);
callback && callback(newBlob);
}
);
};
const onAudioProcess = (e: AudioProcessingEvent) => {
if (!recording) {
return;
}
const float = new Float32Array(e.inputBuffer.getChannelData(0));
const newDuration =
new Date().getTime() - ((startDate && startDate.getTime()) || 0);
setRecordedData(prev => [...prev, float]);
setRecordingLength(prev => prev + bufferSize);
setDuration(newDuration);
handleRecording && handleRecording(newDuration);
};
const onMicrophoneError = (e: Error) => {
console.log(e);
};
const onMicrophoneCaptured = (microphone: MediaStream) => {
setRecording(true);
if (audioCtx) {
const newAudioInput = audioCtx.createMediaStreamSource(microphone);
const newDate = new Date();
const audioCtxSampleRate = audioCtx.sampleRate;
setMicrophone(microphone);
setAudioInput(newAudioInput);
setStartDate(newDate);
setSampleRate(audioCtxSampleRate);
}
};
useEffect(() => {
if (!canvas || !audioCtx || !audioInput) return;
visualization(canvas, audioCtx, audioInput, config.visualizer, false);
});
useEffect(() => {
if (duration > 30000) {
stop();
}
}, [duration]);
useEffect(() => {
if (audioInput && audioNode && audioCtx && startDate) {
audioInput.connect(audioNode);
audioNode.addEventListener('audioprocess', onAudioProcess);
}
return () => {
audioNode &&
audioNode.removeEventListener('audioprocess', onAudioProcess);
};
}, [audioInput, startDate]);
useEffect(() => {
if (audioNode && (audioCtx && audioCtx.state !== 'closed')) {
audioNode.connect(audioCtx.destination);
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(onMicrophoneCaptured)
.catch(onMicrophoneError);
}
return () => {
if (audioNode && audioCtx && audioCtx.state === 'closed') {
audioNode.disconnect();
setAudioNode(null);
}
};
}, [audioNode]);
useEffect(() => {
if (audioCtx && audioCtx.createScriptProcessor) {
const newNode: ScriptProcessorNode = audioCtx.createScriptProcessor(
bufferSize,
1,
1
) as ScriptProcessorNode;
setAudioNode(newNode);
} else if (!audioCtx) {
console.log('awaiting context');
} else {
const error = { message: 'WebAudio not supported!' };
console.log(error);
}
return () => {
if (audioCtx && audioCtx.state !== 'closed') {
try {
audioCtx.close();
setAudioCtx(null);
} catch (error) {
console.log(error);
}
}
};
}, [audioCtx]);
const start = () => {
const AudioContext = new (window.AudioContext ||
(window as any).webkitAudioContext)();
setAudioCtx(AudioContext);
setRecordedData([]);
setRecordingLength(0);
handleStartRecording && handleStartRecording();
};
const stop = () => {
stopRecording(newBlob => {
setBlob(newBlob);
const fileId = Date.now();
const newFile = new File([newBlob], `file-${fileId}.mp3`, {
type: 'audio/mp3',
});
const newFileWithId = { id: fileId, file: newFile };
setFiles(prevState => [newFileWithId, ...prevState]);
handleFinishRecording && handleFinishRecording(newBlob);
setDuration(0);
});
};
return { duration, toggleRecording, recording };
}
Audio Component Example
const Audio: FC<AudioProps> = props => {
const [files, setFiles] = useState<FileInterface[]>([]);
const [audioFile, setAudioFile] = useState<FileInterface | null>(null);
const submitField = (values: { [key: string]: string }) => {
const key = Object.keys(values)[0];
const id = parseInt(key.split('audio_input_')[1]);
const fileToUpdate = files.filter(file => file.id === id && file)[0];
Object.defineProperty(fileToUpdate.file, 'name', {
writable: true,
value: values[key],
});
};
const handleDelete = (id: number) => {
if (audioFile && audioFile.id === id) setAudioFile(null);
setFiles(prevState => prevState.filter(file => file.id !== id && file));
props.handleDelete && props.handleDelete(id);
};
const handleSelect = (id: number) => {
if (id === (audioFile && audioFile.id)) return setAudioFile(null);
const newAudioFile = files.filter(file => file.id === id)[0];
setAudioFile(newAudioFile);
props.handleSelect && props.handleSelect(id);
};
const handleUpload = (id?: number) => {
if (!props.handleUpload) return;
const uploadedFile = props.handleUpload();
if (id) {
const newFiles = files.filter(file => file.id !== id && file);
setFiles([uploadedFile, ...newFiles]);
setAudioFile(uploadedFile);
} else {
setFiles(prevState => [uploadedFile, ...prevState]);
setAudioFile(uploadedFile);
}
};
return (
<S.AudioContainer>
{audioFile ? (
<AudioPlayer config={props.playerConfig} audioFile={audioFile} />
) : (
<AudioRecorder config={props.recorderConfig} setFiles={setFiles} />
)}
{props.handleUpload && (
<S.IconButton
onClick={e => {
e.preventDefault();
handleUpload();
}}
>
<Upload />
</S.IconButton>
)}
{files.length > 0 && (
<FileList
files={files}
handleDelete={handleDelete}
handleSelect={handleSelect}
handleUpload={handleUpload}
selectedFile={audioFile && audioFile.id}
submitField={submitField}
/>
)}
</S.AudioContainer>
);
};
AudioPlayer Component Example
const AudioPlayer: FC<AudioPlayerProps> = ({ audioFile, ...props }) => {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const config: PlayerConfigInterface = {
audioFile,
canvas: canvasRef && canvasRef.current,
container: containerRef && containerRef.current,
handleStartPlaying: () => console.log('starting'),
handlePlaying: seconds => console.log('playing => ', seconds),
handleFinishPlaying: () => console.log('stopping'),
visualizer: { enabled: true, lineWidth: 5 },
};
const {
curTime,
duration,
playing,
togglePlaying,
} = useAudioPlayer({
...config,
...props.config,
});
return (
<S.PlayerRecorderContainer>
<S.VisualizerContainer>
<S.RecordButton
onClick={e => {
e.preventDefault();
togglePlaying();
}}
>
<PlayPauseIcon playing={playing} />
</S.RecordButton>
<S.Visualizer ref={canvasRef} />
</S.VisualizerContainer>
<S.Duration>
{!duration
? '0:00 / 0:00'
: `${formatSecs(curTime)} / ${formatSecs(duration)}`}
</S.Duration>
</S.PlayerRecorderContainer>
);
};
AudioRecorder Component Example
const AudioRecorder: FC<AudioRecorderProps> = ({
setFiles,
...props
}) => {
if (!props) return <div />;
const canvasRef = useRef<HTMLCanvasElement>(null);
const config: RecorderConfigInterface = {
canvas: canvasRef && canvasRef.current,
handleStartRecording: () => console.log('starting'),
handleFinishRecording: () => console.log('stopping'),
handleRecording: (miliseconds) => console.log('recording => ', miliseconds),
setFiles,
visualizer: { enabled: true, lineWidth: 5 },
};
const { duration, toggleRecording, recording } = useAudioRecorder({
...config,
...props.config,
});
return (
<S.PlayerRecorderContainer>
<S.VisualizerContainer>
<S.RecordButton
onClick={e => {
e.preventDefault();
toggleRecording();
}}
>
<Microphone height="2.8rem" />
</S.RecordButton>
<S.Visualizer ref={canvasRef} />
</S.VisualizerContainer>
<S.Duration id="duration">
{duration === 0 || !recording ? '0:00' : formatMilis(duration)}
</S.Duration>
</S.PlayerRecorderContainer>
);
};