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
103
104
105
106
107
108
109
|
import React = require('react');
import axios from 'axios';
import { action, computed, IReactionDisposer, reaction } from 'mobx';
import { observer } from 'mobx-react';
import Waveform from 'react-audio-waveform';
import { Doc, NumListCast } from '../../fields/Doc';
import { List } from '../../fields/List';
import { listSpec } from '../../fields/Schema';
import { Cast } from '../../fields/Types';
import { numberRange } from '../../Utils';
import './AudioWaveform.scss';
import { Colors } from './global/globalEnums';
/**
* AudioWaveform
*
* Used in CollectionStackedTimeline to render a canvas with a visual of an audio waveform for AudioBox and VideoBox documents.
* Uses react-audio-waveform package.
* Bins the audio data into audioBuckets which are passed to package to render the lines.
* Calculates new buckets each time a new zoom factor or new set of trim bounds is created and stores it in a field on the layout doc with a title indicating the bounds and zoom for that list (see audioBucketField)
*/
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;
fieldKey: string;
}
@observer
export class AudioWaveform extends React.Component<AudioWaveformProps> {
public static NUMBER_OF_BUCKETS = 100; // number of buckets data is divided into to draw waveform lines
_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 NumListCast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)]);
}
audioBucketField = (start: number, end: number, zoomFactor: number) => this.props.fieldKey + '_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<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 (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<number>(bucketList);
})
);
});
};
render() {
return (
<div className="audioWaveform">
<Waveform color={Colors.MEDIUM_BLUE_ALT} height={this.waveHeight} barWidth={200 / this.audioBuckets.length} pos={this.props.duration} duration={this.props.duration} peaks={Array.from(this.audioBuckets)} progressColor={Colors.MEDIUM_BLUE_ALT} />
</div>
);
}
}
|