Travis Ralston 2021-06-28 21:00:36 -06:00
parent 964a97d59e
commit d6cf2346fe
5 changed files with 68 additions and 81 deletions

View File

@ -27,9 +27,14 @@ limitations under the License.
display: flex;
align-items: center;
contain: content;
.mx_Waveform {
.mx_Waveform_bar {
background-color: $voice-record-waveform-incomplete-fg-color;
height: 100%;
/* Variable set by a JS component */
transform: scaleY(max(0.05, var(--barHeight)));
&.mx_Waveform_bar_100pct {
// Small animation to remove the mechanical feel of progress

View File

@ -18,6 +18,7 @@ import React from "react";
import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
import { MarkedExecution } from "../../../utils/MarkedExecution";
interface IProps {
recorder: VoiceRecording;
@ -32,16 +33,31 @@ interface IState {
*/
@replaceableComponent("views.audio_messages.LiveRecordingClock")
export default class LiveRecordingClock extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);
private seconds = 0;
private scheduledUpdate = new MarkedExecution(
() => this.updateClock(),
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
);
this.state = {seconds: 0};
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
constructor(props) {
super(props);
this.state = {
seconds: 0,
};
}
private onRecordingUpdate = (update: IRecordingUpdate) => {
this.setState({seconds: update.timeSeconds});
};
componentDidMount() {
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
this.seconds = update.timeSeconds;
this.scheduledUpdate.mark();
});
}
private updateClock() {
this.setState({
seconds: this.seconds,
});
}
public render() {
return <Clock seconds={this.state.seconds} />;

View File

@ -20,13 +20,14 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
import { percentageOf } from "../../../utils/numbers";
import Waveform from "./Waveform";
import { MarkedExecution } from "../../../utils/MarkedExecution";
interface IProps {
recorder: VoiceRecording;
}
interface IState {
heights: number[];
waveform: number[];
}
/**
@ -34,27 +35,35 @@ interface IState {
*/
@replaceableComponent("views.audio_messages.LiveRecordingWaveform")
export default class LiveRecordingWaveform extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);
this.state = {heights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES)};
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
}
private onRecordingUpdate = (update: IRecordingUpdate) => {
// The waveform and the downsample target are pretty close, so we should be fine to
// do this, despite the docs on arrayFastResample.
const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
this.setState({
// The incoming data is between zero and one, but typically even screaming into a
// microphone won't send you over 0.6, so we artificially adjust the gain for the
// waveform. This results in a slightly more cinematic/animated waveform for the
// user.
heights: bars.map(b => percentageOf(b, 0, 0.50)),
});
public static defaultProps = {
progress: 1,
};
private waveform: number[] = [];
private scheduledUpdate = new MarkedExecution(
() => this.updateWaveform(),
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
);
constructor(props) {
super(props);
this.state = {
waveform: [],
};
}
componentDidMount() {
this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
this.waveform = update.waveform;
this.scheduledUpdate.mark();
});
}
private updateWaveform() {
this.setState({ waveform: this.waveform });
}
public render() {
return <Waveform relHeights={this.state.heights} />;
return <Waveform relHeights={this.state.waveform} />;
}
}

View File

@ -17,6 +17,11 @@ limitations under the License.
import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import classNames from "classnames";
import { CSSProperties } from "react";
interface WaveformCSSProperties extends CSSProperties {
'--barHeight': number;
}
interface IProps {
relHeights: number[]; // relative heights (0-1)
@ -40,10 +45,6 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
progress: 1,
};
public constructor(props) {
super(props);
}
public render() {
return <div className='mx_Waveform'>
{this.props.relHeights.map((h, i) => {
@ -53,7 +54,9 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
'mx_Waveform_bar': true,
'mx_Waveform_bar_100pct': isCompleteBar,
});
return <span key={i} style={{height: (h * 100) + '%'}} className={classes} />;
return <span key={i} style={{
"--barHeight": h,
} as WaveformCSSProperties} className={classes} />;
})}
</div>;
}

View File

@ -18,22 +18,18 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { _t } from "../../../languageHandler";
import React, {ReactNode} from "react";
import {
IRecordingUpdate,
RECORDING_PLAYBACK_SAMPLES,
RecordingState,
VoiceRecording,
} from "../../../voice/VoiceRecording";
import {Room} from "matrix-js-sdk/src/models/room";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import classNames from "classnames";
import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform";
import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
import { percentageOf } from "../../../utils/numbers";
import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
import LiveRecordingClock from "../audio_messages/LiveRecordingClock";
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import RecordingPlayback from "../voice_messages/RecordingPlayback";
import RecordingPlayback from "../audio_messages/RecordingPlayback";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
@ -46,8 +42,6 @@ interface IProps {
interface IState {
recorder?: VoiceRecording;
recordingPhase?: RecordingState;
relHeights: number[];
seconds: number;
}
/**
@ -55,58 +49,18 @@ interface IState {
*/
@replaceableComponent("views.rooms.VoiceRecordComposerTile")
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
private waveform: number[] = [];
private seconds = 0;
private scheduledAnimationFrame = false;
public constructor(props) {
super(props);
this.state = {
recorder: null, // no recording started by default
seconds: 0,
relHeights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES),
};
}
public componentDidUpdate(prevProps, prevState) {
if (!prevState.recorder && this.state.recorder) {
this.state.recorder.liveData.onUpdate(this.onRecordingUpdate);
}
}
public async componentWillUnmount() {
await VoiceRecordingStore.instance.disposeRecording();
}
private onRecordingUpdate = (update: IRecordingUpdate): void => {
this.waveform = update.waveform;
this.seconds = update.timeSeconds;
if (this.scheduledAnimationFrame) {
return;
}
this.scheduledAnimationFrame = true;
// The audio recorder flushes data faster than the screen refresh rate
// Using requestAnimationFrame makes sure that we only flush the data
// to react once per tick to avoid unneeded work.
requestAnimationFrame(() => {
// The waveform and the downsample target are pretty close, so we should be fine to
// do this, despite the docs on arrayFastResample.
const bars = arrayFastResample(Array.from(this.waveform), RECORDING_PLAYBACK_SAMPLES);
this.setState({
// The incoming data is between zero and one, but typically even screaming into a
// microphone won't send you over 0.6, so we artificially adjust the gain for the
// waveform. This results in a slightly more cinematic/animated waveform for the
// user.
relHeights: bars.map(b => percentageOf(b, 0, 0.50)),
seconds: this.seconds,
});
this.scheduledAnimationFrame = false;
});
}
// called by composer
public async send() {
if (!this.state.recorder) {