diff --git a/ChangeLog.md b/ChangeLog.md index 519ed197..c64e2daf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,7 @@ # Changelog: * **12.06.20** - Added a copy/paste menu for all HTML input elements + - Heavily improved web client audio de/encoding * **11.06.20** - Fixed channel tree deletions diff --git a/web/js/codec/CodecWorkerMessages.ts b/web/js/codec/CodecWorkerMessages.ts new file mode 100644 index 00000000..f74e2aaa --- /dev/null +++ b/web/js/codec/CodecWorkerMessages.ts @@ -0,0 +1,92 @@ +import {CodecType} from "tc-backend/web/codec/Codec"; + +export type CWMessageResponse = { + type: "success"; + token: string; + + response: any; + + timestampReceived: number; + timestampSend: number; +}; + +export type CWMessageErrorResponse = { + type: "error"; + token: string; + + error: string; + + timestampReceived: number; + timestampSend: number; +} + +export type CWMessageCommand = { + type: "command"; + token: string; + + command: keyof T; + payload: any; +} + +export type CWMessageNotify = { + type: "notify"; +} + +export type CWMessage = CWMessageCommand | CWMessageErrorResponse | CWMessageResponse | CWMessageNotify; + +/* from handle to worker */ +export interface CWCommand { + "global-initialize": {}, + + + "initialise": { + type: CodecType, + channelCount: number + }, + "reset": {} + "finalize": {}, + + "decode-payload": { + buffer: ArrayBuffer; + byteLength: number; + byteOffset: number; + maxByteLength: number; + }, + + "encode-payload": { + buffer: ArrayBuffer; + byteLength: number; + byteOffset: number; + maxByteLength: number; + }, +} + +/* from worker to handle */ +export interface CWCommandResponse { + "decode-payload-result": { + buffer: ArrayBuffer; + byteLength: number; + byteOffset: number; + }, + + "encode-payload-result": { + buffer: ArrayBuffer; + byteLength: number; + byteOffset: number; + } +} + +export interface CWMessageRelations { + "decode-payload": "decode-payload-result", + "decode-payload-result": never, + + "encode-payload": "encode-payload-result", + "encode-payload-result": never, + + "global-initialize": void, + "initialise": void, + "reset": void, + "finalize": void +} + +export type CWCommandResponseType = CWMessageRelations[T] extends string ? CWCommandResponse[CWMessageRelations[T]] : CWMessageRelations[T]; \ No newline at end of file diff --git a/web/js/codec/CodecWrapperWorker.ts b/web/js/codec/CodecWrapperWorker.ts index 91526a66..f75fc05a 100644 --- a/web/js/codec/CodecWrapperWorker.ts +++ b/web/js/codec/CodecWrapperWorker.ts @@ -2,18 +2,52 @@ import {BasicCodec} from "./BasicCodec"; import {CodecType} from "./Codec"; import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; +import { + CWCommand, + CWCommandResponseType, + CWMessage, CWMessageCommand, + CWMessageErrorResponse, + CWMessageResponse +} from "tc-backend/web/codec/CodecWorkerMessages"; -interface ExecuteResult { - result?: any; - error?: string; +type MessageTimings = { + upstream: number; + downstream: number; + handle: number; +}; +interface ExecuteResultBase { success: boolean; - timings: { - upstream: number; - downstream: number; - handle: number; + timings: MessageTimings +} + +interface SuccessExecuteResult extends ExecuteResultBase { + success: true; + result: T; +} + +interface ErrorExecuteResult extends ExecuteResultBase { + success: false; + error: string; +} +type ExecuteResult = SuccessExecuteResult | ErrorExecuteResult; + +const cachedBufferSize = 1024 * 8; +let cachedBuffers: ArrayBuffer[] = []; +function nextCachedBuffer() : ArrayBuffer { + if(cachedBuffers.length === 0) { + return new ArrayBuffer(cachedBufferSize); } + return cachedBuffers.pop(); +} + +function freeCachedBuffer(buffer: ArrayBuffer) { + if(cachedBuffers.length > 32) + return; + else if(buffer.byteLength < cachedBufferSize) + return; + cachedBuffers.push(buffer); } export class CodecWrapperWorker extends BasicCodec { @@ -27,10 +61,8 @@ export class CodecWrapperWorker extends BasicCodec { private pending_executes: {[key: string]: { timeout?: any; - timestamp_send: number, - + timestampSend: number, resolve: (_: ExecuteResult) => void; - reject: (_: any) => void; }} = {}; constructor(type: CodecType) { @@ -61,7 +93,7 @@ export class CodecWrapperWorker extends BasicCodec { type: this.type, channelCount: this.channelCount, })).then(result => { - if(result.success) { + if(result.success === true) { this._initialized = true; return Promise.resolve(true); } @@ -78,7 +110,7 @@ export class CodecWrapperWorker extends BasicCodec { } deinitialise() { - this.execute("deinitialise", {}); + this.execute("finalize", {}); this._initialized = false; this._initialize_promise = undefined; } @@ -86,44 +118,52 @@ export class CodecWrapperWorker extends BasicCodec { async decode(data: Uint8Array): Promise { if(!this.initialized()) throw "codec not initialized/initialize failed"; - const result = await this.execute("decodeSamples", { data: data, length: data.length }); + const cachedBuffer = nextCachedBuffer(); + new Uint8Array(cachedBuffer).set(data); + + const result = await this.execute("decode-payload", { + byteLength: data.byteLength, + buffer: cachedBuffer, + byteOffset: 0, + maxByteLength: cachedBuffer.byteLength + }, 5000, [ cachedBuffer ]); if(result.timings.downstream > 5 || result.timings.upstream > 5 || result.timings.handle > 5) log.warn(LogCategory.VOICE, tr("Worker message stock time: {downstream: %dms, handle: %dms, upstream: %dms}"), result.timings.downstream, result.timings.handle, result.timings.upstream); - if(!result.success) throw result.error || tr("unknown decode error"); + if(result.success === false) + throw result.error; - let array = new Float32Array(result.result.length); - for(let index = 0; index < array.length; index++) - array[index] = result.result.data[index]; + const chunkLength = result.result.byteLength / this.channelCount; + const audioBuffer = this._audioContext.createBuffer(this.channelCount, chunkLength / 4, this._codecSampleRate); - let audioBuf = this._audioContext.createBuffer(this.channelCount, array.length / this.channelCount, this._codecSampleRate); - for (let channel = 0; channel < this.channelCount; channel++) { - for (let offset = 0; offset < audioBuf.length; offset++) { - audioBuf.getChannelData(channel)[offset] = array[channel + offset * this.channelCount]; - } + for(let channel = 0; channel < this.channelCount; channel++) { + const buffer = new Float32Array(result.result.buffer, result.result.byteOffset + chunkLength * channel, chunkLength / 4); + audioBuffer.copyToChannel(buffer, channel, 0); } - return audioBuf; + freeCachedBuffer(result.result.buffer); + return audioBuffer; } async encode(data: AudioBuffer) : Promise { if(!this.initialized()) throw "codec not initialized/initialize failed"; - let buffer = new Float32Array(this.channelCount * data.length); - for (let offset = 0; offset < data.length; offset++) { - for (let channel = 0; channel < this.channelCount; channel++) - buffer[offset * this.channelCount + channel] = data.getChannelData(channel)[offset]; - } + const buffer = nextCachedBuffer(); + const f32Buffer = new Float32Array(buffer); + for(let channel = 0; channel < this.channelCount; channel++) + data.copyFromChannel(f32Buffer, channel, data.length * channel); + + const result = await this.execute("encode-payload", { byteLength: data.length * this.channelCount * 4, buffer: buffer, byteOffset: 0, maxByteLength: buffer.byteLength }); - const result = await this.execute("encodeSamples", { data: buffer, length: buffer.length }); if(result.timings.downstream > 5 || result.timings.upstream > 5) log.warn(LogCategory.VOICE, tr("Worker message stock time: {downstream: %dms, handle: %dms, upstream: %dms}"), result.timings.downstream, result.timings.handle, result.timings.upstream); - if(!result.success) throw result.error || tr("unknown encode error"); - let array = new Uint8Array(result.result.length); - for(let index = 0; index < array.length; index++) - array[index] = result.result.data[index]; - return array; + if(result.success === false) + throw result.error; + + const encodedResult = new Uint8Array(result.result.buffer, result.result.byteOffset, result.result.byteLength).slice(0); + freeCachedBuffer(result.result.buffer); + return encodedResult; } reset() : boolean { @@ -132,85 +172,118 @@ export class CodecWrapperWorker extends BasicCodec { return true; } - private handle_worker_message(message: any) { - if(!message["token"]) { - log.error(LogCategory.VOICE, tr("Invalid worker token!")); + private handleWorkerMessage(message: CWMessage) { + if(message.type === "notify") { + log.warn(LogCategory.VOICE, tr("Received unknown notify from worker.")); return; - } - - if(message["token"] === "notify") { - /* currently not really used */ - if(message["type"] == "chatmessage_server") { - //FIXME? + } else if(message.type === "error") { + const request = this.pending_executes[message.token]; + if(typeof request !== "object") { + log.warn(LogCategory.VOICE, tr("Received worker execute error for unknown token (%s)"), message.token); return; } - log.debug(LogCategory.VOICE, tr("Costume callback! (%o)"), message); - return; - } + delete this.pending_executes[message.token]; + clearTimeout(request.timeout); - const request = this.pending_executes[message["token"]]; - if(typeof request !== "object") { - log.error(LogCategory.VOICE, tr("Received worker execute result for unknown token (%s)"), message["token"]); - return; - } - delete this.pending_executes[message["token"]]; - - const result: ExecuteResult = { - success: message["success"], - error: message["error"], - result: message["result"], - timings: { - downstream: message["timestamp_received"] - request.timestamp_send, - handle: message["timestamp_send"] - message["timestamp_received"], - upstream: Date.now() - message["timestamp_send"] + const eresponse = message as CWMessageErrorResponse; + request.resolve({ + success: false, + timings: { + downstream: eresponse.timestampReceived - request.timestampSend, + handle: eresponse.timestampSend - eresponse.timestampReceived, + upstream: Date.now() - eresponse.timestampSend + }, + error: eresponse.error + }); + } else if(message.type === "success") { + const request = this.pending_executes[message.token]; + if(typeof request !== "object") { + log.warn(LogCategory.VOICE, tr("Received worker execute result for unknown token (%s)"), message.token); + return; } - }; - clearTimeout(request.timeout); - request.resolve(result); + delete this.pending_executes[message.token]; + clearTimeout(request.timeout); + + const response = message as CWMessageResponse; + request.resolve({ + success: true, + timings: { + downstream: response.timestampReceived - request.timestampSend, + handle: response.timestampSend - response.timestampReceived, + upstream: Date.now() - response.timestampSend + }, + result: response.response + }); + } else if(message.type === "command") { + log.warn(LogCategory.VOICE, tr("Received command %s from voice worker. This should never happen!"), (message as CWMessageCommand).command); + return; + } else { + log.warn(LogCategory.VOICE, tr("Received unknown message of type %s from voice worker. This should never happen!"), (message as any).type); + return; + } } - private handle_worker_error(error: any) { + private handleWorkerError() { log.error(LogCategory.VOICE, tr("Received error from codec worker. Closing worker.")); for(const token of Object.keys(this.pending_executes)) { - this.pending_executes[token].reject(error); + this.pending_executes[token].resolve({ + success: false, + error: tr("worker terminated with an error"), + timings: { downstream: 0, handle: 0, upstream: 0} + }); delete this.pending_executes[token]; } this._worker = undefined; } - private execute(command: string, data: any, timeout?: number) : Promise { - return new Promise((resolve, reject) => { + private execute(command: T, data: CWCommand[T], timeout?: number, transfer?: Transferable[]) : Promise>> { + return new Promise(resolve => { if(!this._worker) { - reject(tr("worker does not exists")); + resolve({ + success: false, + error: tr("worker does not exists"), + timings: { + downstream: 0, + handle: 0, + upstream: 0 + } + }); return; } const token = this._token_index++ + "_token"; - const payload = { - token: token, - command: command, - data: data, - }; - this.pending_executes[token] = { - timeout: typeof timeout === "number" ? setTimeout(() => reject(tr("timeout for command ") + command), timeout) : undefined, + timeout: typeof timeout === "number" ? setTimeout(() => { + delete this.pending_executes[token]; + resolve({ + success: false, + error: tr("command timed out"), + timings: { upstream: 0, handle: 0, downstream: 0 } + }) + }, timeout) : undefined, resolve: resolve, - reject: reject, - timestamp_send: Date.now() + timestampSend: Date.now() }; - this._worker.postMessage(payload); + this._worker.postMessage({ + command: command, + type: "command", + + payload: data, + token: token + } as CWMessageCommand, transfer); }); } private async spawn_worker() : Promise { this._worker = new Worker("tc-backend/web/workers/codec", { type: "module" }); - this._worker.onmessage = event => this.handle_worker_message(event.data); - this._worker.onerror = event => this.handle_worker_error(event.error); + this._worker.onmessage = event => this.handleWorkerMessage(event.data); + this._worker.onerror = () => this.handleWorkerError(); const result = await this.execute("global-initialize", {}, 15000); - if(!result.success) throw result.error; + if(result.success === false) + throw result.error; } } \ No newline at end of file diff --git a/web/js/workers/codec/CodecWorker.ts b/web/js/workers/codec/CodecWorker.ts index e27ddad3..5ea2ba7d 100644 --- a/web/js/workers/codec/CodecWorker.ts +++ b/web/js/workers/codec/CodecWorker.ts @@ -1,4 +1,11 @@ import {CodecType} from "tc-backend/web/codec/Codec"; +import { + CWMessageCommand, + CWCommand, + CWMessage, + CWMessageResponse, + CWMessageErrorResponse, CWCommandResponseType +} from "tc-backend/web/codec/CodecWorkerMessages"; const prefix = "[CodecWorker] "; @@ -6,8 +13,8 @@ export interface CodecWorker { name(); initialise?() : string; deinitialise(); - decode(data: Uint8Array); - encode(data: Float32Array) : Uint8Array | string; + decode(buffer: Uint8Array, responseBuffer: (length: number) => Uint8Array) : number | string; + encode(buffer: Uint8Array, responseBuffer: (length: number) => Uint8Array) : number | string; reset(); } @@ -25,110 +32,152 @@ export function set_initialize_callback(callback: () => Promise) export let codec_instance: CodecWorker; let globally_initialized = false; let global_initialize_result; +let commandTransferableResponse: Transferable[]; -/** - * @param command - * @param data - * @return string on error or object on success - */ -async function handle_message(command: string, data: any) : Promise { - switch (command) { - case "global-initialize": - try { - const init_result = globally_initialized ? global_initialize_result : await initialize_callback(); - globally_initialized = true; +let messageHandlers: { [T in keyof CWCommand]: (message: CWCommand[T]) => Promise> } = {} as any; - if(typeof init_result === "string") - throw init_result; - } catch (e) { - if(typeof e === "string") - return e; - throw e; - } - - return {}; - case "initialise": - console.log(prefix + "Initialize for codec %s", CodecType[data.type as CodecType]); - if(!supported_types[data.type]) - return "type unsupported"; - - try { - codec_instance = await supported_types[data.type](data.options); - } catch(ex) { - console.error(prefix + "Failed to allocate codec: %o", ex); - return typeof ex === "string" ? ex : "failed to allocate codec"; - } - - const error = codec_instance.initialise(); - if(error) return error; - - return {}; - case "encodeSamples": - if(!codec_instance) - return "codec not initialized/initialize failed"; - - let encodeArray = new Float32Array(data.length); - for(let index = 0; index < encodeArray.length; index++) - encodeArray[index] = data.data[index]; - - let encodeResult = codec_instance.encode(encodeArray); - if(typeof encodeResult === "string") - return encodeResult; - else - return { data: encodeResult, length: encodeResult.length }; - case "decodeSamples": - if(!codec_instance) - return "codec not initialized/initialize failed"; - - let decodeArray = new Uint8Array(data.length); - for(let index = 0; index < decodeArray.length; index++) - decodeArray[index] = data.data[index]; - - let decodeResult = codec_instance.decode(decodeArray); - if(typeof decodeResult === "string") - return decodeResult; - else - return { data: decodeResult, length: decodeResult.length }; - case "reset": - codec_instance.reset(); - break; - default: - return "unknown command"; - } +function registerCommandHandler(command: T, callback: (message: CWCommand[T]) => Promise>) { + messageHandlers[command as any] = callback; } +const handleOwnerMessage = (e: MessageEvent) => { + const timestampReceived = Date.now(); + const message = e.data as CWMessage; -const handle_message_event = (e: MessageEvent) => { - const token = e.data.token; - const received = Date.now(); + if(message.type === "error" || message.type === "success") { + console.warn("%sReceived a command response within the worker. We're not sending any commands so this should not happen!", prefix); + return; + } else if(message.type === "notify") { + console.warn("%sReceived a notify within the worker. This should not happen!", prefix); + return; + } else if(message.type === "command") { + const command = message as CWMessageCommand; - const send_result = result => { - const data = {}; - if(typeof result === "object") { - data["result"] = result; - data["success"] = true; - } else if(typeof result === "string") { - data["error"] = result; - data["success"] = false; - } else { - data["error"] = "invalid result"; - data["success"] = false; - } - data["token"] = token; - data["timestamp_received"] = received; - data["timestamp_send"] = Date.now(); + const sendExecuteError = error => { + let errorMessage; + if(typeof error === "string") { + errorMessage = error; + } else if(error instanceof Error) { + console.error("%sMessage handle error: %o", prefix, error); + errorMessage = error.message; + } else { + console.error("%sMessage handle error: %o", prefix, error); + errorMessage = "lookup the console"; + } - postMessage(data, undefined); - }; - handle_message(e.data.command, e.data.data).then(res => { - if(token) { - send_result(res); + postMessage({ + type: "error", + error: errorMessage, + + timestampReceived: timestampReceived, + timestampSend: Date.now(), + + token: command.token + } as CWMessageErrorResponse, undefined, commandTransferableResponse); + }; + + const sendExecuteResult = result => { + postMessage({ + type: "success", + response: result, + + timestampReceived: timestampReceived, + timestampSend: Date.now(), + + token: command.token + } as CWMessageResponse, undefined); + }; + + const handler = messageHandlers[message.command as any]; + if(!handler) { + sendExecuteError("unknown command"); + return; } - }).catch(error => { - console.warn("An error has been thrown while handing command %s: %o", e.data.command, error); - if(token) { - send_result(typeof error === "string" ? error : "unexpected exception has been thrown"); - } - }); + + handler(command.payload).then(sendExecuteResult).catch(sendExecuteError); + } }; -addEventListener("message", handle_message_event); \ No newline at end of file +addEventListener("message", handleOwnerMessage); + + +/* command handlers */ +registerCommandHandler("global-initialize", async () => { + const init_result = globally_initialized ? global_initialize_result : await initialize_callback(); + globally_initialized = true; + + if(typeof init_result === "string") + throw init_result; +}); + +registerCommandHandler("initialise", async data => { + console.log(prefix + "Initialize for codec %s", CodecType[data.type as CodecType]); + if(!supported_types[data.type]) + throw "type unsupported"; + + try { + codec_instance = await supported_types[data.type](data); + } catch(ex) { + console.error("%sFailed to allocate codec: %o", prefix, ex); + throw typeof ex === "string" ? ex : "failed to allocate codec"; + } + + const error = codec_instance.initialise(); + if(error) + throw error; +}); + +registerCommandHandler("reset", async () => { + codec_instance.reset(); +}); + +registerCommandHandler("finalize", async () => { + /* memory will be cleaned up by its own */ +}); + +let responseBuffer: Uint8Array; +const popResponseBuffer = () => { const temp = responseBuffer; responseBuffer = undefined; return temp; } +registerCommandHandler("decode-payload", async data => { + if(!codec_instance) + throw "codec not initialized/initialize failed"; + + const byteLength = codec_instance.decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength), length => { + if(length > data.maxByteLength) + throw "source buffer too small to hold the result"; + //return responseBuffer = new Uint8Array(length); + return responseBuffer = new Uint8Array(data.buffer, 0, data.maxByteLength); + }); + const buffer = popResponseBuffer(); + if(typeof byteLength === "string") { + throw byteLength; + } + + commandTransferableResponse = [buffer.buffer]; + return { + buffer: buffer.buffer, + byteLength: byteLength, + byteOffset: 0, + }; +}); + +registerCommandHandler("encode-payload", async data => { + if(!codec_instance) + throw "codec not initialized/initialize failed"; + + const byteLength = codec_instance.encode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength), length => { + if(length > data.maxByteLength) + throw "source buffer too small to hold the result"; + //return responseBuffer = new Uint8Array(length); + return responseBuffer = new Uint8Array(data.buffer, 0, data.maxByteLength); + }); + const buffer = popResponseBuffer(); + if(typeof byteLength === "string") { + throw byteLength; + } + + commandTransferableResponse = [buffer.buffer]; + return { + buffer: buffer.buffer, + byteLength: byteLength, + byteOffset: 0 + }; +}); \ No newline at end of file diff --git a/web/js/workers/codec/OpusCodec.ts b/web/js/workers/codec/OpusCodec.ts index 0af18483..f892ac3f 100644 --- a/web/js/workers/codec/OpusCodec.ts +++ b/web/js/workers/codec/OpusCodec.ts @@ -1,7 +1,6 @@ import * as cworker from "./CodecWorker"; import {CodecType} from "tc-backend/web/codec/Codec"; import {CodecWorker} from "./CodecWorker"; -import {type} from "os"; declare global { interface Window { @@ -113,7 +112,7 @@ enum OpusType { const OPUS_ERROR_CODES = [ "One or more invalid/out of range arguments", //-1 (OPUS_BAD_ARG) - "Not enough bytes allocated in the buffer", //-2 (OPUS_BUFFER_TOO_SMALL) + "Not enough bytes allocated in the target buffer", //-2 (OPUS_BUFFER_TOO_SMALL) "An internal error was detected", //-3 (OPUS_INTERNAL_ERROR) "The compressed data passed is corrupted", //-4 (OPUS_INVALID_PACKET) "Invalid/unsupported request number", //-5 (OPUS_UNIMPLEMENTED) @@ -162,23 +161,33 @@ class OpusWorker implements CodecWorker { deinitialise() { } //TODO - decode(data: Uint8Array): Float32Array | string { - if (data.byteLength > this.decode_buffer.byteLength) return "supplied data exceeds internal buffer"; - this.decode_buffer.set(data); + decode(buffer: Uint8Array, responseBuffer: (length: number) => Uint8Array): number | string { + if (buffer.byteLength > this.decode_buffer.byteLength) + return "supplied data exceeds internal buffer"; - let result = this.fn_decode(this.nativeHandle, this.decode_buffer.byteOffset, data.byteLength, this.decode_buffer.byteLength); + this.decode_buffer.set(buffer); + + let result = this.fn_decode(this.nativeHandle, this.decode_buffer.byteOffset, buffer.byteLength, this.decode_buffer.byteLength); if (result < 0) return OPUS_ERROR_CODES[-result] || "unknown decode error " + result; - return Module.HEAPF32.slice(this.decode_buffer.byteOffset / 4, (this.decode_buffer.byteOffset / 4) + (result * this.channelCount)); + const resultByteLength = result * this.channelCount * 4; + const resultBuffer = responseBuffer(resultByteLength); + resultBuffer.set(this.decode_buffer.subarray(0, resultByteLength), 0); + return resultByteLength; } - encode(data: Float32Array): Uint8Array | string { - this.encode_buffer.set(data); + encode(buffer: Uint8Array, responseBuffer: (length: number) => Uint8Array): number | string { + if (buffer.byteLength > this.decode_buffer.byteLength) + return "supplied data exceeds internal buffer"; - let result = this.fn_encode(this.nativeHandle, this.encode_buffer.byteOffset, data.length, this.encode_buffer.byteLength); + this.encode_buffer.set(buffer); + + let result = this.fn_encode(this.nativeHandle, this.encode_buffer.byteOffset, buffer.byteLength, this.encode_buffer.byteLength); if (result < 0) return OPUS_ERROR_CODES[-result] || "unknown encode error " + result; - return Module.HEAP8.slice(this.encode_buffer.byteOffset, this.encode_buffer.byteOffset + result); + const resultBuffer = responseBuffer(result); + resultBuffer.set(Module.HEAP8.subarray(this.encode_buffer.byteOffset, this.encode_buffer.byteOffset + result)); + return result; } reset() { diff --git a/web/native-codec/src/ilarvecon.cpp b/web/native-codec/src/ilarvecon.cpp new file mode 100644 index 00000000..0261504b --- /dev/null +++ b/web/native-codec/src/ilarvecon.cpp @@ -0,0 +1,68 @@ +// +// Created by WolverinDEV on 12/06/2020. +// + +/* source and target should not be intersecting! */ +template +void sequenced2interleaved(float* source, float* target, size_t sample_count) { + #pragma unroll + for(size_t channel = 0; channel < kChannelCount; channel++) { + auto src_ptr = source + channel * sample_count; + auto dest_ptr = target + channel; + + auto samples_left = sample_count; + while(samples_left--) { + *dest_ptr = *src_ptr; + + src_ptr++; + dest_ptr += kChannelCount; + } + } +} + +/* source and target should not be intersecting! */ +template +void interleaved2sequenced(float* source, float* target, size_t sample_count) { + #pragma unroll + for(size_t channel = 0; channel < kChannelCount; channel++) { + auto src_ptr = source + channel; + auto dest_ptr = target + channel * sample_count; + + auto samples_left = sample_count; + while(samples_left--) { + *dest_ptr = *src_ptr; + + src_ptr += kChannelCount; + dest_ptr++; + } + } +} + +#define kTempBufferSize (1024 * 8) + +/* since js is single threaded we need no lock here */ +float temp_buffer[kTempBufferSize]; + +template +void interleaved2sequenced_intersecting(float* buffer, size_t sample_count) { + auto temp = temp_buffer; + if(sample_count * kChannelCount > kTempBufferSize) + temp = (float*) malloc(sample_count * sizeof(float) * kChannelCount); + + memcpy(temp, buffer, sample_count * sizeof(float) * kChannelCount); + interleaved2sequenced(temp, buffer, sample_count); + if(temp != temp_buffer) + free(temp); +} + +template +void sequenced2interleaved_intersecting(float* buffer, size_t sample_count) { + auto temp = temp_buffer; + if(sample_count * kChannelCount > kTempBufferSize) + temp = (float*) malloc(sample_count * sizeof(float) * kChannelCount); + + memcpy(temp, buffer, sample_count * sizeof(float) * kChannelCount); + sequenced2interleaved(temp, buffer, sample_count); + if(temp != temp_buffer) + free(temp); +} \ No newline at end of file diff --git a/web/native-codec/src/opus.cpp b/web/native-codec/src/opus.cpp index bdc1030a..a3384f30 100644 --- a/web/native-codec/src/opus.cpp +++ b/web/native-codec/src/opus.cpp @@ -3,6 +3,7 @@ #include #include #include +#include "./ilarvecon.cpp" typedef std::unique_ptr opus_encoder_t; typedef std::unique_ptr opus_decoder_t; @@ -82,16 +83,22 @@ void codec_opus_deleteNativeHandle(OpusHandle *codec) { } EMSCRIPTEN_KEEPALIVE -int codec_opus_encode(OpusHandle *handle, uint8_t *buffer, size_t length, size_t maxLength) { - auto result = opus_encode_float(&*handle->encoder, (float *) buffer, length / handle->channelCount, buffer, maxLength); +int codec_opus_encode(OpusHandle *handle, uint8_t *buffer, size_t byte_length, size_t maxLength) { + if(handle->channelCount == 2) + sequenced2interleaved_intersecting<2>((float *) buffer, byte_length / (sizeof(float) * 2)); + + auto result = opus_encode_float(&*handle->encoder, (float *) buffer, byte_length / handle->channelCount, buffer, maxLength); if (result < 0) return result; return result; } EMSCRIPTEN_KEEPALIVE -int codec_opus_decode(OpusHandle *handle, uint8_t *buffer, size_t length, size_t maxLength) { - auto result = opus_decode_float(&*handle->decoder, buffer, length, (float *) buffer, maxLength / sizeof(float) / handle->channelCount, false); +int codec_opus_decode(OpusHandle *handle, uint8_t *buffer, size_t byte_length, size_t buffer_max_byte_length) { + auto result = opus_decode_float(&*handle->decoder, buffer, byte_length, (float *) buffer, buffer_max_byte_length / sizeof(float) / handle->channelCount, false); if (result < 0) return result; + + if(handle->channelCount == 2) + interleaved2sequenced_intersecting<2>((float *) buffer, result); return result; }