1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
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";
export interface AudioWaveformProps {
duration: number; // length of media clip
rawDuration: number; // length of underlying media data
mediaPath: string;
layoutDoc: Doc;
trimming: boolean;
clipStart: number;
clipEnd: number;
PanelHeight: () => number;
}
@observer
export class AudioWaveform extends React.Component<AudioWaveformProps> {
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; }
audioBucketField = (start: number, end: number) => { return "audioBuckets-" + start.toFixed(2) + "-" + end.toFixed(2); }
@computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd)], listSpec("number"), []); }
componentWillUnmount() {
this._disposer?.();
}
componentDidMount() {
this._disposer = reaction(() => [this.clipStart, this.clipEnd, this.audioBuckets.length],
(range) => {
if (range[2] !== AudioWaveform.NUMBER_OF_BUCKETS) {
if (!this.props.layoutDoc[this.audioBucketField(range[0], range[1])]) {
// setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nerly the same time.
this.props.layoutDoc[this.audioBucketField(range[0], range[1])] = new List<number>(numberRange(AudioWaveform.NUMBER_OF_BUCKETS));
setTimeout(this.createWaveformBuckets);
}
}
}, { fireImmediately: true });
}
// decodes the audio file into peaks for generating the waveform
createWaveformBuckets = async () => {
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 = this.clipStart / this.props.rawDuration;
const endInd = this.clipEnd / this.props.rawDuration;
const decodedAudioData = rawDecodedAudioData.slice(Math.floor(startInd * rawDecodedAudioData.length), Math.floor(endInd * rawDecodedAudioData.length));
const bucketDataSize = Math.floor(
decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS
);
const brange = Array.from(Array(bucketDataSize));
const bucketList = numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map(
(i: number) =>
brange.reduce(
(p, x, j) =>
Math.abs(
Math.max(p, decodedAudioData[i * bucketDataSize + j])
),
0
) / 2
);
this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd)] = new List<number>(bucketList);
})
);
}
);
}
render() {
return (
<div className="audioWaveform">
<Waveform
color={Colors.MEDIUM_BLUE}
height={this._waveHeight}
barWidth={0.1}
pos={this.props.duration}
duration={this.props.duration}
peaks={this.audioBuckets}
progressColor={Colors.MEDIUM_BLUE}
/>
</div>
);
}
}
|