Improve AI music prompts and audio metadata

This commit is contained in:
boutmoun123
2026-06-09 10:02:19 +03:00
parent 87cd42b706
commit 1dd5a20846
11 changed files with 876 additions and 14 deletions
+42
View File
@@ -10,6 +10,38 @@ const expectDisplaySafeWaveform = (peaks: number[], expectedLength = 100) => {
expect(peaks.every((value) => value >= 0 && value <= 100)).toBe(true);
};
const createPcmWavBuffer = (amplitudeByFrame: (frame: number) => number, frames = 8000) => {
const sampleRate = 8000;
const channels = 1;
const bitsPerSample = 16;
const bytesPerSample = bitsPerSample / 8;
const dataSize = frames * channels * bytesPerSample;
const buffer = Buffer.alloc(44 + dataSize);
buffer.write('RIFF', 0, 'ascii');
buffer.writeUInt32LE(36 + dataSize, 4);
buffer.write('WAVE', 8, 'ascii');
buffer.write('fmt ', 12, 'ascii');
buffer.writeUInt32LE(16, 16);
buffer.writeUInt16LE(1, 20);
buffer.writeUInt16LE(channels, 22);
buffer.writeUInt32LE(sampleRate, 24);
buffer.writeUInt32LE(sampleRate * channels * bytesPerSample, 28);
buffer.writeUInt16LE(channels * bytesPerSample, 32);
buffer.writeUInt16LE(bitsPerSample, 34);
buffer.write('data', 36, 'ascii');
buffer.writeUInt32LE(dataSize, 40);
for (let frame = 0; frame < frames; frame += 1) {
const tone = Math.sin((frame / sampleRate) * Math.PI * 2 * 440);
const amplitude = amplitudeByFrame(frame);
const sample = Math.max(-32768, Math.min(32767, Math.round(tone * amplitude * 32767)));
buffer.writeInt16LE(sample, 44 + frame * bytesPerSample);
}
return buffer;
};
describe('waveform util', () => {
const originalAudioWaveformPeaks = process.env.AUDIO_WAVEFORM_PEAKS;
@@ -67,4 +99,14 @@ describe('waveform util', () => {
expectDisplaySafeWaveform(generateWaveformPeaksFromBuffer(Buffer.from('fake audio bytes')));
expectDisplaySafeWaveform(generateWaveformPeaksFromSeed('audio-url'));
});
it('generates varied normalized peaks from actual WAV PCM samples', () => {
const wavBuffer = createPcmWavBuffer((frame) => 0.12 + (frame / 7999) * 0.78);
const peaks = generateWaveformPeaksFromBuffer(wavBuffer);
expectDisplaySafeWaveform(peaks);
expect(new Set(peaks).size).toBeGreaterThan(8);
expect(peaks.slice(0, 40).some((value) => value < 60)).toBe(true);
expect(peaks.slice(60).some((value) => value > 80)).toBe(true);
});
});
+120 -2
View File
@@ -29,12 +29,125 @@ const scaleToRange = (values: number[]): number[] => {
return [];
}
const max = Math.max(...values);
const sorted = [...values].sort((left, right) => left - right);
const max = sorted[sorted.length - 1] ?? 0;
if (max <= 0) {
return values.map((value) => clampPeak(value));
}
return values.map((value) => clampPeak((value / max) * 100));
const percentileIndex = Math.max(0, Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95)));
const scaleMax = Math.max(sorted[percentileIndex] ?? max, 1);
return values.map((value) => clampPeak((Math.min(value, scaleMax) / scaleMax) * 100));
};
const readPcmSample = (
buffer: Buffer,
offset: number,
bitsPerSample: number,
audioFormat: number,
): number => {
if (audioFormat === 3) {
if (bitsPerSample === 32 && offset + 4 <= buffer.length) {
return Math.max(-1, Math.min(1, buffer.readFloatLE(offset)));
}
if (bitsPerSample === 64 && offset + 8 <= buffer.length) {
return Math.max(-1, Math.min(1, buffer.readDoubleLE(offset)));
}
return 0;
}
switch (bitsPerSample) {
case 8:
return (buffer.readUInt8(offset) - 128) / 128;
case 16:
return buffer.readInt16LE(offset) / 32768;
case 24: {
if (offset + 3 > buffer.length) {
return 0;
}
let value = buffer.readUIntLE(offset, 3);
if (value & 0x800000) {
value |= 0xff000000;
}
return value / 8388608;
}
case 32:
return buffer.readInt32LE(offset) / 2147483648;
default:
return 0;
}
};
const generateWaveformPeaksFromWavBuffer = (buffer: Buffer, samples: number): number[] | null => {
if (buffer.length < 44 || buffer.toString('ascii', 0, 4) !== 'RIFF' || buffer.toString('ascii', 8, 12) !== 'WAVE') {
return null;
}
let offset = 12;
let audioFormat = 0;
let channels = 0;
let bitsPerSample = 0;
let dataOffset = 0;
let dataSize = 0;
while (offset + 8 <= buffer.length) {
const chunkId = buffer.toString('ascii', offset, offset + 4);
const chunkSize = buffer.readUInt32LE(offset + 4);
const chunkDataOffset = offset + 8;
if (chunkId === 'fmt ' && chunkDataOffset + 16 <= buffer.length) {
audioFormat = buffer.readUInt16LE(chunkDataOffset);
channels = buffer.readUInt16LE(chunkDataOffset + 2);
bitsPerSample = buffer.readUInt16LE(chunkDataOffset + 14);
} else if (chunkId === 'data') {
dataOffset = chunkDataOffset;
dataSize = Math.min(chunkSize, buffer.length - chunkDataOffset);
}
offset = chunkDataOffset + chunkSize + (chunkSize % 2);
}
const bytesPerSample = bitsPerSample / 8;
const frameSize = bytesPerSample * channels;
if (
!dataOffset ||
!dataSize ||
!channels ||
!Number.isInteger(bytesPerSample) ||
frameSize <= 0 ||
(audioFormat !== 1 && audioFormat !== 3)
) {
return null;
}
const frameCount = Math.floor(dataSize / frameSize);
if (frameCount <= 0) {
return null;
}
const bucketSize = Math.max(1, Math.ceil(frameCount / samples));
const peaks: number[] = [];
for (let bucketStart = 0; bucketStart < frameCount; bucketStart += bucketSize) {
const bucketEnd = Math.min(frameCount, bucketStart + bucketSize);
let sumSquares = 0;
let count = 0;
for (let frameIndex = bucketStart; frameIndex < bucketEnd; frameIndex += 1) {
const frameOffset = dataOffset + frameIndex * frameSize;
for (let channel = 0; channel < channels; channel += 1) {
const sampleOffset = frameOffset + channel * bytesPerSample;
const value = readPcmSample(buffer, sampleOffset, bitsPerSample, audioFormat);
sumSquares += value * value;
count += 1;
}
}
peaks.push(count ? Math.sqrt(sumSquares / count) * 100 : 0);
}
return normalizeWaveformPeaks(peaks, samples);
};
export const normalizeWaveformPeaks = (
@@ -93,6 +206,11 @@ export const generateWaveformPeaksFromBuffer = (
return normalizeWaveformPeaks([], samples);
}
const wavPeaks = generateWaveformPeaksFromWavBuffer(buffer, samples);
if (wavPeaks) {
return wavPeaks;
}
const chunkSize = Math.max(1, Math.ceil(buffer.length / samples));
const peaks: number[] = [];