Files
feuerwerk/tests/editor/wave-view.test.ts
T
adminandClaude Opus 5.5 c7fa1d15b0 Zoom the height of a sound's wave over its level scale (#357)
The Sound tab always drew the wave at the full height of the tab. Quiet
stretches were hard to see, and there was no room above a loud wave.

- A level scale in dB stands left of the wave, a mark every 6 dB down
  from 0, as long as a mark lies inside the area and at least 14 px
  from the middle.
- The wheel over the scale zooms the height, from 0.25x to 16x. Over
  the wave it zooms the time as before.
- What is louder than the area stops at its edge. The fade ramps grow
  with the wave.
- Fit puts the height back to 1x. The tab keeps the height with its
  view when another tab is shown.
- The rules live in waveView.ts (zoomLevel, levelMarks), the hooks
  give the height, the scale and the top of a full-scale wave.

Tests:
- Unit wave-view: 10 green (2 new), 2 counter-checks each red
- e2e 34 (melektaus-atx): ALL PASSED, 87 checks (1 new)
- Gate (melektaus-atx): CHECK EXIT 0, 270 files, 4183 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:21:02 +02:00

128 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The Sound tab's view (#343): seconds across, zoomed about the pointer, and the stretch of the
* file between two handles – how a drag moves it and what the asset then stores.
*/
import { describe, expect, it } from 'vitest';
import {
clampWave,
draggedSpan,
edgeAt,
fitWave,
levelMarks,
MAX_LEVEL,
MIN_LEVEL,
rulerLabel,
rulerStep,
secondsToX,
spanOf,
spanPeak,
storedCut,
wavePeaks,
xToSeconds,
zoomLevel,
zoomWave,
} from '../../src/editor/sound/waveView';
describe('the height of the wave (#357)', () => {
it('zooms between a quarter and sixteen times full scale', () => {
expect(zoomLevel(1, 2)).toBe(2);
expect(zoomLevel(1, 0.5)).toBe(0.5);
expect(zoomLevel(0.3, 0.5)).toBe(MIN_LEVEL);
expect(zoomLevel(12, 2)).toBe(MAX_LEVEL);
});
it('marks every 6 dB that lies inside the area and not too close to the middle', () => {
// Level 1, full scale 100 px from the middle: 0 dB at 100, -6 at 50, -12 at 25, -18 at 12.6.
const marks = levelMarks(1, 100, 110, 14);
expect(marks.map((m) => m.db)).toEqual([0, -6, -12]);
expect(marks[1]!.offset).toBeCloseTo(50.1, 1);
// Zoomed out, -6 dB lies 12.5 px from the middle and is left out.
expect(levelMarks(0.25, 100, 110, 14).map((m) => m.db)).toEqual([0]);
// Zoomed in, 0 dB lies past the edge and the quiet ones show.
const loud = levelMarks(16, 100, 110, 14).map((m) => m.db);
expect(loud[0]).toBe(-24);
expect(loud).not.toContain(0);
});
});
describe('the wave view', () => {
it('zooms about the pointer and fits the file with a margin either side', () => {
const view = { panX: 10, pxPerSecond: 100 };
const zoomed = zoomWave(view, 2, 210);
expect(xToSeconds(zoomed, 210)).toBeCloseTo(xToSeconds(view, 210));
expect(zoomed.pxPerSecond).toBe(200);
const fitted = fitWave(4, 832, 16);
expect(secondsToX(fitted, 0)).toBe(16);
expect(secondsToX(fitted, 4)).toBe(816);
});
it('keeps either end of the file from going further in than half the width', () => {
expect(clampWave({ panX: 900, pxPerSecond: 100 }, 20, 1000).panX).toBe(500);
expect(clampWave({ panX: -1600, pxPerSecond: 100 }, 20, 1000).panX).toBe(-1500);
expect(clampWave({ panX: -900, pxPerSecond: 100 }, 20, 1000).panX).toBe(-900);
// Two seconds fit: they stay on screen.
expect(clampWave({ panX: 20, pxPerSecond: 100 }, 2, 1000).panX).toBe(20);
expect(clampWave({ panX: -50, pxPerSecond: 100 }, 2, 1000).panX).toBe(0);
});
it('labels the ruler in steps of 1, 2 or 5, with the decimals the step needs', () => {
expect(rulerStep(100, 70)).toBe(1);
expect(rulerStep(40, 70)).toBe(2);
expect(rulerStep(1000, 70)).toBeCloseTo(0.1);
expect(rulerStep(10, 70)).toBe(10);
expect(rulerLabel(1.5, 0.5)).toBe('1.5s');
expect(rulerLabel(2, 1)).toBe('2s');
expect(rulerLabel(0.25, 0.05)).toBe('0.25s');
expect(rulerLabel(75, 5)).toBe('1:15');
});
});
describe('the cut span', () => {
const view = { panX: 0, pxPerSecond: 100 };
it('reads an end of 0 or past the file as its end', () => {
expect(spanOf(0, 0, 3)).toEqual({ start: 0, end: 3 });
expect(spanOf(1, 9, 3)).toEqual({ start: 1, end: 3 });
expect(spanOf(2.5, 1, 3)).toEqual({ start: 1, end: 1 });
});
it('finds the handle in reach, the nearer one where both are', () => {
const span = { start: 1, end: 2 };
expect(edgeAt(view, span, 103)).toBe('start');
expect(edgeAt(view, span, 197)).toBe('end');
expect(edgeAt(view, span, 150)).toBeNull();
expect(edgeAt(view, { start: 1, end: 1.04 }, 103)).toBe('end');
});
it('moves one edge, never past the other, or spans a new stretch inside the file', () => {
const span = { start: 1, end: 2 };
expect(draggedSpan(span, 'start', 1, 0.5, 3, 0.001)).toEqual({ start: 0.5, end: 2 });
expect(draggedSpan(span, 'start', 1, 2.5, 3, 0.001)).toEqual({ start: 1.999, end: 2 });
expect(draggedSpan(span, 'end', 2, 9, 3, 0.001)).toEqual({ start: 1, end: 3 });
expect(draggedSpan(span, null, 2.5, 0.5, 3, 0.001)).toEqual({ start: 0.5, end: 2.5 });
expect(draggedSpan(span, null, -1, 1, 3, 0.001)).toEqual({ start: 0, end: 1 });
});
it('stores the ends of the file as 0, everything else to the sample', () => {
expect(storedCut({ start: 0, end: 3 }, 3, 48000)).toEqual({ start: 0, end: 0 });
expect(storedCut({ start: 0.000001, end: 2.999999 }, 3, 48000)).toEqual({ start: 0, end: 0 });
expect(storedCut({ start: 1.2345678, end: 2 }, 3, 48000)).toEqual({ start: 1.234563, end: 2 });
});
});
describe('the wave peaks', () => {
it('reads the lowest and highest sample of any stretch, over the blocks and the samples at its ends', () => {
const left = new Float32Array(1000);
const right = new Float32Array(1000);
left[10] = 0.5;
right[600] = -0.75;
left[999] = 0.9;
const peaks = wavePeaks([left, right]);
expect(peaks.max).toHaveLength(4);
expect(spanPeak([left, right], peaks, 0, 1000)).toEqual({ min: -0.75, max: Math.fround(0.9) });
expect(spanPeak([left, right], peaks, 5, 20)).toEqual({ min: 0, max: 0.5 });
expect(spanPeak([left, right], peaks, 11, 599)).toEqual({ min: 0, max: 0 });
expect(spanPeak([left, right], peaks, 300.2, 300.4)).toEqual({ min: 0, max: 0 });
});
});