audio messages are fully functional but buggy and ugly

This commit is contained in:
or-else
2022-03-14 20:50:24 -07:00
parent cc3b1190fd
commit 589b46a8c2
10 changed files with 269 additions and 186 deletions
+94 -79
View File
@@ -3,142 +3,142 @@
import React from 'react';
import { secondsToTime } from '../lib/strformat';
import { base64ToIntArray } from '../lib/blob-helpers';
// Make canvas bigger than the element size to reduce blurring.
const CANVAS_UPSCALING = 2.0;
// Thickness of a visualization bar.
const LINE_WIDTH = 4;
const LINE_WIDTH = 3 * CANVAS_UPSCALING;
// Spacing between two visualization bars.
const SPACING = 2;
// Duration represented by one visualization bar.
const MILLIS_PER_BAR = 100;
const SPACING = 2 * CANVAS_UPSCALING;
// Color of histogram bars
const BAR_COLOR = '#8fbed6';
// Scaling for visualization bars.
const BAR_SCALE = 96.0;
const BAR_COLOR_DARK = '#5f8ea5';
// Background color
const BKG_COLOR = '#eeeeee';
const BKG_COLOR = '#eee';
// Minimum number of amplitude bars to draw.
const MIN_PREVIEW_LENGTH = 16;
export default class AudioPlayer extends React.PureComponent {
constructor(props) {
super(props);
let preview = base64ToIntArray(this.props.preview);
if (Array.isArray(preview) && preview.length < MIN_PREVIEW_LENGTH) {
preview = null;
}
this.state = {
canPlay: false,
playing: false,
currentTime: '0:00',
duration: secondsToTime(this.props.duration)
duration: this.props.duration > 0 ? secondsToTime(this.props.duration / 1000) : '-:--',
preview: preview
};
this.calcPreviewBars = this.calcPreviewBars.bind(this);
this.visualize = this.visualize.bind(this);
this.initVisualizer = this.initVisualizer.bind(this);
this.handlePlay = this.handlePlay.bind(this);
this.handlePause = this.handlePause.bind(this);
this.handleSeek = this.handleSeek.bind(this);
this.handleError = this.handleError.bind(this);
this.audioPlayer = null;
this.durationMillis = 0;
this.startedOn = null;
this.viewBuffer = [];
this.canvasRef = React.createRef();
}
componentDidMount() {
this.canvasContext = this.canvasRef.current.getContext('2d');
this.canvasContext.lineCap = 'round';
// To reduce line blurring.
this.canvasContext.translate(0.5, 0.5);
if (this.props.src) {
this.audioPlayer = new Audio(this.props.src);
this.audioPlayer.addEventListener('loadedmetadata', _ => this.setState({duration: secondsToTime(this.audioPlayer.duration)}));
this.audioPlayer.addEventListener('canplay', _ => this.setState({canPlay: true}));
this.audioPlayer.addEventListener('timeupdate', _ => console.log(this.audioPlayer.currentTime));
this.audioPlayer.addEventListener('timeupdate', _ => this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime)}));
this.audioPlayer.addEventListener('ended', _ => this.setState({playing: false, currentTime: secondsToTime(0)}));
}
if (this.props.src) {
this.initVisualizer();
if (this.state.preview) {
// Force canvas aspect ration to match one of the element + upscale canvas to reduce blurring.
this.canvasRef.current.width = this.canvasRef.current.offsetWidth * CANVAS_UPSCALING;
this.canvasRef.current.height = this.canvasRef.current.offsetHeight * CANVAS_UPSCALING;
this.canvasContext = this.canvasRef.current.getContext('2d');
this.canvasContext.lineCap = 'round';
this.viewBuffer = this.calcPreviewBars(this.state.preview);
this.visualize();
}
}
// Draw amplitude of sound.
visualize() {
const width = this.canvasRef.current.width;
const height = this.canvasRef.current.height;
this.canvasContext.fillStyle = BKG_COLOR;
this.canvasContext.lineWidth = LINE_WIDTH;
this.canvasContext.strokeStyle = BAR_COLOR;
let prevBarCount = 0;
const drawFrame = () => {
if (!this.startedOn) {
return;
}
window.requestAnimationFrame(drawFrame);
const duration = this.durationMillis + (Date.now() - this.startedOn);
// Update record length timer.
this.setState({duration: secondsToTime(duration / 1000)});
// Check if record is too long.
if (duration > MAX_DURATION) {
this.mediaRecorder.pause();
this.durationMillis += Date.now() - this.startedOn;
this.startedOn = null;
this.setState({enabled: false, recording: false, duration: secondsToTime(this.durationMillis / 1000)});
}
// Draw histogram.
// Get current waveform and calculate its amplitude.
this.analyser.getByteTimeDomainData(pcmData);
let volume = 0.0;
for (const amplitude of pcmData) {
volume += (amplitude - 127) ** 2;
}
volume = Math.sqrt(volume/pcmData.length);
let barCount = (duration / MILLIS_PER_BAR) | 0;
// Shift of the histogram along x-axis to make scrolling smooth. No need to shift if recording is too short.
const dx = viewDuration > duration ? 0 :
(duration - MILLIS_PER_BAR * barCount) / MILLIS_PER_BAR * (LINE_WIDTH + SPACING);
if (prevBarCount != barCount) {
prevBarCount = barCount;
// Add new amplitude visualization bar.
this.viewBuffer.push(volume);
if (this.viewBuffer.length > viewLength) {
// Keep at most 'viewLength' amplitude bars.
this.viewBuffer.shift();
}
}
// Clear canvas.
this.canvasContext.fillStyle = BKG_COLOR;
this.canvasContext.strokeStyle = BAR_COLOR;
this.canvasContext.fillRect(0, 0, width, height);
// Draw amplitude bars.
// Start new drawing.
this.canvasContext.beginPath();
for (let i = 0; i < this.viewBuffer.length; i++) {
let x = i * (LINE_WIDTH + SPACING) - dx;
let y = this.viewBuffer[i] / BAR_SCALE * height;
this.canvasContext.moveTo(x, (height - y) * 0.5);
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
if (this.viewBuffer) {
window.requestAnimationFrame(drawFrame);
// Draw amplitude bars.
for (let i = 0; i < this.viewBuffer.length; i++) {
let x = 1 + i * (LINE_WIDTH + SPACING) + LINE_WIDTH * 0.5;
let y = this.viewBuffer[i] * height * 0.9;
this.canvasContext.moveTo(x, (height - y) * 0.5);
this.canvasContext.lineTo(x, height * 0.5 + y * 0.5);
}
}
// Actually draw the bars on canvas.
this.canvasContext.stroke();
if (this.props.duration) {
this.canvasContext.beginPath();
const x = Math.max(0, Math.min(this.audioPlayer.currentTime * 1000 / this.props.duration, 1)) * width;
this.canvasContext.arc(x + LINE_WIDTH * 0.5, height * 0.5, LINE_WIDTH * 2, 0, 2 * Math.PI);
this.canvasContext.fillStyle = BAR_COLOR_DARK;
this.canvasContext.fill();
}
}
drawFrame();
}
initVisualizer() {
console.log("Draw amplitude bars");
}
calcPreviewBars(original) {
// Quick and dirty downsampling of the original preview bars into a smaller (or equal) number of bars we can display here.
const barCount = Math.min(original.length, ((this.canvasRef.current.width - 1) / (LINE_WIDTH + SPACING)) | 0);
console.log("Bar count:", barCount, this.canvasRef.current.width, LINE_WIDTH + SPACING);
const factor = original.length / barCount;
let amps = [];
let max = -1;
for (let i=0;i<barCount;i++) {
let lo = (i * factor) | 0; // low bound;
let hi = ((i + 1) * factor) | 0; // high bound;
let amp = 0.0;
for (let j=lo; j<hi; j++) {
amp += original[j]
}
amps[i] = Math.max(0, amp / (hi - lo));
max = Math.max(amps[i], max);
}
handlePause(e) {
e.preventDefault();
this.setState({recording: false, duration: secondsToTime(this.durationMillis / 1000)});
if (max > 0) {
return amps.map(a => a / max);
}
return null;
}
handlePlay(e) {
@@ -148,8 +148,10 @@ export default class AudioPlayer extends React.PureComponent {
}
if (this.state.playing) {
this.audioPlayer.pause();
this.startedOn = null;
} else {
this.audioPlayer.play();
this.startedOn = Date.now();
}
this.setState({playing: !this.state.playing});
}
@@ -158,6 +160,16 @@ export default class AudioPlayer extends React.PureComponent {
console.log(err);
}
handleSeek(e) {
e.preventDefault();
if (e.target && this.props.duration) {
const rect = e.target.getBoundingClientRect();
const offset = (e.clientX - rect.left) / rect.width;
this.audioPlayer.currentTime = this.props.duration * offset / 1000;
this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime)});
}
}
render() {
const playClass = 'material-icons large' + (this.state.canPlay ? '' : ' disabled');
return (
@@ -166,7 +178,10 @@ export default class AudioPlayer extends React.PureComponent {
<i className={playClass}>{this.state.playing ? 'pause_circle' : 'play_circle'}</i>
</a>
<div>
<canvas className="visualiser" style={{border: '1px solid #666'}} ref={this.canvasRef} />
{this.state.preview ?
<canvas className="visualiser" ref={this.canvasRef} onClick={this.handleSeek} /> :
<div className="visualiser">unavailable</div>
}
<div className="timer">{this.state.currentTime}/{this.state.duration}</div>
</div>
</div>