Make waveform update match the screen refresh rate
parent
27d255f30e
commit
a85c6c67e0
|
@ -15,19 +15,26 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import {_t} from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import React, {ReactNode} from "react";
|
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 {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform";
|
import Waveform from "../voice_messages/Waveform";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
|
import { arrayFastResample, arraySeed } from "../../../utils/arrays";
|
||||||
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
|
import { percentageOf } from "../../../utils/numbers";
|
||||||
|
import Clock from "../voice_messages/Clock";
|
||||||
|
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
|
||||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||||
import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
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 Modal from "../../../Modal";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import CallMediaHandler from "../../../CallMediaHandler";
|
import CallMediaHandler from "../../../CallMediaHandler";
|
||||||
|
@ -39,6 +46,8 @@ interface IProps {
|
||||||
interface IState {
|
interface IState {
|
||||||
recorder?: VoiceRecording;
|
recorder?: VoiceRecording;
|
||||||
recordingPhase?: RecordingState;
|
recordingPhase?: RecordingState;
|
||||||
|
relHeights: number[];
|
||||||
|
seconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,18 +55,58 @@ interface IState {
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.rooms.VoiceRecordComposerTile")
|
@replaceableComponent("views.rooms.VoiceRecordComposerTile")
|
||||||
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
|
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
|
||||||
|
private waveform: number[] = [];
|
||||||
|
private seconds = 0;
|
||||||
|
private scheduledAnimationFrame = false;
|
||||||
|
|
||||||
public constructor(props) {
|
public constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
recorder: null, // no recording started by default
|
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() {
|
public async componentWillUnmount() {
|
||||||
await VoiceRecordingStore.instance.disposeRecording();
|
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
|
// called by composer
|
||||||
public async send() {
|
public async send() {
|
||||||
if (!this.state.recorder) {
|
if (!this.state.recorder) {
|
||||||
|
@ -178,8 +227,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
|
|
||||||
// only other UI is the recording-in-progress UI
|
// only other UI is the recording-in-progress UI
|
||||||
return <div className="mx_VoiceMessagePrimaryContainer mx_VoiceRecordComposerTile_recording">
|
return <div className="mx_VoiceMessagePrimaryContainer mx_VoiceRecordComposerTile_recording">
|
||||||
<LiveRecordingClock recorder={this.state.recorder} />
|
<Clock seconds={this.state.seconds} />
|
||||||
<LiveRecordingWaveform recorder={this.state.recorder} />
|
<Waveform relHeights={this.state.relHeights} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<IProps, IState> {
|
|
||||||
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 <Clock seconds={this.state.seconds} />;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<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 render() {
|
|
||||||
return <Waveform relHeights={this.state.heights} />;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue