diff options
Diffstat (limited to 'src/client/views/AudioWaveform.tsx')
-rw-r--r-- | src/client/views/AudioWaveform.tsx | 242 |
1 files changed, 164 insertions, 78 deletions
diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx index 8f3b7c2cd..8a3c3c319 100644 --- a/src/client/views/AudioWaveform.tsx +++ b/src/client/views/AudioWaveform.tsx @@ -1,6 +1,6 @@ import React = require("react"); import axios from "axios"; -import { action, computed } from "mobx"; +import { action, computed, reaction, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; import Waveform from "react-audio-waveform"; import { Doc } from "../../fields/Doc"; @@ -10,117 +10,203 @@ 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; + duration: number; // length of media clip + rawDuration: number; // length of underlying media data mediaPath: string; layoutDoc: Doc; - trimming: boolean; - PanelHeight: () => number; + clipStart: number; + clipEnd: number; + zoomFactor: number; + PanelHeight: number; + PanelWidth: number; } @observer export class AudioWaveform extends React.Component<AudioWaveformProps> { public static NUMBER_OF_BUCKETS = 100; - @computed get _waveHeight() { - return Math.max(50, this.props.PanelHeight()); + _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() { - const audioBuckets = Cast( - this.props.layoutDoc.audioBuckets, - listSpec("number"), - [] - ); - if (!audioBuckets.length) { - this.props.layoutDoc.audioBuckets = new List<number>([0, 0]); /// "lock" to prevent other views from computing the same data - setTimeout(this.createWaveformBuckets); - } + 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<number>(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 () => { + 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 decodedAudioData = buffer.getChannelData(0); + 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 / AudioWaveform.NUMBER_OF_BUCKETS + decodedAudioData.length / numBuckets ); const brange = Array.from(Array(bucketDataSize)); - this.props.layoutDoc.audioBuckets = new List<number>( - numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map( - (i: number) => - brange.reduce( - (p, x, j) => - Math.abs( - Math.max(p, decodedAudioData[i * bucketDataSize + j]) - ), - 0 - ) / 2 - ) + 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<number>(bucketList); }) ); } ); } - - @action - createTrimBuckets = () => { - const audioBuckets = Cast( - this.props.layoutDoc.audioBuckets, - listSpec("number"), - [] - ); - - const start = Math.floor( - (NumCast(this.props.layoutDoc.clipStart) / this.props.duration) * 100 - ); - const end = Math.floor( - (NumCast(this.props.layoutDoc.clipEnd) / this.props.duration) * 100 - ); - return audioBuckets.slice(start, end); - } - render() { - const audioBuckets = Cast( - this.props.layoutDoc.audioBuckets, - listSpec("number"), - [] - ); - return ( <div className="audioWaveform"> - {this.props.trimming || !this.props.layoutDoc.clipEnd ? ( - <Waveform - color={Colors.MEDIUM_BLUE} - height={this._waveHeight} - barWidth={0.1} - pos={this.props.duration} - duration={this.props.duration} - peaks={ - audioBuckets.length === AudioWaveform.NUMBER_OF_BUCKETS - ? audioBuckets - : undefined - } - progressColor={Colors.MEDIUM_BLUE} - /> - ) : ( - <Waveform - color={Colors.MEDIUM_BLUE} - height={this._waveHeight} - barWidth={0.1} - pos={this.props.duration} - duration={this.props.duration} - peaks={this.createTrimBuckets()} - progressColor={Colors.MEDIUM_BLUE} - /> - )} + {/* <Waveform + barWidth={2} + width={() => this.props.PanelWidth} + height={this.props.PanelHeight} + peaks={this.audioBuckets} + color={Colors.MEDIUM_BLUE} + /> */} + <Waveform + color={Colors.MEDIUM_BLUE_ALT} + height={this.waveHeight} + barWidth={200 / this.audioBuckets.length} + pos={this.props.duration} + duration={this.props.duration} + peaks={this.audioBuckets} + progressColor={Colors.MEDIUM_BLUE_ALT} + /> </div> ); } } + + +export interface WaveformProps { + barWidth: number; + width: () => number; + height: () => number; + peaks: number[]; + color: string; +} + +// @observer +// export class Waveform extends React.Component<WaveformProps> { +// 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 ? ( +// <canvas +// ref={(instance) => { +// this._canvas = instance; +// }} +// /> +// ) : null +// } +// }
\ No newline at end of file |