import React = require("react"); import axios from "axios"; import { action, computed, reaction, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; import Waveform from "react-audio-waveform"; import { Doc } from "../../fields/Doc"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast, NumCast } from "../../fields/Types"; import { numberRange } from "../../Utils"; import "./AudioWaveform.scss"; import { Colors } from "./global/globalEnums"; import Color = require("color"); export interface AudioWaveformProps { duration: number; // length of media clip rawDuration: number; // length of underlying media data mediaPath: string; layoutDoc: Doc; clipStart: number; clipEnd: number; zoomFactor: number; PanelHeight: number; PanelWidth: number; } @observer export class AudioWaveform extends React.Component { public static NUMBER_OF_BUCKETS = 100; _disposer: IReactionDisposer | undefined; @computed get waveHeight() { return Math.max(50, this.props.PanelHeight); } @computed get clipStart() { return this.props.clipStart; } @computed get clipEnd() { return this.props.clipEnd; } @computed get zoomFactor() { return this.props.zoomFactor; } @computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec("number"), []); } audioBucketField = (start: number, end: number, zoomFactor: number) => "audioBuckets/" + "/" + start.toFixed(2).replace(".", "_") + "/" + end.toFixed(2).replace(".", "_") + "/" + (zoomFactor * 10); componentWillUnmount() { this._disposer?.(); } componentDidMount() { this._disposer = reaction(() => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor), zoomFactor: this.props.zoomFactor }), ({ clipStart, clipEnd, fieldKey, zoomFactor }) => { if (!this.props.layoutDoc[fieldKey]) { // setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nerly the same time. const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration, 1)], listSpec("number")); this.props.layoutDoc[fieldKey] = waveform && new List(waveform.slice(clipStart / this.props.rawDuration * waveform.length, clipEnd / this.props.rawDuration * waveform.length)); setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd, zoomFactor)); } }, { fireImmediately: true }); } // decodes the audio file into peaks for generating the waveform createWaveformBuckets = async (fieldKey: string, clipStart: number, clipEnd: number, zoomFactor: number) => { axios({ url: this.props.mediaPath, responseType: "arraybuffer" }).then( (response) => { const context = new window.AudioContext(); context.decodeAudioData( response.data, action((buffer) => { const rawDecodedAudioData = buffer.getChannelData(0); const startInd = clipStart / this.props.rawDuration; const endInd = clipEnd / this.props.rawDuration; const decodedAudioData = rawDecodedAudioData.slice(Math.floor(startInd * rawDecodedAudioData.length), Math.floor(endInd * rawDecodedAudioData.length)); const numBuckets = Math.floor(AudioWaveform.NUMBER_OF_BUCKETS * zoomFactor); const bucketDataSize = Math.floor( decodedAudioData.length / numBuckets ); const brange = Array.from(Array(bucketDataSize)); const bucketList = numberRange(numBuckets).map( (i: number) => brange.reduce( (p, x, j) => Math.abs( Math.max(p, decodedAudioData[i * bucketDataSize + j]) ), 0 ) / 2 ); this.props.layoutDoc[fieldKey] = new List(bucketList); }) ); } ); } render() { return (
{/* this.props.PanelWidth} height={this.props.PanelHeight} peaks={this.audioBuckets} color={Colors.MEDIUM_BLUE} /> */}
); } } export interface WaveformProps { barWidth: number; width: () => number; height: () => number; peaks: number[]; color: string; } // @observer // export class Waveform extends React.Component { // private _canvas: HTMLCanvasElement | null = null; // get width() { return this.props.width(); } // get height() { return this.props.height(); } // get peaks() { return this.props.peaks; } // componentDidMount() { // this.drawBars(); // } // drawBars() { // const waveCanvasCtx = this._canvas?.getContext("2d"); // if (waveCanvasCtx) { // const pixelRatio = window.devicePixelRatio; // console.log(pixelRatio); // const displayWidth = Math.round(this.width); // const displayHeight = Math.round(this.height); // waveCanvasCtx.canvas.width = this.width; // waveCanvasCtx.canvas.height = this.height; // waveCanvasCtx.canvas.style.width = `${displayWidth}px`; // waveCanvasCtx.canvas.style.height = `${displayHeight}px`; // waveCanvasCtx.clearRect(0, 0, this.width, this.height); // const hasMinVals = [].some.call(this.peaks, (val) => val < 0); // let filteredPeaks = this.peaks; // if (hasMinVals) { // // If the first value is negative, add 1 to the filtered indices // let indexOffset = 0; // if (this.peaks[0] < 0) { // indexOffset = 1; // } // filteredPeaks = [].filter.call( // this.peaks, // (_, index) => (index + indexOffset) % 2 == 0 // ); // } // const $ = 0.5; // const height = this.height; // const offsetY = 0; // const halfH = this.height / 2; // const length = filteredPeaks.length; // const bar = this.props.barWidth; // const gap = 2; // const step = bar + gap; // let absmax = 1; // absmax = this.absMax(filteredPeaks); // const scale = length / this.width; // waveCanvasCtx.fillStyle = this.props.color; // for (let i = 0; i < this.width; i += step) { // let h = Math.round(filteredPeaks[Math.floor(i * scale)] / absmax * halfH) // if (h === 0) { // h = 1 // } // waveCanvasCtx.fillRect(i + $, halfH - h + offsetY, bar + $, h * 2) // } // } // } // absMax = (values: number[]) => { // let max = -Infinity; // for (const i in values) { // const num = Math.abs(values[i]); // if (num > max) { // max = num; // } // } // return max; // } // render() { // return this.props.peaks ? ( // { // this._canvas = instance; // }} // /> // ) : null // } // }