mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
* add a test * recognize as input format * scaffold flac demuxer * implement getting metadata * Implement mime type * read all metadata + deduplicate stubs * Read first packet * copyright headers * read the first packet * Get entire first packet, work on advancing * iterate over all samples * testable with bun * stub out metadata support * parse descriptive metadata * All in 1 file seems more appropriate to the philosophy * some parameters are not needed anymore all within 1 class * skip over bytes we are sure are not the syncword * run prettier * timestamp is determined based on passed blocks, not maximumBlockSize * no binary search needed! * simplifications * Finish demuxer reading sequentially * more tests + add a file with a seektable * don't throw if (this.audioInfo.minimumBlockSize !== this.audioInfo.maximumBlockSize * Add docs * Update format compatibility table * Simplification * confirm conversion is working * Finish * Resolve TODO comment * Support images (read-only) * Returning description as Uint8Array * Cleanup of demuxer * Misc renames * Object on same line * Resolve first batch of comments * Throw errors on corrupt blocks, correctly use requestSlice() * Fix description field * Explain why last frame is a bit shorter * Add FLAC to README * Put track backings below demuxer * convert to methods * Reorder container checking * getBlockSize() -> readBlockSize() * bitStream -> bitstream * better naming for bytes * use .skip() * Don't return blockSize twice in readFlacFrameHeader * Use enum + switch to distinguish Flac block types * Use else-if * Use else-if * Compressed switch statement * readCodedNumber * Update flac-misc.ts * We don't need the bits variable at all * reorder functions in flac-demuxer * Handle gracefully not being able to load another sample * Update flac-demuxer.ts * blockingbit null * use async instead of promise.resolve * use binary search * Replace recursion with while loop * Add mutex to getPacket() * Load more data not in getPacketAtIndex, but outside * Apply suggestion from @Vanilagy Co-authored-by: David P. <[email protected]> * computeDuration() reads last packet * Update flac-demuxer.ts * share vorbis comment reading logic * reuse vorbis comment writing logic, set vendor always to "Mediabunny" * flush after writign * Use FileSlice.tempFromBytes * assert !== null * fixing nitpicks * apply suggestions * fix ogg * We are now muxing images * apply suggestion * Update src/flac/flac-muxer.ts Co-authored-by: David P. <[email protected]> * apply suggestion * readSampleRate() * seek outside writeHeader() * mention vorbis metadata + add to metadatatags comment * should be able to -> can * Add test in metadata tags * Add test for packets being byte identical after remuxing * compare to null * no casting to uint8array * make test pass * Update flac-muxer.ts * Call validateAudioChunkMetadata() and validateAndNormalizeTimestamp() * `onFrame` option * emit frames using onFrame * Run prettier over files * Fix FLAC PICTURE block logic, small other changes * Update docs * Remove .only modifier * fix remuxing and add test * Don't throw error if parsing fails in header, since hitting a syncword might just be coincidential * Fix remaining type errors --------- Co-authored-by: David P. <[email protected]>
257 lines
7.9 KiB
TypeScript
257 lines
7.9 KiB
TypeScript
import { expect, test } from 'vitest';
|
|
import path from 'node:path';
|
|
import { assert, toUint8Array } from '../../src/misc.js';
|
|
import { Input } from '../../src/input.js';
|
|
import { BufferSource, FilePathSource } from '../../src/source.js';
|
|
import { ALL_FORMATS, FLAC } from '../../src/input-format.js';
|
|
import { EncodedPacketSink } from '../../src/media-sink.js';
|
|
import { Output } from '../../src/output.js';
|
|
import { BufferTarget } from '../../src/target.js';
|
|
import { FlacOutputFormat } from '../../src/output-format.js';
|
|
import { Conversion } from '../../src/conversion.js';
|
|
|
|
const __dirname = new URL('.', import.meta.url).pathname;
|
|
|
|
test('can loop over all samples', async () => {
|
|
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
|
const input = new Input({
|
|
source: new FilePathSource(filePath),
|
|
formats: ALL_FORMATS,
|
|
});
|
|
|
|
const track = await input.getPrimaryAudioTrack();
|
|
assert(track);
|
|
expect(await track.computeDuration()).toEqual(19.71428571428571);
|
|
expect(await track.getDecoderConfig()).toEqual({
|
|
codec: 'flac',
|
|
numberOfChannels: 2,
|
|
sampleRate: 44100,
|
|
description: new Uint8Array([
|
|
102, 76, 97, 67, 128, 0, 0, 34, 16, 0, 16, 0, 0, 6, 45, 0, 37, 173, 10,
|
|
196, 66, 240, 0, 13, 68, 24, 85, 22, 231, 0, 113, 139, 185, 1, 33, 54,
|
|
155, 80, 241, 191, 203, 112,
|
|
]),
|
|
});
|
|
expect(await track.getCodecParameterString()).toEqual('flac');
|
|
expect(track.timeResolution).toEqual(44100);
|
|
expect(await input.getMimeType()).toEqual('audio/flac');
|
|
|
|
const sink = new EncodedPacketSink(track);
|
|
let samples = 0;
|
|
let lastSampleTimestamp = 0;
|
|
for await (const sample of sink.packets()) {
|
|
samples++;
|
|
lastSampleTimestamp = sample.timestamp;
|
|
if (sample.sequenceNumber === 212) {
|
|
// Last frame is a bit shorter
|
|
// due it having a custom block size and the duration not being a multiple of the frame size
|
|
expect(sample.duration).toEqual(0.023764172335600908);
|
|
} else {
|
|
expect(sample.duration).toEqual(0.09287981859410431);
|
|
}
|
|
}
|
|
expect(samples).toBe(213);
|
|
expect(lastSampleTimestamp).toBe(19.690521541950112);
|
|
});
|
|
|
|
test('can do random access', async () => {
|
|
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
|
const input = new Input({
|
|
source: new FilePathSource(filePath),
|
|
formats: ALL_FORMATS,
|
|
});
|
|
|
|
const track = await input.getPrimaryAudioTrack();
|
|
assert(track);
|
|
const packetSink = new EncodedPacketSink(track);
|
|
|
|
const packet = await packetSink.getPacket(10);
|
|
assert(packet);
|
|
expect(packet.timestamp).toBe(9.93814058956916);
|
|
expect(packet.data.byteLength).toBe(8345);
|
|
expect(packet.sequenceNumber).toBe(107);
|
|
expect(packet.duration).toBe(0.09287981859410431);
|
|
|
|
const nextPacket = await packetSink.getNextPacket(packet);
|
|
assert(nextPacket);
|
|
expect(nextPacket.timestamp).toBe(10.031020408163265);
|
|
expect(nextPacket.data.byteLength).toBe(8988);
|
|
expect(nextPacket.sequenceNumber).toBe(108);
|
|
expect(nextPacket.duration).toBe(0.09287981859410431);
|
|
|
|
const priorPacket = await packetSink.getPacket(3);
|
|
assert(priorPacket);
|
|
expect(priorPacket.timestamp).toBe(2.972154195011338);
|
|
expect(priorPacket.data.byteLength).toBe(6877);
|
|
expect(priorPacket.sequenceNumber).toBe(32);
|
|
expect(priorPacket.duration).toBe(0.09287981859410431);
|
|
});
|
|
|
|
test('can get metadata-only packets', async () => {
|
|
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
|
const input = new Input({
|
|
source: new FilePathSource(filePath),
|
|
formats: ALL_FORMATS,
|
|
});
|
|
|
|
const track = await input.getPrimaryAudioTrack();
|
|
assert(track);
|
|
const packetSink = new EncodedPacketSink(track);
|
|
|
|
const packet = await packetSink.getPacket(10, { metadataOnly: true });
|
|
assert(packet);
|
|
expect(packet.timestamp).toBe(9.93814058956916);
|
|
expect(packet.isMetadataOnly).toBe(true);
|
|
expect(packet.sequenceNumber).toBe(107);
|
|
expect(packet.duration).toBe(0.09287981859410431);
|
|
});
|
|
|
|
test('can get metadata', async () => {
|
|
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
|
const input = new Input({
|
|
source: new FilePathSource(filePath),
|
|
formats: ALL_FORMATS,
|
|
});
|
|
|
|
const { images: inputImages, ...descriptiveMetadata } = await input.getMetadataTags();
|
|
|
|
expect(inputImages![0]!.data).toHaveLength(4852);
|
|
expect(inputImages![0]!.data.slice(0, 8)).toEqual(new Uint8Array([
|
|
137, 80, 78, 71, 13, 10, 26, 10,
|
|
]));
|
|
expect(inputImages![0]!.mimeType).toBe('image/png');
|
|
expect(inputImages![0]!.description).toBe('Album cover');
|
|
expect(inputImages![0]!.kind).toBe('coverFront');
|
|
|
|
expect(descriptiveMetadata).toEqual({
|
|
title: 'The Happy Meeting',
|
|
date: new Date('2020'),
|
|
album: 'Samples files',
|
|
artist: 'Samples Files',
|
|
trackNumber: 4,
|
|
genre: 'Ambient',
|
|
raw: {
|
|
ALBUM: 'Samples files',
|
|
ARTIST: 'Samples Files',
|
|
DATE: '2020',
|
|
ENCODER: 'Lavf58.76.100',
|
|
GENRE: 'Ambient',
|
|
TITLE: 'The Happy Meeting',
|
|
TRACKNUMBER: '4',
|
|
vendor: 'Lavf58.76.100',
|
|
},
|
|
});
|
|
});
|
|
|
|
test('can re-mux a .flac', async () => {
|
|
const filePath = path.join(__dirname, '..', 'public/sample.flac');
|
|
const input = new Input({
|
|
source: new FilePathSource(filePath),
|
|
formats: ALL_FORMATS,
|
|
});
|
|
|
|
let framesWritten = 0;
|
|
|
|
const output = new Output({
|
|
format: new FlacOutputFormat({
|
|
onFrame() {
|
|
framesWritten++;
|
|
},
|
|
}),
|
|
target: new BufferTarget(),
|
|
});
|
|
|
|
const conversion = await Conversion.init({ input, output });
|
|
await conversion.execute();
|
|
|
|
expect(framesWritten).toBe(213);
|
|
|
|
const buffer = output.target.buffer;
|
|
assert(buffer);
|
|
|
|
const outputAsInput = new Input({
|
|
source: new BufferSource(buffer),
|
|
formats: [FLAC],
|
|
});
|
|
|
|
const outputTrack = await outputAsInput.getPrimaryAudioTrack();
|
|
assert(outputTrack);
|
|
|
|
const inputTrack = await input.getPrimaryAudioTrack();
|
|
assert(inputTrack);
|
|
expect(inputTrack.sampleRate).toBe(outputTrack.sampleRate);
|
|
expect(inputTrack.numberOfChannels).toBe(outputTrack.numberOfChannels);
|
|
expect(inputTrack.timeResolution).toBe(outputTrack.timeResolution);
|
|
|
|
const outputMetadataTags = await outputAsInput.getMetadataTags();
|
|
|
|
const inputMetadataTags = await input.getMetadataTags();
|
|
expect(inputMetadataTags.images).toHaveLength(1);
|
|
expect(Object.keys(outputMetadataTags)).toEqual([
|
|
'images',
|
|
'raw',
|
|
'title',
|
|
'date',
|
|
'album',
|
|
'artist',
|
|
'trackNumber',
|
|
'genre',
|
|
]);
|
|
|
|
const { images: inputImages, ...otherInputMetadataTags } = inputMetadataTags;
|
|
|
|
expect(outputMetadataTags).toEqual({
|
|
...otherInputMetadataTags,
|
|
raw: {
|
|
...otherInputMetadataTags.raw,
|
|
vendor: 'Mediabunny',
|
|
},
|
|
images: inputImages,
|
|
});
|
|
|
|
const inputPacketSink = new EncodedPacketSink(inputTrack);
|
|
const outputPacketSink = new EncodedPacketSink(outputTrack);
|
|
let packets = 0;
|
|
let timestamp = 0;
|
|
for await (const packet of outputPacketSink.packets()) {
|
|
packets++;
|
|
timestamp = packet.timestamp;
|
|
}
|
|
|
|
expect(packets).toBe(213);
|
|
expect(timestamp).toBe(19.690521541950112);
|
|
|
|
// Test that packets are byte-identical
|
|
const inputPacket = await inputPacketSink.getPacket(10);
|
|
const outputPacket = await outputPacketSink.getPacket(10);
|
|
|
|
assert(inputPacket);
|
|
assert(outputPacket);
|
|
|
|
const { data: inputPacketData, ...otherInputPacket } = inputPacket;
|
|
const { data: outputPacketData, ...otherOutputPacket } = outputPacket;
|
|
|
|
expect(otherInputPacket).toEqual(otherOutputPacket);
|
|
expect(inputPacketData).toEqual(outputPacketData);
|
|
|
|
const inputDecoderConfig = await inputTrack.getDecoderConfig();
|
|
const outputDecoderConfig = await outputTrack.getDecoderConfig();
|
|
assert(inputDecoderConfig);
|
|
assert(outputDecoderConfig);
|
|
|
|
const { description: inputDescription, ...otherInputDecoderConfig } = inputDecoderConfig;
|
|
const { description: outputDescription, ...otherOutputDecoderConfig } = outputDecoderConfig;
|
|
|
|
assert(inputDescription);
|
|
assert(outputDescription);
|
|
|
|
const inputArray = toUint8Array(inputDescription);
|
|
const outputArray = toUint8Array(outputDescription);
|
|
|
|
const inputWithoutCrc = inputArray.slice(0, -16);
|
|
const outputWithoutCrc = outputArray.slice(0, -16);
|
|
expect(inputWithoutCrc).toEqual(outputWithoutCrc);
|
|
|
|
expect(otherInputDecoderConfig).toEqual(otherOutputDecoderConfig);
|
|
});
|