136 lines
4.0 KiB
JavaScript
136 lines
4.0 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Convert raw PCM to WAV format for easier playback and verification
|
|
function pcmToWav(pcmBuffer, outputPath, sampleRate = 24000, channels = 1, bitDepth = 16) {
|
|
const byteRate = sampleRate * channels * (bitDepth / 8);
|
|
const blockAlign = channels * (bitDepth / 8);
|
|
const dataSize = pcmBuffer.length;
|
|
|
|
// Create WAV header
|
|
const header = Buffer.alloc(44);
|
|
|
|
// "RIFF" chunk descriptor
|
|
header.write('RIFF', 0);
|
|
header.writeUInt32LE(dataSize + 36, 4); // File size - 8
|
|
header.write('WAVE', 8);
|
|
|
|
// "fmt " sub-chunk
|
|
header.write('fmt ', 12);
|
|
header.writeUInt32LE(16, 16); // Subchunk1Size (16 for PCM)
|
|
header.writeUInt16LE(1, 20); // AudioFormat (1 for PCM)
|
|
header.writeUInt16LE(channels, 22); // NumChannels
|
|
header.writeUInt32LE(sampleRate, 24); // SampleRate
|
|
header.writeUInt32LE(byteRate, 28); // ByteRate
|
|
header.writeUInt16LE(blockAlign, 32); // BlockAlign
|
|
header.writeUInt16LE(bitDepth, 34); // BitsPerSample
|
|
|
|
// "data" sub-chunk
|
|
header.write('data', 36);
|
|
header.writeUInt32LE(dataSize, 40); // Subchunk2Size
|
|
|
|
// Combine header and PCM data
|
|
const wavBuffer = Buffer.concat([header, pcmBuffer]);
|
|
|
|
// Write to file
|
|
fs.writeFileSync(outputPath, wavBuffer);
|
|
|
|
return outputPath;
|
|
}
|
|
|
|
// Analyze audio buffer for debugging
|
|
function analyzeAudioBuffer(buffer, label = 'Audio') {
|
|
const int16Array = new Int16Array(buffer.buffer, buffer.byteOffset, buffer.length / 2);
|
|
|
|
let minValue = 32767;
|
|
let maxValue = -32768;
|
|
let avgValue = 0;
|
|
let rmsValue = 0;
|
|
let silentSamples = 0;
|
|
|
|
for (let i = 0; i < int16Array.length; i++) {
|
|
const sample = int16Array[i];
|
|
minValue = Math.min(minValue, sample);
|
|
maxValue = Math.max(maxValue, sample);
|
|
avgValue += sample;
|
|
rmsValue += sample * sample;
|
|
|
|
if (Math.abs(sample) < 100) {
|
|
silentSamples++;
|
|
}
|
|
}
|
|
|
|
avgValue /= int16Array.length;
|
|
rmsValue = Math.sqrt(rmsValue / int16Array.length);
|
|
|
|
const silencePercentage = (silentSamples / int16Array.length) * 100;
|
|
|
|
console.log(`${label} Analysis:`);
|
|
console.log(` Samples: ${int16Array.length}`);
|
|
console.log(` Min: ${minValue}, Max: ${maxValue}`);
|
|
console.log(` Average: ${avgValue.toFixed(2)}`);
|
|
console.log(` RMS: ${rmsValue.toFixed(2)}`);
|
|
console.log(` Silence: ${silencePercentage.toFixed(1)}%`);
|
|
console.log(` Dynamic Range: ${20 * Math.log10(maxValue / (rmsValue || 1))} dB`);
|
|
|
|
return {
|
|
minValue,
|
|
maxValue,
|
|
avgValue,
|
|
rmsValue,
|
|
silencePercentage,
|
|
sampleCount: int16Array.length,
|
|
};
|
|
}
|
|
|
|
// Save audio buffer with metadata for debugging
|
|
function saveDebugAudio(buffer, type, timestamp = Date.now()) {
|
|
const homeDir = require('os').homedir();
|
|
const debugDir = path.join(homeDir, 'cheating-daddy-debug');
|
|
|
|
if (!fs.existsSync(debugDir)) {
|
|
fs.mkdirSync(debugDir, { recursive: true });
|
|
}
|
|
|
|
const pcmPath = path.join(debugDir, `${type}_${timestamp}.pcm`);
|
|
const wavPath = path.join(debugDir, `${type}_${timestamp}.wav`);
|
|
const metaPath = path.join(debugDir, `${type}_${timestamp}.json`);
|
|
|
|
// Save raw PCM
|
|
fs.writeFileSync(pcmPath, buffer);
|
|
|
|
// Convert to WAV for easy playback
|
|
pcmToWav(buffer, wavPath);
|
|
|
|
// Analyze and save metadata
|
|
const analysis = analyzeAudioBuffer(buffer, type);
|
|
fs.writeFileSync(
|
|
metaPath,
|
|
JSON.stringify(
|
|
{
|
|
timestamp,
|
|
type,
|
|
bufferSize: buffer.length,
|
|
analysis,
|
|
format: {
|
|
sampleRate: 24000,
|
|
channels: 1,
|
|
bitDepth: 16,
|
|
},
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
);
|
|
|
|
console.log(`Debug audio saved: ${wavPath}`);
|
|
|
|
return { pcmPath, wavPath, metaPath };
|
|
}
|
|
|
|
module.exports = {
|
|
pcmToWav,
|
|
analyzeAudioBuffer,
|
|
saveDebugAudio,
|
|
};
|