Zoomed out, the wave left room above and below it, but the bright file, the picked clip and its handles still took the whole height. And a click in the wave beside the picked clip kept it picked, so only the strip could take it back to the sound. - fileReach in waveView.ts is how far up and down the file reaches: full scale and its inset, never past the area. At level 1 it is the whole height, zoomed in too. - The bright file, the shade outside the clip and the two handles are drawn within that reach. - A click over the wave beside the picked clip picks the sound. Inside the clip or on a handle nothing changes, and a drag still spans the clip anew. Tests: - Unit wave-view: 11 green (1 new), a counter-check red - e2e 34 (melektaus-atx): ALL PASSED, 91 checks (1 new) - Gate (melektaus-atx): CHECK EXIT 0, 271 files, 4195 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
136 lines
5.5 KiB
TypeScript
136 lines
5.5 KiB
TypeScript
/**
|
||
* 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,
|
||
fileReach,
|
||
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);
|
||
});
|
||
|
||
it('lets the file and its clip reach as high as full scale zoomed out, never past the area', () => {
|
||
// Full scale 100 px from the middle at level 1, 6 px inset, 106 px of room.
|
||
expect(fileReach(1, 100, 6, 106)).toBe(106);
|
||
expect(fileReach(0.25, 100, 6, 106)).toBe(31);
|
||
expect(fileReach(4, 100, 6, 106)).toBe(106);
|
||
});
|
||
});
|
||
|
||
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 });
|
||
});
|
||
});
|