Improve AI music prompts and audio metadata
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user