mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-01 12:53:49 +02:00
Implement PCM encoders
This commit is contained in:
+1
-1
@@ -914,7 +914,7 @@ export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2,
|
||||
}
|
||||
|
||||
if ((PCM_CODECS as readonly string[]).includes(codec)) {
|
||||
return false; // TODO write encoder
|
||||
return true; // Because we encode these ourselves
|
||||
}
|
||||
|
||||
if (typeof AudioEncoder === 'undefined') {
|
||||
|
||||
+3
-51
@@ -11,6 +11,7 @@ import {
|
||||
toAsyncIterator,
|
||||
validateAnyIterable,
|
||||
} from './misc';
|
||||
import { fromAlaw, fromUlaw } from './pcm';
|
||||
import { EncodedAudioSample, EncodedVideoSample } from './sample';
|
||||
|
||||
/** @public */
|
||||
@@ -890,58 +891,9 @@ class PcmAudioDecoderWrapper extends DecoderWrapper<EncodedAudioSample, AudioDat
|
||||
} else if (dataType === 'signed') {
|
||||
this.readInputValue = (view, byteOffset) => view.getInt8(byteOffset);
|
||||
} else if (dataType === 'ulaw') {
|
||||
// https://github.com/dystopiancode/pcm-g711/blob/master/pcm-g711/g711.c
|
||||
this.readInputValue = (view, byteOffset) => {
|
||||
const MULAW_BIAS = 33;
|
||||
let sign = 0;
|
||||
let position = 0;
|
||||
|
||||
// Get byte and invert
|
||||
let number = ~view.getUint8(byteOffset);
|
||||
|
||||
// Handle sign
|
||||
if (number & 0x80) {
|
||||
number &= ~(1 << 7);
|
||||
sign = -1;
|
||||
}
|
||||
|
||||
// Calculate position
|
||||
position = ((number & 0xF0) >> 4) + 5;
|
||||
|
||||
// Reconstruct linear value
|
||||
const decoded = ((1 << position) | ((number & 0x0F) << (position - 4))
|
||||
| (1 << (position - 5))) - MULAW_BIAS;
|
||||
|
||||
return (sign === 0) ? decoded : -decoded;
|
||||
};
|
||||
this.readInputValue = (view, byteOffset) => fromUlaw(view.getUint8(byteOffset));
|
||||
} else if (dataType === 'alaw') {
|
||||
this.readInputValue = (view, byteOffset) => {
|
||||
let sign = 0x00;
|
||||
let position = 0;
|
||||
|
||||
// Get byte and XOR with 0x55
|
||||
let number = view.getUint8(byteOffset) ^ 0x55;
|
||||
|
||||
// Handle sign
|
||||
if (number & 0x80) {
|
||||
number &= ~(1 << 7);
|
||||
sign = -1;
|
||||
}
|
||||
|
||||
// Calculate position
|
||||
position = ((number & 0xF0) >> 4) + 4;
|
||||
|
||||
// Reconstruct linear value
|
||||
let decoded = 0;
|
||||
if (position !== 4) {
|
||||
decoded = ((1 << position) | ((number & 0x0F) << (position - 4))
|
||||
| (1 << (position - 5)));
|
||||
} else {
|
||||
decoded = (number << 1) | 1;
|
||||
}
|
||||
|
||||
return (sign === 0) ? decoded : -decoded;
|
||||
};
|
||||
this.readInputValue = (view, byteOffset) => fromAlaw(view.getUint8(byteOffset));
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
+178
-39
@@ -5,17 +5,20 @@ import {
|
||||
buildVideoCodecString,
|
||||
getAudioEncoderConfigExtension,
|
||||
getVideoEncoderConfigExtension,
|
||||
parsePcmCodec,
|
||||
PCM_CODECS,
|
||||
PcmAudioCodec,
|
||||
SUBTITLE_CODECS,
|
||||
SubtitleCodec,
|
||||
VIDEO_CODECS,
|
||||
VideoCodec,
|
||||
} from './codec';
|
||||
import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from './output';
|
||||
import { assert } from './misc';
|
||||
import { assert, clamp, setInt24, setUint24 } from './misc';
|
||||
import { Muxer } from './muxer';
|
||||
import { SubtitleParser } from './subtitles';
|
||||
import { EncodedAudioSample, EncodedVideoSample } from './sample';
|
||||
import { toAlaw, toUlaw } from './pcm';
|
||||
|
||||
/** @public */
|
||||
export abstract class MediaSource {
|
||||
@@ -31,23 +34,23 @@ export abstract class MediaSource {
|
||||
/** @internal */
|
||||
_ensureValidDigest() {
|
||||
if (!this._connectedTrack) {
|
||||
throw new Error('Cannot call digest without connecting the source to an output track.');
|
||||
throw new Error('Source is not connected to an output track.');
|
||||
}
|
||||
|
||||
if (this._connectedTrack.output._canceled) {
|
||||
throw new Error('Cannot call digest after output has been canceled.');
|
||||
throw new Error('Output has been canceled.');
|
||||
}
|
||||
|
||||
if (!this._connectedTrack.output._started) {
|
||||
throw new Error('Cannot call digest before output has been started.');
|
||||
throw new Error('Output has not started.');
|
||||
}
|
||||
|
||||
if (this._connectedTrack.output._finalizing) {
|
||||
throw new Error('Cannot call digest after output has started finalizing.');
|
||||
throw new Error('Output is finalizing.');
|
||||
}
|
||||
|
||||
if (this._closed) {
|
||||
throw new Error('Cannot call digest after source has been closed.');
|
||||
throw new Error('Source is closed.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +129,7 @@ export type VideoEncodingConfig = {
|
||||
bitrate: number;
|
||||
latencyMode?: VideoEncoderConfig['latencyMode'];
|
||||
keyFrameInterval?: number;
|
||||
onEncodedChunk?: (chunk: EncodedVideoChunk, meta: EncodedVideoChunkMetadata | undefined) => unknown;
|
||||
onEncodedSample?: (chunk: EncodedVideoSample, meta: EncodedVideoChunkMetadata | undefined) => unknown;
|
||||
onEncodingError?: (error: Error) => unknown;
|
||||
};
|
||||
|
||||
@@ -149,7 +152,7 @@ const validateVideoEncodingConfig = (config: VideoEncodingConfig) => {
|
||||
) {
|
||||
throw new TypeError('config.keyFrameInterval, when provided, must be a non-negative number.');
|
||||
}
|
||||
if (config.onEncodedChunk !== undefined && typeof config.onEncodedChunk !== 'function') {
|
||||
if (config.onEncodedSample !== undefined && typeof config.onEncodedSample !== 'function') {
|
||||
throw new TypeError('config.onEncodedChunk, when provided, must be a function.');
|
||||
}
|
||||
if (config.onEncodingError !== undefined && typeof config.onEncodingError !== 'function') {
|
||||
@@ -215,12 +218,10 @@ class VideoEncoderWrapper {
|
||||
|
||||
this.encoder = new VideoEncoder({
|
||||
output: (chunk, meta) => {
|
||||
this.encodingConfig.onEncodedChunk?.(chunk, meta);
|
||||
void this.muxer!.addEncodedVideoSample(
|
||||
this.source._connectedTrack!,
|
||||
EncodedVideoSample.fromEncodedVideoChunk(chunk),
|
||||
meta,
|
||||
);
|
||||
const sample = EncodedVideoSample.fromEncodedVideoChunk(chunk);
|
||||
|
||||
this.encodingConfig.onEncodedSample?.(sample, meta);
|
||||
void this.muxer!.addEncodedVideoSample(this.source._connectedTrack!, sample, meta);
|
||||
},
|
||||
error: this.encodingConfig.onEncodingError ?? (error => console.error('VideoEncoder error:', error)),
|
||||
});
|
||||
@@ -420,7 +421,7 @@ export class EncodedAudioSampleSource extends AudioSource {
|
||||
export type AudioEncodingConfig = {
|
||||
codec: AudioCodec;
|
||||
bitrate?: number;
|
||||
onEncodedChunk?: (chunk: EncodedAudioChunk, meta: EncodedAudioChunkMetadata | undefined) => unknown;
|
||||
onEncodedSample?: (chunk: EncodedAudioSample, meta: EncodedAudioChunkMetadata | undefined) => unknown;
|
||||
onEncodingError?: (error: Error) => unknown;
|
||||
};
|
||||
|
||||
@@ -443,11 +444,16 @@ const validateAudioEncodingConfig = (config: AudioEncodingConfig) => {
|
||||
};
|
||||
|
||||
class AudioEncoderWrapper {
|
||||
private encoderInitialized = false;
|
||||
private encoder: AudioEncoder | null = null;
|
||||
private muxer: Muxer | null = null;
|
||||
private lastNumberOfChannels: number | null = null;
|
||||
private lastSampleRate: number | null = null;
|
||||
|
||||
private isPcmEncoder = false;
|
||||
private outputSampleSize: number | null = null;
|
||||
private writeOutputValue: ((view: DataView, byteOffset: number, value: number) => void) | null = null;
|
||||
|
||||
constructor(private source: AudioSource, private encodingConfig: AudioEncodingConfig) {
|
||||
validateAudioEncodingConfig(encodingConfig);
|
||||
}
|
||||
@@ -473,44 +479,177 @@ class AudioEncoderWrapper {
|
||||
}
|
||||
|
||||
this.ensureEncoder(audioData);
|
||||
assert(this.encoder);
|
||||
assert(this.encoderInitialized);
|
||||
|
||||
this.encoder.encode(audioData);
|
||||
if (this.isPcmEncoder) {
|
||||
await this.doPcmEncoding(audioData);
|
||||
} else {
|
||||
assert(this.encoder);
|
||||
this.encoder.encode(audioData);
|
||||
|
||||
if (this.encoder.encodeQueueSize >= 4) {
|
||||
await new Promise(resolve => this.encoder!.addEventListener('dequeue', resolve, { once: true }));
|
||||
if (this.encoder.encodeQueueSize >= 4) {
|
||||
await new Promise(resolve => this.encoder!.addEventListener('dequeue', resolve, { once: true }));
|
||||
}
|
||||
|
||||
await this.muxer!.mutex.currentPromise; // Allow the writer to apply backpressure
|
||||
}
|
||||
}
|
||||
|
||||
private async doPcmEncoding(audioData: AudioData) {
|
||||
assert(this.outputSampleSize);
|
||||
assert(this.writeOutputValue);
|
||||
|
||||
// All user agents are required to support conversion to f32-planar
|
||||
const allocationSize = audioData.allocationSize(({ planeIndex: 0, format: 'f32-planar' }));
|
||||
const floats = new Float32Array(allocationSize / Float32Array.BYTES_PER_ELEMENT);
|
||||
|
||||
const channelCount = audioData.numberOfChannels;
|
||||
const outputSize = audioData.numberOfFrames * channelCount * this.outputSampleSize;
|
||||
const outputBuffer = new ArrayBuffer(outputSize);
|
||||
const outputView = new DataView(outputBuffer);
|
||||
|
||||
for (let i = 0; i < channelCount; i++) {
|
||||
audioData.copyTo(floats, { planeIndex: i, format: 'f32-planar' });
|
||||
for (let j = 0; j < floats.length; j++) {
|
||||
// Write it interleaved... interleavedly?
|
||||
this.writeOutputValue(outputView, (j * channelCount + i) * this.outputSampleSize, floats[j]!);
|
||||
}
|
||||
}
|
||||
|
||||
await this.muxer!.mutex.currentPromise; // Allow the writer to apply backpressure
|
||||
const sample = new EncodedAudioSample(
|
||||
new Uint8Array(outputBuffer),
|
||||
'key',
|
||||
audioData.timestamp / 1e6,
|
||||
audioData.duration / 1e6,
|
||||
);
|
||||
const meta: EncodedAudioChunkMetadata = {
|
||||
decoderConfig: {
|
||||
codec: this.encodingConfig.codec,
|
||||
numberOfChannels: audioData.numberOfChannels,
|
||||
sampleRate: audioData.sampleRate,
|
||||
},
|
||||
};
|
||||
|
||||
this.encodingConfig.onEncodedSample?.(sample, meta);
|
||||
await this.muxer!.addEncodedAudioSample(this.source._connectedTrack!, sample, meta); // With backpressure
|
||||
}
|
||||
|
||||
private ensureEncoder(audioData: AudioData) {
|
||||
if (this.encoder) {
|
||||
if (this.encoderInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.encoder = new AudioEncoder({
|
||||
output: (chunk, meta) => {
|
||||
this.encodingConfig.onEncodedChunk?.(chunk, meta);
|
||||
void this.muxer!.addEncodedAudioSample(
|
||||
this.source._connectedTrack!,
|
||||
EncodedAudioSample.fromEncodedAudioChunk(chunk),
|
||||
meta,
|
||||
);
|
||||
},
|
||||
error: this.encodingConfig.onEncodingError ?? (error => console.error('AudioEncoder error:', error)),
|
||||
});
|
||||
if ((PCM_CODECS as readonly string[]).includes(this.encodingConfig.codec)) {
|
||||
this.initPcmEncoder();
|
||||
} else {
|
||||
this.encoder = new AudioEncoder({
|
||||
output: (chunk, meta) => {
|
||||
const sample = EncodedAudioSample.fromEncodedAudioChunk(chunk);
|
||||
|
||||
this.encoder.configure({
|
||||
codec: buildAudioCodecString(this.encodingConfig.codec, audioData.numberOfChannels, audioData.sampleRate),
|
||||
numberOfChannels: audioData.numberOfChannels,
|
||||
sampleRate: audioData.sampleRate,
|
||||
bitrate: this.encodingConfig.bitrate,
|
||||
...getAudioEncoderConfigExtension(this.encodingConfig.codec),
|
||||
});
|
||||
this.encodingConfig.onEncodedSample?.(sample, meta);
|
||||
void this.muxer!.addEncodedAudioSample(this.source._connectedTrack!, sample, meta);
|
||||
},
|
||||
error: this.encodingConfig.onEncodingError ?? (error => console.error('AudioEncoder error:', error)),
|
||||
});
|
||||
|
||||
this.encoder.configure({
|
||||
codec: buildAudioCodecString(
|
||||
this.encodingConfig.codec,
|
||||
audioData.numberOfChannels,
|
||||
audioData.sampleRate,
|
||||
),
|
||||
numberOfChannels: audioData.numberOfChannels,
|
||||
sampleRate: audioData.sampleRate,
|
||||
bitrate: this.encodingConfig.bitrate,
|
||||
...getAudioEncoderConfigExtension(this.encodingConfig.codec),
|
||||
});
|
||||
}
|
||||
|
||||
assert(this.source._connectedTrack);
|
||||
this.muxer = this.source._connectedTrack.output._muxer;
|
||||
|
||||
this.encoderInitialized = true;
|
||||
}
|
||||
|
||||
private initPcmEncoder() {
|
||||
this.isPcmEncoder = true;
|
||||
|
||||
const codec = this.encodingConfig.codec as PcmAudioCodec;
|
||||
const { dataType, sampleSize, littleEndian } = parsePcmCodec(codec);
|
||||
|
||||
this.outputSampleSize = sampleSize;
|
||||
|
||||
// All these functions receive a float sample as input and map it into the desired format
|
||||
|
||||
switch (sampleSize) {
|
||||
case 1: {
|
||||
if (dataType === 'unsigned') {
|
||||
this.writeOutputValue = (view, byteOffset, value) =>
|
||||
view.setUint8(byteOffset, clamp((value + 1) * 127.5, 0, 255));
|
||||
} else if (dataType === 'signed') {
|
||||
this.writeOutputValue = (view, byteOffset, value) => {
|
||||
view.setInt8(byteOffset, clamp(Math.round(value * 128), -128, 127));
|
||||
};
|
||||
} else if (dataType === 'ulaw') {
|
||||
this.writeOutputValue = (view, byteOffset, value) => {
|
||||
const int16 = clamp(Math.floor(value * 32767), -32768, 32767);
|
||||
view.setUint8(byteOffset, toUlaw(int16));
|
||||
};
|
||||
} else if (dataType === 'alaw') {
|
||||
this.writeOutputValue = (view, byteOffset, value) => {
|
||||
const int16 = clamp(Math.floor(value * 32767), -32768, 32767);
|
||||
view.setUint8(byteOffset, toAlaw(int16));
|
||||
};
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}; break;
|
||||
case 2: {
|
||||
if (dataType === 'unsigned') {
|
||||
this.writeOutputValue = (view, byteOffset, value) =>
|
||||
view.setUint16(byteOffset, clamp((value + 1) * 32767.5, 0, 65535), littleEndian);
|
||||
} else if (dataType === 'signed') {
|
||||
this.writeOutputValue = (view, byteOffset, value) =>
|
||||
view.setInt16(byteOffset, clamp(Math.round(value * 32767), -32768, 32767), littleEndian);
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}; break;
|
||||
case 3: {
|
||||
if (dataType === 'unsigned') {
|
||||
this.writeOutputValue = (view, byteOffset, value) =>
|
||||
setUint24(view, byteOffset, clamp((value + 1) * 8388607.5, 0, 16777215), littleEndian);
|
||||
} else if (dataType === 'signed') {
|
||||
this.writeOutputValue = (view, byteOffset, value) =>
|
||||
setInt24(
|
||||
view,
|
||||
byteOffset,
|
||||
clamp(Math.round(value * 8388607), -8388608, 8388607),
|
||||
littleEndian,
|
||||
);
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}; break;
|
||||
case 4: {
|
||||
if (dataType === 'unsigned') {
|
||||
this.writeOutputValue = (view, byteOffset, value) =>
|
||||
view.setUint32(byteOffset, clamp((value + 1) * 2147483647.5, 0, 4294967295), littleEndian);
|
||||
} else if (dataType === 'signed') {
|
||||
this.writeOutputValue = (view, byteOffset, value) =>
|
||||
view.setInt32(
|
||||
byteOffset,
|
||||
clamp(Math.round(value * 2147483647), -2147483648, 2147483647),
|
||||
littleEndian,
|
||||
);
|
||||
} else if (dataType === 'float') {
|
||||
this.writeOutputValue = (view, byteOffset, value) =>
|
||||
view.setFloat32(byteOffset, value, littleEndian);
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async flush() {
|
||||
|
||||
+32
@@ -269,6 +269,34 @@ export const getInt24 = (view: DataView, byteOffset: number, littleEndian: boole
|
||||
return getUint24(view, byteOffset, littleEndian) << 8 >> 8;
|
||||
};
|
||||
|
||||
export const setUint24 = (view: DataView, byteOffset: number, value: number, littleEndian: boolean) => {
|
||||
// Ensure the value is within 24-bit unsigned range (0 to 16777215)
|
||||
value = value >>> 0; // Convert to unsigned 32-bit
|
||||
value = value & 0xFFFFFF; // Mask to 24 bits
|
||||
|
||||
if (littleEndian) {
|
||||
view.setUint8(byteOffset, value & 0xFF);
|
||||
view.setUint8(byteOffset + 1, (value >>> 8) & 0xFF);
|
||||
view.setUint8(byteOffset + 2, (value >>> 16) & 0xFF);
|
||||
} else {
|
||||
view.setUint8(byteOffset, (value >>> 16) & 0xFF);
|
||||
view.setUint8(byteOffset + 1, (value >>> 8) & 0xFF);
|
||||
view.setUint8(byteOffset + 2, value & 0xFF);
|
||||
}
|
||||
};
|
||||
|
||||
export const setInt24 = (view: DataView, byteOffset: number, value: number, littleEndian: boolean) => {
|
||||
// Ensure the value is within 24-bit signed range (-8388608 to 8388607)
|
||||
value = clamp(value, -8388608, 8388607);
|
||||
|
||||
// Convert negative values to their 24-bit representation
|
||||
if (value < 0) {
|
||||
value = (value + 0x1000000) & 0xFFFFFF;
|
||||
}
|
||||
|
||||
setUint24(view, byteOffset, value, littleEndian);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls a function on each value spat out by an async generator. The reason for writing this manually instead of
|
||||
* using a generator function is that the generator function queues return() calls - here, we forward them immediately.
|
||||
@@ -297,3 +325,7 @@ export const mapAsyncGenerator = <T, U>(
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const clamp = (value: number, min: number, max: number) => {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
};
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
// https://github.com/dystopiancode/pcm-g711/blob/master/pcm-g711/g711.c
|
||||
export const toUlaw = (s16: number) => {
|
||||
const MULAW_MAX = 0x1FFF;
|
||||
const MULAW_BIAS = 33;
|
||||
|
||||
let number = s16;
|
||||
let mask = 0x1000;
|
||||
let sign = 0;
|
||||
let position = 12;
|
||||
let lsb = 0;
|
||||
|
||||
// Handle negative numbers
|
||||
if (number < 0) {
|
||||
number = -number;
|
||||
sign = 0x80;
|
||||
}
|
||||
|
||||
// Add bias
|
||||
number += MULAW_BIAS;
|
||||
|
||||
// Clip to maximum
|
||||
if (number > MULAW_MAX) {
|
||||
number = MULAW_MAX;
|
||||
}
|
||||
|
||||
// Find position of first 1 in the number
|
||||
for (; ((number & mask) !== mask && position >= 5); mask >>= 1, position--) {
|
||||
// Empty loop body - all work done in condition
|
||||
}
|
||||
|
||||
// Extract least significant bits
|
||||
lsb = (number >> (position - 4)) & 0x0f;
|
||||
|
||||
// Combine sign, position and lsb, then invert all bits
|
||||
return ~(sign | ((position - 5) << 4) | lsb) & 0xFF;
|
||||
};
|
||||
|
||||
export const fromUlaw = (u8: number) => {
|
||||
const MULAW_BIAS = 33;
|
||||
let sign = 0;
|
||||
let position = 0;
|
||||
|
||||
// Get byte and invert
|
||||
let number = ~u8;
|
||||
|
||||
// Handle sign
|
||||
if (number & 0x80) {
|
||||
number &= ~(1 << 7);
|
||||
sign = -1;
|
||||
}
|
||||
|
||||
// Calculate position
|
||||
position = ((number & 0xF0) >> 4) + 5;
|
||||
|
||||
// Reconstruct linear value
|
||||
const decoded = ((1 << position) | ((number & 0x0F) << (position - 4))
|
||||
| (1 << (position - 5))) - MULAW_BIAS;
|
||||
|
||||
return (sign === 0) ? decoded : -decoded;
|
||||
};
|
||||
|
||||
export const toAlaw = (s16: number) => {
|
||||
const ALAW_MAX = 0xFFF;
|
||||
let mask = 0x800;
|
||||
let sign = 0;
|
||||
let position = 11;
|
||||
let lsb = 0;
|
||||
|
||||
let number = s16;
|
||||
|
||||
// Handle negative numbers
|
||||
if (number < 0) {
|
||||
number = -number;
|
||||
sign = 0x80;
|
||||
}
|
||||
|
||||
// Clip to maximum
|
||||
if (number > ALAW_MAX) {
|
||||
number = ALAW_MAX;
|
||||
}
|
||||
|
||||
// Find position of first 1 in the number
|
||||
for (; ((number & mask) !== mask && position >= 5); mask >>= 1, position--) {
|
||||
// Empty loop body - all work done in condition
|
||||
}
|
||||
|
||||
// Extract least significant bits
|
||||
lsb = (number >> ((position === 4) ? 1 : (position - 4))) & 0x0f;
|
||||
|
||||
// Combine sign, position and lsb, then XOR with 0x55
|
||||
return (sign | ((position - 4) << 4) | lsb) ^ 0x55;
|
||||
};
|
||||
|
||||
export const fromAlaw = (u8: number) => {
|
||||
let sign = 0x00;
|
||||
let position = 0;
|
||||
|
||||
// Get byte and XOR with 0x55
|
||||
let number = u8 ^ 0x55;
|
||||
|
||||
// Handle sign
|
||||
if (number & 0x80) {
|
||||
number &= ~(1 << 7);
|
||||
sign = -1;
|
||||
}
|
||||
|
||||
// Calculate position
|
||||
position = ((number & 0xF0) >> 4) + 4;
|
||||
|
||||
// Reconstruct linear value
|
||||
let decoded = 0;
|
||||
if (position !== 4) {
|
||||
decoded = ((1 << position) | ((number & 0x0F) << (position - 4))
|
||||
| (1 << (position - 5)));
|
||||
} else {
|
||||
decoded = (number << 1) | 1;
|
||||
}
|
||||
|
||||
return (sign === 0) ? decoded : -decoded;
|
||||
};
|
||||
+1
-1
@@ -12,7 +12,7 @@ export abstract class Target {
|
||||
|
||||
/** @public */
|
||||
export class BufferTarget extends Target {
|
||||
buffer: Uint8Array | null = null;
|
||||
buffer: ArrayBuffer | null = null;
|
||||
|
||||
/** @internal */
|
||||
_createWriter() {
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ export class BufferTargetWriter extends Writer {
|
||||
|
||||
async finalize() {
|
||||
this.ensureSize(this.pos);
|
||||
this.target.buffer = this.bytes.subarray(0, Math.max(this.maxPos, this.pos));
|
||||
this.target.buffer = this.buffer.slice(0, Math.max(this.maxPos, this.pos));
|
||||
}
|
||||
|
||||
async close() {}
|
||||
|
||||
Reference in New Issue
Block a user