Improve waveform peaks for audio player
Deploy To Ghaymah / deploy (push) Has been cancelled

This commit is contained in:
boutmoun123
2026-06-19 02:45:09 +03:00
parent 23c1a2d12b
commit e1edc1c848
5 changed files with 63 additions and 33 deletions
@@ -12,11 +12,15 @@ describe('post media response util', () => {
expect(media.mediaType).toBe(PostType.AUDIO); expect(media.mediaType).toBe(PostType.AUDIO);
expect(media.durationSeconds).toBe(42); expect(media.durationSeconds).toBe(42);
expect(media.waveformPeaks).toHaveLength(100); expect(media.waveformPeaks).toHaveLength(160);
expect(media.waveformPeaksPreview).toHaveLength(100); expect(media.waveformPeaksPreview).toHaveLength(160);
expect(media.waveformPeaksDetailed.length).toBeGreaterThanOrEqual(100); expect(media.waveformPeaksDetailed.length).toBeGreaterThanOrEqual(500);
expect(media.waveformPeaksDetailed.length).toBeLessThanOrEqual(1000);
expect(media.waveformPeaks).toEqual(media.waveformPeaksPreview);
expect(media.waveformPeaks.every((value) => Number.isInteger(value))).toBe(true); expect(media.waveformPeaks.every((value) => Number.isInteger(value))).toBe(true);
expect(media.waveformPeaks.every((value) => value >= 0 && value <= 100)).toBe(true); expect(media.waveformPeaks.every((value) => value >= 0 && value <= 100)).toBe(true);
expect(media.waveformPeaksDetailed.every((value) => Number.isInteger(value))).toBe(true);
expect(media.waveformPeaksDetailed.every((value) => value >= 0 && value <= 100)).toBe(true);
}); });
it('does not attach waveformPeaks to non-audio media', () => { it('does not attach waveformPeaks to non-audio media', () => {
+35 -14
View File
@@ -7,7 +7,7 @@ import {
normalizeWaveformPeaks, normalizeWaveformPeaks,
} from './waveform.util'; } from './waveform.util';
const expectDisplaySafeWaveform = (peaks: number[], expectedLength = 100) => { const expectDisplaySafeWaveform = (peaks: number[], expectedLength = 160) => {
expect(peaks).toHaveLength(expectedLength); expect(peaks).toHaveLength(expectedLength);
expect(peaks.every((value) => Number.isInteger(value))).toBe(true); expect(peaks.every((value) => Number.isInteger(value))).toBe(true);
expect(peaks.every((value) => value >= 0 && value <= 100)).toBe(true); expect(peaks.every((value) => value >= 0 && value <= 100)).toBe(true);
@@ -61,7 +61,7 @@ describe('waveform util', () => {
process.env.AUDIO_WAVEFORM_PEAKS = originalAudioWaveformPeaks; process.env.AUDIO_WAVEFORM_PEAKS = originalAudioWaveformPeaks;
}); });
it('returns exactly 100 values by default', () => { it('returns exactly 160 preview values by default', () => {
expectDisplaySafeWaveform(normalizeWaveformPeaks([10, 20, 30, 40])); expectDisplaySafeWaveform(normalizeWaveformPeaks([10, 20, 30, 40]));
}); });
@@ -71,19 +71,19 @@ describe('waveform util', () => {
expectDisplaySafeWaveform(peaks); expectDisplaySafeWaveform(peaks);
}); });
it('resamples short input to 100 values', () => { it('resamples short input to 160 values', () => {
const peaks = normalizeWaveformPeaks([5, 20, 60, 90, 30, 10]); const peaks = normalizeWaveformPeaks([5, 20, 60, 90, 30, 10]);
expectDisplaySafeWaveform(peaks); expectDisplaySafeWaveform(peaks);
}); });
it('downsamples long input to 100 values', () => { it('downsamples long input to 160 values', () => {
const input = Array.from({ length: 200 }, (_, index) => index + 1); const input = Array.from({ length: 200 }, (_, index) => index + 1);
expectDisplaySafeWaveform(normalizeWaveformPeaks(input)); expectDisplaySafeWaveform(normalizeWaveformPeaks(input));
}); });
it('returns fallback 100 values for empty input', () => { it('returns fallback 160 values for empty input', () => {
const peaks = normalizeWaveformPeaks([]); const peaks = normalizeWaveformPeaks([]);
expectDisplaySafeWaveform(peaks); expectDisplaySafeWaveform(peaks);
@@ -91,11 +91,11 @@ describe('waveform util', () => {
}); });
it('uses configured values only within the Flutter display-safe range', () => { it('uses configured values only within the Flutter display-safe range', () => {
process.env.AUDIO_WAVEFORM_PEAKS = '120'; process.env.AUDIO_WAVEFORM_PEAKS = '180';
expectDisplaySafeWaveform(normalizeWaveformPeaks([1, 2, 3]), 120); expectDisplaySafeWaveform(normalizeWaveformPeaks([1, 2, 3]), 180);
process.env.AUDIO_WAVEFORM_PEAKS = '48'; process.env.AUDIO_WAVEFORM_PEAKS = '100';
expectDisplaySafeWaveform(normalizeWaveformPeaks([1, 2, 3]), 100); expectDisplaySafeWaveform(normalizeWaveformPeaks([1, 2, 3]), 160);
}); });
it('generates display-safe peaks from audio buffers and seeds', () => { it('generates display-safe peaks from audio buffers and seeds', () => {
@@ -113,6 +113,21 @@ describe('waveform util', () => {
expect(peaks.slice(60).some((value) => value > 80)).toBe(true); expect(peaks.slice(60).some((value) => value > 80)).toBe(true);
}); });
it('keeps detailed peak counts within Flutter response limits', () => {
expect(getDetailedWaveformPeakCount(12)).toBe(500);
expect(getDetailedWaveformPeakCount(130)).toBeGreaterThanOrEqual(600);
expect(getDetailedWaveformPeakCount(130)).toBeLessThanOrEqual(1000);
expect(getDetailedWaveformPeakCount(600)).toBe(1000);
});
it('keeps silent WAV samples low instead of amplifying them into a fake waveform', () => {
const wavBuffer = createPcmWavBuffer(() => 0);
const peaks = generateWaveformPeaksFromBuffer(wavBuffer);
expectDisplaySafeWaveform(peaks);
expect(peaks.every((value) => value === 0)).toBe(true);
});
it('generates detailed peaks for long audio without changing preview size', () => { it('generates detailed peaks for long audio without changing preview size', () => {
const detailedCount = getDetailedWaveformPeakCount(180); const detailedCount = getDetailedWaveformPeakCount(180);
const waveformSet = buildWaveformPeakSet( const waveformSet = buildWaveformPeakSet(
@@ -121,20 +136,26 @@ describe('waveform util', () => {
); );
expect(detailedCount).toBeGreaterThanOrEqual(600); expect(detailedCount).toBeGreaterThanOrEqual(600);
expectDisplaySafeWaveform(waveformSet.waveformPeaksPreview, 100); expect(detailedCount).toBeLessThanOrEqual(1000);
expectDisplaySafeWaveform(waveformSet.waveformPeaks, 100); expectDisplaySafeWaveform(waveformSet.waveformPeaksPreview, 160);
expectDisplaySafeWaveform(waveformSet.waveformPeaks, 160);
expect(waveformSet.waveformPeaks).toEqual(waveformSet.waveformPeaksPreview);
expect(waveformSet.waveformPeaksDetailed.length).toBeGreaterThanOrEqual(600); expect(waveformSet.waveformPeaksDetailed.length).toBeGreaterThanOrEqual(600);
expect(waveformSet.waveformPeaksDetailed.length).toBeLessThanOrEqual(1200); expect(waveformSet.waveformPeaksDetailed.length).toBeLessThanOrEqual(1000);
expect(waveformSet.waveformPeaksDetailed.every((value) => Number.isInteger(value))).toBe(true);
expect(waveformSet.waveformPeaksDetailed.every((value) => value >= 0 && value <= 100)).toBe(true); expect(waveformSet.waveformPeaksDetailed.every((value) => value >= 0 && value <= 100)).toBe(true);
expect(new Set(waveformSet.waveformPeaksDetailed).size).toBeGreaterThan(1);
}); });
it('generates preview and detailed peak sets from buffers', () => { it('generates preview and detailed peak sets from buffers', () => {
const wavBuffer = createPcmWavBuffer((frame) => 0.15 + (frame / 7999) * 0.7); const wavBuffer = createPcmWavBuffer((frame) => 0.15 + (frame / 7999) * 0.7);
const waveformSet = generateWaveformPeakSetFromBuffer(wavBuffer, 240); const waveformSet = generateWaveformPeakSetFromBuffer(wavBuffer, 240);
expectDisplaySafeWaveform(waveformSet.waveformPeaksPreview, 100); expectDisplaySafeWaveform(waveformSet.waveformPeaksPreview, 160);
expect(waveformSet.waveformPeaks).toEqual(waveformSet.waveformPeaksPreview);
expect(waveformSet.waveformPeaksDetailed.length).toBeGreaterThanOrEqual(600); expect(waveformSet.waveformPeaksDetailed.length).toBeGreaterThanOrEqual(600);
expect(waveformSet.waveformPeaksDetailed.length).toBeLessThanOrEqual(1200); expect(waveformSet.waveformPeaksDetailed.length).toBeLessThanOrEqual(1000);
expect(waveformSet.waveformPeaksDetailed.every((value) => Number.isInteger(value))).toBe(true);
expect(new Set(waveformSet.waveformPeaksDetailed).size).toBeGreaterThan(8); expect(new Set(waveformSet.waveformPeaksDetailed).size).toBeGreaterThan(8);
}); });
}); });
+14 -11
View File
@@ -1,10 +1,11 @@
export const DEFAULT_WAVEFORM_PEAK_COUNT = 100; export const DEFAULT_WAVEFORM_PEAK_COUNT = 160;
export const DEFAULT_DETAILED_WAVEFORM_PEAK_COUNT = 800; export const DEFAULT_DETAILED_WAVEFORM_PEAK_COUNT = 800;
const MIN_DISPLAY_WAVEFORM_PEAKS = 80; const MIN_DISPLAY_WAVEFORM_PEAKS = 120;
const MAX_DISPLAY_WAVEFORM_PEAKS = 120; const MAX_DISPLAY_WAVEFORM_PEAKS = 180;
const MIN_DETAILED_WAVEFORM_PEAKS = 100; const MIN_DETAILED_WAVEFORM_PEAKS = 500;
const LONG_AUDIO_MIN_DETAILED_WAVEFORM_PEAKS = 600; const LONG_AUDIO_MIN_DETAILED_WAVEFORM_PEAKS = 600;
const MAX_DETAILED_WAVEFORM_PEAKS = 1200; const MAX_DETAILED_WAVEFORM_PEAKS = 1000;
const NEAR_SILENCE_THRESHOLD = 1;
export type WaveformPeakSet = { export type WaveformPeakSet = {
waveformPeaks: number[]; waveformPeaks: number[];
@@ -54,7 +55,7 @@ const scaleToRange = (values: number[]): number[] => {
const sorted = [...values].sort((left, right) => left - right); const sorted = [...values].sort((left, right) => left - right);
const max = sorted[sorted.length - 1] ?? 0; const max = sorted[sorted.length - 1] ?? 0;
if (max <= 0) { if (max <= NEAR_SILENCE_THRESHOLD) {
return values.map((value) => clampPeak(value)); return values.map((value) => clampPeak(value));
} }
@@ -267,13 +268,15 @@ export const generateWaveformPeaksFromSeed = (
hash = Math.imul(hash, 16777619); hash = Math.imul(hash, 16777619);
} }
const phase = (hash % 360) * (Math.PI / 180);
const peaks: number[] = []; const peaks: number[] = [];
let state = hash >>> 0;
for (let i = 0; i < samples; i += 1) { for (let i = 0; i < samples; i += 1) {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0; const progress = i / Math.max(1, samples - 1);
const base = 18 + (state % 65); const primary = Math.sin(progress * Math.PI * 12 + phase) * 24;
const accent = i % 6 === 0 ? 18 : i % 3 === 0 ? 10 : 0; const secondary = Math.sin(progress * Math.PI * 37 + phase / 2) * 11;
peaks.push(base + accent); const accent = i % 11 === 0 ? 16 : i % 7 === 0 ? 8 : 0;
peaks.push(42 + primary + secondary + accent);
} }
return normalizeWaveformPeaks(peaks, samples); return normalizeWaveformPeaks(peaks, samples);
+1 -1
View File
@@ -128,7 +128,7 @@ export default () => ({
}, },
audioProcessing: { audioProcessing: {
enabled: (process.env.AUDIO_PROCESSING_ENABLED ?? 'false').toLowerCase() === 'true', enabled: (process.env.AUDIO_PROCESSING_ENABLED ?? 'false').toLowerCase() === 'true',
waveformPeaks: Number(process.env.AUDIO_WAVEFORM_PEAKS ?? 100), waveformPeaks: Number(process.env.AUDIO_WAVEFORM_PEAKS ?? 160),
}, },
logging: { logging: {
level: process.env.LOG_LEVEL ?? 'log', level: process.env.LOG_LEVEL ?? 'log',
+6 -4
View File
@@ -142,12 +142,14 @@ describe('MediaService', () => {
expect(result.audioUrl).toBe('/uploads/ai-music/generated.wav'); expect(result.audioUrl).toBe('/uploads/ai-music/generated.wav');
expect(result.mimeType).toBe('audio/wav'); expect(result.mimeType).toBe('audio/wav');
expect(result.sizeBytes).toBe(audioBuffer.length); expect(result.sizeBytes).toBe(audioBuffer.length);
expect(result.waveformPeaks).toHaveLength(100); expect(result.waveformPeaks).toHaveLength(160);
expect(result.waveformPeaksPreview).toHaveLength(100); expect(result.waveformPeaksPreview).toHaveLength(160);
expect(result.waveformPeaksDetailed.length).toBeGreaterThanOrEqual(100); expect(result.waveformPeaks).toEqual(result.waveformPeaksPreview);
expect(result.waveformPeaksDetailed.length).toBeLessThanOrEqual(1200); expect(result.waveformPeaksDetailed.length).toBeGreaterThanOrEqual(500);
expect(result.waveformPeaksDetailed.length).toBeLessThanOrEqual(1000);
expect(result.waveformPeaksDetailed.every((value: number) => Number.isInteger(value))).toBe(true); expect(result.waveformPeaksDetailed.every((value: number) => Number.isInteger(value))).toBe(true);
expect(result.waveformPeaksDetailed.every((value: number) => value >= 0 && value <= 100)).toBe(true); expect(result.waveformPeaksDetailed.every((value: number) => value >= 0 && value <= 100)).toBe(true);
expect(new Set(result.waveformPeaksDetailed).size).toBeGreaterThan(1);
expect(storageService.resolveLocalFilePath).toHaveBeenCalledWith( expect(storageService.resolveLocalFilePath).toHaveBeenCalledWith(
'/uploads/ai-music/generated.wav', '/uploads/ai-music/generated.wav',
); );