import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* Test audio file generator
* Creates minimal valid audio file structures for testing
*/
export class TestAudioGenerator {
/**
* Create a minimal MP3 file with ID3 tag
*/
static async createTestMP3(filePath, options = {}) {
const {
title = 'Test Song',
artist = 'Test Artist',
duration = 10,
bitrate = 128
} = options;
// Basic ID3v2.3 header
const id3Header = Buffer.from([
0x49, 0x44, 0x33, // "ID3"
0x03, 0x00, // Version 2.3
0x00, // Flags
0x00, 0x00, 0x00, 0x7F // Size (127 bytes)
]);
// TIT2 frame (Title)
const titleFrame = this.createID3Frame('TIT2', title);
// TPE1 frame (Artist)
const artistFrame = this.createID3Frame('TPE1', artist);
// Minimal MP3 frame header (fake audio data)
const mp3Frame = Buffer.from([
0xFF, 0xFB, 0x90, 0x00, // MP3 frame sync + header
...Array(100).fill(0x00) // Dummy audio data
]);
const fileContent = Buffer.concat([id3Header, titleFrame, artistFrame, mp3Frame]);
await fs.writeFile(filePath, fileContent);
}
/**
* Create a minimal WAV file
*/
static async createTestWAV(filePath, options = {}) {
const {
sampleRate = 44100,
channels = 2,
bitsPerSample = 16,
durationSeconds = 1
} = options;
const dataSize = sampleRate * channels * (bitsPerSample / 8) * durationSeconds;
const fileSize = 36 + dataSize;
const header = Buffer.alloc(44);
// RIFF header
header.write('RIFF', 0);
header.writeUInt32LE(fileSize, 4);
header.write('WAVE', 8);
// fmt chunk
header.write('fmt ', 12);
header.writeUInt32LE(16, 16); // fmt chunk size
header.writeUInt16LE(1, 20); // audio format (PCM)
header.writeUInt16LE(channels, 22);
header.writeUInt32LE(sampleRate, 24);
header.writeUInt32LE(sampleRate * channels * (bitsPerSample / 8), 28); // byte rate
header.writeUInt16LE(channels * (bitsPerSample / 8), 32); // block align
header.writeUInt16LE(bitsPerSample, 34);
// data chunk
header.write('data', 36);
header.writeUInt32LE(dataSize, 40);
// Generate simple sine wave data
const audioData = Buffer.alloc(dataSize);
for (let i = 0; i < dataSize / 2; i++) {
const sample = Math.sin(2 * Math.PI * 440 * i / sampleRate) * 32767;
audioData.writeInt16LE(sample, i * 2);
}
const fileContent = Buffer.concat([header, audioData]);
await fs.writeFile(filePath, fileContent);
}
/**
* Create a minimal FLAC file
*/
static async createTestFLAC(filePath, options = {}) {
const {
sampleRate = 44100,
channels = 2,
bitsPerSample = 16
} = options;
// FLAC signature
const flacSignature = Buffer.from('fLaC', 'ascii');
// STREAMINFO metadata block (mandatory)
const streamInfo = Buffer.alloc(42);
streamInfo[0] = 0x80; // Last metadata block flag + block type (0)
streamInfo.writeUIntBE(34, 1, 3); // Block length
streamInfo.writeUInt16BE(16, 4); // Min block size
streamInfo.writeUInt16BE(4096, 6); // Max block size
streamInfo.writeUIntBE(0, 8, 3); // Min frame size
streamInfo.writeUIntBE(0, 11, 3); // Max frame size
streamInfo.writeUIntBE((sampleRate << 12) | ((channels - 1) << 9) | ((bitsPerSample - 1) << 4), 14, 4);
// Minimal frame data (just header)
const frameHeader = Buffer.from([0xFF, 0xF8, 0xC8, 0x00]);
const fileContent = Buffer.concat([flacSignature, streamInfo, frameHeader]);
await fs.writeFile(filePath, fileContent);
}
/**
* Create a minimal OGG file
*/
static async createTestOGG(filePath, options = {}) {
// OGG page header
const oggHeader = Buffer.from([
0x4F, 0x67, 0x67, 0x53, // "OggS"
0x00, // Version
0x02, // Type flag (first page of logical bitstream)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Granule position
0x01, 0x02, 0x03, 0x04, // Serial number
0x00, 0x00, 0x00, 0x00, // Page sequence number
0x00, 0x00, 0x00, 0x00, // Checksum (would need to be calculated)
0x01, // Page segments
0x1E // Segment table (30 bytes)
]);
// Minimal Vorbis identification header
const vorbisHeader = Buffer.from([
0x01, // Packet type
...Buffer.from('vorbis', 'ascii'), // "vorbis"
0x00, 0x00, 0x00, 0x00, // Version
0x02, // Channels
0x44, 0xAC, 0x00, 0x00, // Sample rate (44100)
...Array(16).fill(0x00) // Padding
]);
const fileContent = Buffer.concat([oggHeader, vorbisHeader]);
await fs.writeFile(filePath, fileContent);
}
/**
* Create an ID3 frame
*/
static createID3Frame(frameId, content) {
const contentBuffer = Buffer.from(content, 'utf8');
const frame = Buffer.alloc(10 + contentBuffer.length + 1);
frame.write(frameId, 0);
frame.writeUInt32BE(contentBuffer.length + 1, 4);
frame.writeUInt16BE(0, 8); // Flags
frame[10] = 0x00; // Text encoding (ISO-8859-1)
contentBuffer.copy(frame, 11);
return frame;
}
/**
* Create test files for comprehensive testing
*/
static async createTestSuite(assetsDir) {
await fs.mkdir(assetsDir, { recursive: true });
// Create various format samples
await this.createTestMP3(path.join(assetsDir, 'sample.mp3'), {
title: 'Sample MP3',
artist: 'Test Artist',
duration: 5
});
await this.createTestWAV(path.join(assetsDir, 'sample.wav'), {
sampleRate: 44100,
channels: 2,
durationSeconds: 2
});
await this.createTestFLAC(path.join(assetsDir, 'sample.flac'));
await this.createTestOGG(path.join(assetsDir, 'sample.ogg'));
// Create files with different characteristics
await this.createTestWAV(path.join(assetsDir, 'mono.wav'), {
channels: 1,
durationSeconds: 1
});
await this.createTestWAV(path.join(assetsDir, 'high-res.wav'), {
sampleRate: 96000,
bitsPerSample: 24,
durationSeconds: 1
});
await this.createTestMP3(path.join(assetsDir, 'long-track.mp3'), {
title: 'Long Track',
duration: 300 // 5 minutes
});
await this.createTestMP3(path.join(assetsDir, 'short-sfx.mp3'), {
title: 'Sound Effect',
duration: 2
});
// Create problematic files for error testing
await fs.writeFile(path.join(assetsDir, 'corrupted.mp3'), 'Not a real MP3 file');
await fs.writeFile(path.join(assetsDir, 'empty.wav'), '');
// Create a file with partial header
await fs.writeFile(path.join(assetsDir, 'partial.flac'), 'fLaC\x00');
console.log(`Test audio files created in ${assetsDir}`);
}
/**
* Clean up test files
*/
static async cleanupTestSuite(assetsDir) {
try {
const files = await fs.readdir(assetsDir);
for (const file of files) {
await fs.unlink(path.join(assetsDir, file));
}
await fs.rmdir(assetsDir);
console.log(`Test files cleaned up from ${assetsDir}`);
} catch (error) {
console.warn(`Cleanup warning: ${error.message}`);
}
}
}
// Create test files if run directly
if (import.meta.url === `file://${process.argv[1]}`) {
const assetsDir = path.join(__dirname, 'assets');
await TestAudioGenerator.createTestSuite(assetsDir);
}