Apply prettier formatting
This commit is contained in:
parent
1cac306093
commit
526645c791
1576 changed files with 65385 additions and 62478 deletions
|
@ -45,7 +45,7 @@ export const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
|
|||
|
||||
function makePlaybackWaveform(input: number[]): number[] {
|
||||
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
|
||||
const noiseWaveform = input.map(v => Math.abs(v));
|
||||
const noiseWaveform = input.map((v) => Math.abs(v));
|
||||
|
||||
// Then, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
|
||||
// We also rescale the waveform to be 0-1 so we end up with a clamped waveform to rely upon.
|
||||
|
@ -174,7 +174,8 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
|
|||
// Overall, the point of this is to avoid memory-related issues due to storing a massive
|
||||
// audio buffer in memory, as that can balloon to far greater than the input buffer's
|
||||
// byte length.
|
||||
if (this.buf.byteLength > 5 * 1024 * 1024) { // 5mb
|
||||
if (this.buf.byteLength > 5 * 1024 * 1024) {
|
||||
// 5mb
|
||||
logger.log("Audio file too large: processing through <audio /> element");
|
||||
this.element = document.createElement("AUDIO") as HTMLAudioElement;
|
||||
const prom = new Promise((resolve, reject) => {
|
||||
|
@ -186,25 +187,33 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
|
|||
} else {
|
||||
// Safari compat: promise API not supported on this function
|
||||
this.audioBuf = await new Promise((resolve, reject) => {
|
||||
this.context.decodeAudioData(this.buf, b => resolve(b), async e => {
|
||||
try {
|
||||
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
|
||||
// very well.
|
||||
logger.error("Error decoding recording: ", e);
|
||||
logger.warn("Trying to re-encode to WAV instead...");
|
||||
this.context.decodeAudioData(
|
||||
this.buf,
|
||||
(b) => resolve(b),
|
||||
async (e) => {
|
||||
try {
|
||||
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
|
||||
// very well.
|
||||
logger.error("Error decoding recording: ", e);
|
||||
logger.warn("Trying to re-encode to WAV instead...");
|
||||
|
||||
const wav = await decodeOgg(this.buf);
|
||||
const wav = await decodeOgg(this.buf);
|
||||
|
||||
// noinspection ES6MissingAwait - not needed when using callbacks
|
||||
this.context.decodeAudioData(wav, b => resolve(b), e => {
|
||||
logger.error("Still failed to decode recording: ", e);
|
||||
// noinspection ES6MissingAwait - not needed when using callbacks
|
||||
this.context.decodeAudioData(
|
||||
wav,
|
||||
(b) => resolve(b),
|
||||
(e) => {
|
||||
logger.error("Still failed to decode recording: ", e);
|
||||
reject(e);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error("Caught decoding error:", e);
|
||||
reject(e);
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Caught decoding error:", e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Update the waveform to the real waveform once we have channel data to use. We don't
|
||||
|
|
|
@ -64,8 +64,7 @@ export class PlaybackClock implements IDestroyable {
|
|||
private clipDuration = 0;
|
||||
private placeholderDuration = 0;
|
||||
|
||||
public constructor(private context: AudioContext) {
|
||||
}
|
||||
public constructor(private context: AudioContext) {}
|
||||
|
||||
public get durationSeconds(): number {
|
||||
return this.clipDuration || this.placeholderDuration;
|
||||
|
@ -104,7 +103,7 @@ export class PlaybackClock implements IDestroyable {
|
|||
* @param {MatrixEvent} event The event to use for placeholders.
|
||||
*/
|
||||
public populatePlaceholdersFrom(event: MatrixEvent) {
|
||||
const durationMs = Number(event.getContent()['info']?.['duration']);
|
||||
const durationMs = Number(event.getContent()["info"]?.["duration"]);
|
||||
if (Number.isFinite(durationMs)) this.placeholderDuration = durationMs / 1000;
|
||||
}
|
||||
|
||||
|
|
|
@ -40,12 +40,12 @@ export class PlaybackManager {
|
|||
*/
|
||||
public pauseAllExcept(playback?: Playback) {
|
||||
this.instances
|
||||
.filter(p => p !== playback && p.currentState === PlaybackState.Playing)
|
||||
.forEach(p => p.pause());
|
||||
.filter((p) => p !== playback && p.currentState === PlaybackState.Playing)
|
||||
.forEach((p) => p.pause());
|
||||
}
|
||||
|
||||
public destroyPlaybackInstance(playback: ManagedPlayback) {
|
||||
this.instances = this.instances.filter(p => p !== playback);
|
||||
this.instances = this.instances.filter((p) => p !== playback);
|
||||
}
|
||||
|
||||
public createPlaybackInstance(buf: ArrayBuffer, waveform = DEFAULT_WAVEFORM): Playback {
|
||||
|
|
|
@ -116,8 +116,8 @@ export class PlaybackQueue {
|
|||
const instance = this.playbacks.get(next);
|
||||
if (!instance) {
|
||||
logger.warn(
|
||||
"Voice message queue desync: Missing playback for next message: "
|
||||
+ `Current=${this.currentPlaybackId} Last=${last} Next=${next}`,
|
||||
"Voice message queue desync: Missing playback for next message: " +
|
||||
`Current=${this.currentPlaybackId} Last=${last} Next=${next}`,
|
||||
);
|
||||
} else {
|
||||
this.playbackIdOrder = orderClone;
|
||||
|
@ -175,8 +175,8 @@ export class PlaybackQueue {
|
|||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
"Voice message queue desync: Expected playback stop to be last in order. "
|
||||
+ `Current=${this.currentPlaybackId} Last=${last} EventID=${mxEvent.getId()}`,
|
||||
"Voice message queue desync: Expected playback stop to be last in order. " +
|
||||
`Current=${this.currentPlaybackId} Last=${last} EventID=${mxEvent.getId()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -188,8 +188,8 @@ export class PlaybackQueue {
|
|||
if (order.length === 0 || order[order.length - 1] !== this.currentPlaybackId) {
|
||||
const lastInstance = this.playbacks.get(this.currentPlaybackId);
|
||||
if (
|
||||
lastInstance.currentState === PlaybackState.Playing
|
||||
|| lastInstance.currentState === PlaybackState.Paused
|
||||
lastInstance.currentState === PlaybackState.Playing ||
|
||||
lastInstance.currentState === PlaybackState.Paused
|
||||
) {
|
||||
order.push(this.currentPlaybackId);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ function roundTimeToTargetFreq(seconds: number): number {
|
|||
|
||||
function nextTimeForTargetFreq(roundedSeconds: number): number {
|
||||
// The extra round is just to make sure we cut off any floating point issues
|
||||
return roundTimeToTargetFreq(roundedSeconds + (1 / TARGET_AMPLITUDE_FREQUENCY));
|
||||
return roundTimeToTargetFreq(roundedSeconds + 1 / TARGET_AMPLITUDE_FREQUENCY);
|
||||
}
|
||||
|
||||
class MxVoiceWorklet extends AudioWorkletProcessor {
|
||||
|
|
|
@ -37,10 +37,7 @@ export class VoiceMessageRecording implements IDestroyable {
|
|||
private buffer = new Uint8Array(0); // use this.audioBuffer to access
|
||||
private playback: Playback;
|
||||
|
||||
public constructor(
|
||||
private matrixClient: MatrixClient,
|
||||
private voiceRecording: VoiceRecording,
|
||||
) {
|
||||
public constructor(private matrixClient: MatrixClient, private voiceRecording: VoiceRecording) {
|
||||
this.voiceRecording.onDataAvailable = this.onDataAvailable;
|
||||
}
|
||||
|
||||
|
@ -106,12 +103,9 @@ export class VoiceMessageRecording implements IDestroyable {
|
|||
const { url: mxc, file: encrypted } = await uploadFile(
|
||||
this.matrixClient,
|
||||
inRoomId,
|
||||
new Blob(
|
||||
[this.audioBuffer],
|
||||
{
|
||||
type: this.contentType,
|
||||
},
|
||||
),
|
||||
new Blob([this.audioBuffer], {
|
||||
type: this.contentType,
|
||||
}),
|
||||
);
|
||||
this.lastUpload = { mxc, encrypted };
|
||||
this.emit(RecordingState.Uploaded);
|
||||
|
|
|
@ -15,8 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import Recorder from 'opus-recorder/dist/recorder.min.js';
|
||||
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
|
||||
import Recorder from "opus-recorder/dist/recorder.min.js";
|
||||
import encoderPath from "opus-recorder/dist/encoderWorker.min.js";
|
||||
import { SimpleObservable } from "matrix-widget-api";
|
||||
import EventEmitter from "events";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
@ -137,15 +137,15 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
|
||||
// Dev note: we can't use `addEventListener` for some reason. It just doesn't work.
|
||||
this.recorderWorklet.port.onmessage = (ev) => {
|
||||
switch (ev.data['ev']) {
|
||||
switch (ev.data["ev"]) {
|
||||
case PayloadEvent.Timekeep:
|
||||
this.processAudioUpdate(ev.data['timeSeconds']);
|
||||
this.processAudioUpdate(ev.data["timeSeconds"]);
|
||||
break;
|
||||
case PayloadEvent.AmplitudeMark:
|
||||
// Sanity check to make sure we're adding about one sample per second
|
||||
if (ev.data['forIndex'] === this.amplitudes.length) {
|
||||
this.amplitudes.push(ev.data['amplitude']);
|
||||
this.liveWaveform.pushValue(ev.data['amplitude']);
|
||||
if (ev.data["forIndex"] === this.amplitudes.length) {
|
||||
this.amplitudes.push(ev.data["amplitude"]);
|
||||
this.liveWaveform.pushValue(ev.data["amplitude"]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -159,8 +159,9 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
this.recorderProcessor.addEventListener("audioprocess", this.onAudioProcess);
|
||||
}
|
||||
|
||||
const recorderOptions = this.shouldRecordInHighQuality() ?
|
||||
highQualityRecorderOptions : voiceRecorderOptions;
|
||||
const recorderOptions = this.shouldRecordInHighQuality()
|
||||
? highQualityRecorderOptions
|
||||
: voiceRecorderOptions;
|
||||
const { encoderApplication, bitrate } = recorderOptions;
|
||||
|
||||
this.recorder = new Recorder({
|
||||
|
@ -184,12 +185,13 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
this.recorder.ondataavailable = (data: ArrayBuffer) => this?.onDataAvailable(data);
|
||||
} catch (e) {
|
||||
logger.error("Error starting recording: ", e);
|
||||
if (e instanceof DOMException) { // Unhelpful DOMExceptions are common - parse them sanely
|
||||
if (e instanceof DOMException) {
|
||||
// Unhelpful DOMExceptions are common - parse them sanely
|
||||
logger.error(`${e.name} (${e.code}): ${e.message}`);
|
||||
}
|
||||
|
||||
// Clean up as best as possible
|
||||
if (this.recorderStream) this.recorderStream.getTracks().forEach(t => t.stop());
|
||||
if (this.recorderStream) this.recorderStream.getTracks().forEach((t) => t.stop());
|
||||
if (this.recorderSource) this.recorderSource.disconnect();
|
||||
if (this.recorder) this.recorder.close();
|
||||
if (this.recorderContext) {
|
||||
|
@ -221,7 +223,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
if (!this.recording) return;
|
||||
|
||||
this.observable.update({
|
||||
waveform: this.liveWaveform.value.map(v => clamp(v, 0, 1)),
|
||||
waveform: this.liveWaveform.value.map((v) => clamp(v, 0, 1)),
|
||||
timeSeconds: timeSeconds,
|
||||
});
|
||||
|
||||
|
@ -243,7 +245,8 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
}
|
||||
|
||||
const secondsLeft = TARGET_MAX_LENGTH - this.recorderSeconds;
|
||||
if (secondsLeft < 0) { // go over to make sure we definitely capture that last frame
|
||||
if (secondsLeft < 0) {
|
||||
// go over to make sure we definitely capture that last frame
|
||||
// noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping
|
||||
this.stop();
|
||||
} else if (secondsLeft <= TARGET_WARN_TIME_LEFT) {
|
||||
|
@ -256,7 +259,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
|
||||
/**
|
||||
* {@link https://github.com/chris-rudmin/opus-recorder#instance-fields ref for recorderSeconds}
|
||||
*/
|
||||
*/
|
||||
public get recorderSeconds() {
|
||||
return this.recorder.encodedSamplePosition / 48000;
|
||||
}
|
||||
|
@ -295,7 +298,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
await this.recorderContext.close();
|
||||
|
||||
// Now stop all the media tracks so we can release them back to the user/OS
|
||||
this.recorderStream.getTracks().forEach(t => t.stop());
|
||||
this.recorderStream.getTracks().forEach((t) => t.stop());
|
||||
|
||||
// Finally do our post-processing and clean up
|
||||
this.recording = false;
|
||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// @ts-ignore - we know that this is not a module. We're looking for a path.
|
||||
import decoderWasmPath from 'opus-recorder/dist/decoderWorker.min.wasm';
|
||||
import wavEncoderPath from 'opus-recorder/dist/waveWorker.min.js';
|
||||
import decoderPath from 'opus-recorder/dist/decoderWorker.min.js';
|
||||
import decoderWasmPath from "opus-recorder/dist/decoderWorker.min.wasm";
|
||||
import wavEncoderPath from "opus-recorder/dist/waveWorker.min.js";
|
||||
import decoderPath from "opus-recorder/dist/decoderWorker.min.js";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { SAMPLE_RATE } from "./VoiceRecording";
|
||||
|
@ -38,46 +38,54 @@ export function createAudioContext(opts?: AudioContextOptions): AudioContext {
|
|||
export function decodeOgg(audioBuffer: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
// Condensed version of decoder example, using a promise:
|
||||
// https://github.com/chris-rudmin/opus-recorder/blob/master/example/decoder.html
|
||||
return new Promise((resolve) => { // no reject because the workers don't seem to have a fail path
|
||||
return new Promise((resolve) => {
|
||||
// no reject because the workers don't seem to have a fail path
|
||||
logger.log("Decoder WASM path: " + decoderWasmPath); // so we use the variable (avoid tree shake)
|
||||
const typedArray = new Uint8Array(audioBuffer);
|
||||
const decoderWorker = new Worker(decoderPath);
|
||||
const wavWorker = new Worker(wavEncoderPath);
|
||||
|
||||
decoderWorker.postMessage({
|
||||
command: 'init',
|
||||
command: "init",
|
||||
decoderSampleRate: SAMPLE_RATE,
|
||||
outputBufferSampleRate: SAMPLE_RATE,
|
||||
});
|
||||
|
||||
wavWorker.postMessage({
|
||||
command: 'init',
|
||||
command: "init",
|
||||
wavBitDepth: 24, // standard for 48khz (SAMPLE_RATE)
|
||||
wavSampleRate: SAMPLE_RATE,
|
||||
});
|
||||
|
||||
decoderWorker.onmessage = (ev) => {
|
||||
if (ev.data === null) { // null == done
|
||||
wavWorker.postMessage({ command: 'done' });
|
||||
if (ev.data === null) {
|
||||
// null == done
|
||||
wavWorker.postMessage({ command: "done" });
|
||||
return;
|
||||
}
|
||||
|
||||
wavWorker.postMessage({
|
||||
command: 'encode',
|
||||
buffers: ev.data,
|
||||
}, ev.data.map(b => b.buffer));
|
||||
wavWorker.postMessage(
|
||||
{
|
||||
command: "encode",
|
||||
buffers: ev.data,
|
||||
},
|
||||
ev.data.map((b) => b.buffer),
|
||||
);
|
||||
};
|
||||
|
||||
wavWorker.onmessage = (ev) => {
|
||||
if (ev.data.message === 'page') {
|
||||
if (ev.data.message === "page") {
|
||||
// The encoding comes through as a single page
|
||||
resolve(new Blob([ev.data.page], { type: "audio/wav" }).arrayBuffer());
|
||||
}
|
||||
};
|
||||
|
||||
decoderWorker.postMessage({
|
||||
command: 'decode',
|
||||
pages: typedArray,
|
||||
}, [typedArray.buffer]);
|
||||
decoderWorker.postMessage(
|
||||
{
|
||||
command: "decode",
|
||||
pages: typedArray,
|
||||
},
|
||||
[typedArray.buffer],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue