SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,170 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { convertAudioBytes, encodeSilk } from '@snowluma/protocol/highway/ffmpeg-addon';
|
||||
|
||||
interface NativeAudioAddon {
|
||||
convertToNTSilkTct(inputFile: string, outputFile: string): Promise<void>;
|
||||
decodeAudioToFmt(
|
||||
inputFile: string,
|
||||
outputFile: string,
|
||||
format: string,
|
||||
): Promise<{ result: boolean; sampleRate: number; channels: number; format: string }>;
|
||||
getDuration(filePath: string): Promise<number>;
|
||||
}
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function makeMonoWav(sampleRate = 24_000, durationSeconds = 1): Buffer {
|
||||
const samples = sampleRate * durationSeconds;
|
||||
const bytes = Buffer.alloc(44 + samples * 2);
|
||||
bytes.write('RIFF', 0);
|
||||
bytes.writeUInt32LE(36 + samples * 2, 4);
|
||||
bytes.write('WAVE', 8);
|
||||
bytes.write('fmt ', 12);
|
||||
bytes.writeUInt32LE(16, 16);
|
||||
bytes.writeUInt16LE(1, 20);
|
||||
bytes.writeUInt16LE(1, 22);
|
||||
bytes.writeUInt32LE(sampleRate, 24);
|
||||
bytes.writeUInt32LE(sampleRate * 2, 28);
|
||||
bytes.writeUInt16LE(2, 32);
|
||||
bytes.writeUInt16LE(16, 34);
|
||||
bytes.write('data', 36);
|
||||
bytes.writeUInt32LE(samples * 2, 40);
|
||||
|
||||
for (let i = 0; i < samples; i += 1) {
|
||||
bytes.writeInt16LE(
|
||||
Math.round(Math.sin((2 * Math.PI * 440 * i) / sampleRate) * 12_000),
|
||||
44 + i * 2,
|
||||
);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function loadBundledAddon(): NativeAudioAddon {
|
||||
const addonPath = path.resolve(
|
||||
__dirname,
|
||||
`../../../runtime/native/ffmpeg/ffmpegAddon.${process.platform}.${process.arch}.node`,
|
||||
);
|
||||
const mod = { exports: {} as Record<string, unknown> };
|
||||
process.dlopen(mod, addonPath);
|
||||
return mod.exports as unknown as NativeAudioAddon;
|
||||
}
|
||||
|
||||
function hasMp3Signature(bytes: Buffer): boolean {
|
||||
const hasId3Tag = bytes.length >= 3 && bytes.subarray(0, 3).toString('ascii') === 'ID3';
|
||||
const hasFrameSync = bytes.length >= 2 && bytes[0] === 0xff && (bytes[1] & 0xe0) === 0xe0;
|
||||
return hasId3Tag || hasFrameSync;
|
||||
}
|
||||
|
||||
const isReleaseTarget =
|
||||
(process.platform === 'win32' && process.arch === 'x64') ||
|
||||
(process.platform === 'linux' && (process.arch === 'x64' || process.arch === 'arm64'));
|
||||
const describeBundledAddon = isReleaseTarget ? describe : describe.skip;
|
||||
|
||||
describeBundledAddon('bundled native ffmpeg addon', () => {
|
||||
let tmpDir: string;
|
||||
beforeAll(() => {
|
||||
tmpDir = mkdtempSync(path.join(os.tmpdir(), 'sl-native-audio-test-'));
|
||||
});
|
||||
afterAll(() => rmSync(tmpDir, { recursive: true, force: true }));
|
||||
|
||||
describe.each(['silk', 'ntsilk', 'extensionless'] as const)('%s duration', (format) => {
|
||||
it.each([0.02, 1, 2.5])('measures %s seconds from the encoded frames', async (seconds) => {
|
||||
const addon = loadBundledAddon();
|
||||
const wavPath = path.join(tmpDir, `duration-${format}-${seconds}.wav`);
|
||||
const recordPath = path.join(
|
||||
tmpDir,
|
||||
`duration-${format}-${seconds}${format === 'extensionless' ? '' : `.${format}`}`,
|
||||
);
|
||||
writeFileSync(wavPath, makeMonoWav(24_000, seconds));
|
||||
await addon.convertToNTSilkTct(wavPath, recordPath);
|
||||
|
||||
if (format === 'silk') {
|
||||
// Standard SILK has the same packets without the QQ container prefix.
|
||||
const encoded = readFileSync(recordPath);
|
||||
expect(encoded.subarray(0, 10)).toEqual(
|
||||
Buffer.concat([Buffer.from([0x02]), Buffer.from('#!SILK_V3', 'ascii')]),
|
||||
);
|
||||
writeFileSync(recordPath, encoded.subarray(1));
|
||||
}
|
||||
|
||||
expect(await addon.getDuration(recordPath)).toBeCloseTo(seconds, 6);
|
||||
});
|
||||
|
||||
it('rejects a truncated packet instead of reporting a duration', async () => {
|
||||
const addon = loadBundledAddon();
|
||||
const wavPath = path.join(tmpDir, `truncated-${format}.wav`);
|
||||
const recordPath = path.join(
|
||||
tmpDir,
|
||||
`truncated-${format}${format === 'extensionless' ? '' : `.${format}`}`,
|
||||
);
|
||||
writeFileSync(wavPath, makeMonoWav());
|
||||
await addon.convertToNTSilkTct(wavPath, recordPath);
|
||||
const encoded = readFileSync(recordPath);
|
||||
writeFileSync(
|
||||
recordPath,
|
||||
encoded.subarray(format === 'silk' ? 1 : 0, encoded.length - 1),
|
||||
);
|
||||
|
||||
await expect(addon.getDuration(recordPath)).rejects.toThrow(/Invalid data/);
|
||||
await expect(encodeSilk(recordPath, tmpDir)).rejects.toThrow(/Invalid data/);
|
||||
});
|
||||
});
|
||||
|
||||
it('encodes a WAV with the runtime addon when started from the protocol package', async () => {
|
||||
const wavPath = path.join(tmpDir, 'runtime-input.wav');
|
||||
writeFileSync(wavPath, makeMonoWav());
|
||||
const result = await encodeSilk(wavPath, tmpDir);
|
||||
expect(result.converted).toBe(true);
|
||||
expect(result.duration).toBe(1);
|
||||
expect(readFileSync(result.path).subarray(1, 10).toString('ascii')).toBe('#!SILK_V3');
|
||||
expect(await loadBundledAddon().getDuration(result.path)).toBeCloseTo(1, 6);
|
||||
});
|
||||
|
||||
it('converts a mono QQ-style SILK record to a decodable MP3 through the public transcode seam', async () => {
|
||||
const addon = loadBundledAddon();
|
||||
const wavPath = path.join(tmpDir, 'mono-input.wav');
|
||||
const silkPath = path.join(tmpDir, 'mono-input.ntsilk');
|
||||
writeFileSync(wavPath, makeMonoWav());
|
||||
await addon.convertToNTSilkTct(wavPath, silkPath);
|
||||
|
||||
const result = await convertAudioBytes(readFileSync(silkPath), 'mp3', { addon, tmpDir });
|
||||
const mp3 = Buffer.from(result.base64, 'base64');
|
||||
const outputPath = path.join(tmpDir, 'mono-output.mp3');
|
||||
writeFileSync(outputPath, mp3);
|
||||
|
||||
expect(result.size).toBe(mp3.length);
|
||||
expect(hasMp3Signature(mp3)).toBe(true);
|
||||
const duration = await addon.getDuration(outputPath);
|
||||
expect(duration).toBeGreaterThan(0.9);
|
||||
expect(duration).toBeLessThan(1.2);
|
||||
});
|
||||
|
||||
it('converts the QQ AI SILK container variant through the public transcode seam', async () => {
|
||||
const addon = loadBundledAddon();
|
||||
const wavPath = path.join(tmpDir, 'ai-input.wav');
|
||||
const silkPath = path.join(tmpDir, 'ai-input.ntsilk');
|
||||
writeFileSync(wavPath, makeMonoWav());
|
||||
await addon.convertToNTSilkTct(wavPath, silkPath);
|
||||
|
||||
const aiSilk = readFileSync(silkPath);
|
||||
expect(aiSilk.subarray(0, 10)).toEqual(
|
||||
Buffer.concat([Buffer.from([0x02]), Buffer.from('#!SILK_V3', 'ascii')]),
|
||||
);
|
||||
aiSilk[0] = 0x03;
|
||||
|
||||
const result = await convertAudioBytes(aiSilk, 'mp3', { addon, tmpDir });
|
||||
const mp3 = Buffer.from(result.base64, 'base64');
|
||||
const outputPath = path.join(tmpDir, 'ai-output.mp3');
|
||||
writeFileSync(outputPath, mp3);
|
||||
|
||||
expect(hasMp3Signature(mp3)).toBe(true);
|
||||
const duration = await addon.getDuration(outputPath);
|
||||
expect(duration).toBeGreaterThan(0.9);
|
||||
expect(duration).toBeLessThan(1.2);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user