From a85c6c67e059a92b4c57dda42cd88217cb3b3c3f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 12:28:23 +0100 Subject: [PATCH] Make waveform update match the screen refresh rate --- .../views/rooms/VoiceRecordComposerTile.tsx | 69 ++++++++++++++++--- .../voice_messages/LiveRecordingClock.tsx | 49 ------------- .../voice_messages/LiveRecordingWaveform.tsx | 60 ---------------- 3 files changed, 59 insertions(+), 119 deletions(-) delete mode 100644 src/components/views/voice_messages/LiveRecordingClock.tsx delete mode 100644 src/components/views/voice_messages/LiveRecordingWaveform.tsx diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 20d8c9c5d4..6fe6a5ab1c 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -15,19 +15,26 @@ limitations under the License. */ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import {_t} from "../../../languageHandler"; +import { _t } from "../../../languageHandler"; import React, {ReactNode} from "react"; -import {RecordingState, VoiceRecording} from "../../../voice/VoiceRecording"; +import { + IRecordingUpdate, + RECORDING_PLAYBACK_SAMPLES, + RecordingState, + VoiceRecording, +} from "../../../voice/VoiceRecording"; import {Room} from "matrix-js-sdk/src/models/room"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import classNames from "classnames"; -import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; -import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; +import Waveform from "../voice_messages/Waveform"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { arrayFastResample, arraySeed } from "../../../utils/arrays"; +import { percentageOf } from "../../../utils/numbers"; +import Clock from "../voice_messages/Clock"; +import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import RecordingPlayback from "../voice_messages/RecordingPlayback"; -import {MsgType} from "matrix-js-sdk/src/@types/event"; +import { MsgType } from "matrix-js-sdk/src/@types/event"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; import CallMediaHandler from "../../../CallMediaHandler"; @@ -39,6 +46,8 @@ interface IProps { interface IState { recorder?: VoiceRecording; recordingPhase?: RecordingState; + relHeights: number[]; + seconds: number; } /** @@ -46,18 +55,58 @@ interface IState { */ @replaceableComponent("views.rooms.VoiceRecordComposerTile") export default class VoiceRecordComposerTile extends React.PureComponent { + 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) { @@ -178,8 +227,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent - - + + ; } diff --git a/src/components/views/voice_messages/LiveRecordingClock.tsx b/src/components/views/voice_messages/LiveRecordingClock.tsx deleted file mode 100644 index b82539eb16..0000000000 --- a/src/components/views/voice_messages/LiveRecordingClock.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import Clock from "./Clock"; - -interface IProps { - recorder: VoiceRecording; -} - -interface IState { - seconds: number; -} - -/** - * A clock for a live recording. - */ -@replaceableComponent("views.voice_messages.LiveRecordingClock") -export default class LiveRecordingClock extends React.PureComponent { - public constructor(props) { - super(props); - - this.state = {seconds: 0}; - this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); - } - - private onRecordingUpdate = (update: IRecordingUpdate) => { - this.setState({seconds: update.timeSeconds}); - }; - - public render() { - return ; - } -} diff --git a/src/components/views/voice_messages/LiveRecordingWaveform.tsx b/src/components/views/voice_messages/LiveRecordingWaveform.tsx deleted file mode 100644 index aab89f6ab1..0000000000 --- a/src/components/views/voice_messages/LiveRecordingWaveform.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import {IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording} from "../../../voice/VoiceRecording"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {arrayFastResample, arraySeed} from "../../../utils/arrays"; -import {percentageOf} from "../../../utils/numbers"; -import Waveform from "./Waveform"; - -interface IProps { - recorder: VoiceRecording; -} - -interface IState { - heights: number[]; -} - -/** - * A waveform which shows the waveform of a live recording - */ -@replaceableComponent("views.voice_messages.LiveRecordingWaveform") -export default class LiveRecordingWaveform extends React.PureComponent { - 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 render() { - return ; - } -}