Panning the Sound tab drew the whole wave anew on every move, one filled rectangle per pixel column, and gave the ruler's labels new texts whenever a tick left the area. At 2560 px across that took the tab down to 16 fps. - The wave is drawn half the area's width past either side of it. A pan within that moves the drawing. Zoom, height, size or a pan past the margin draw it anew. - The bars of the wave share one fill instead of one fill each. - A ruler label keeps the second it already shows. Before, a pan shifted every label by one and each had to be rendered anew. - A clip's name is given its style only when the style changes. - The rule for what a drawing covers is pure in `waveView.ts` (`shownSeconds`, `drawnSeconds`, `coversShown`). Measured with a copy of the user's project, "Take 1" with 4 clips, 240 pan moves with the middle button, headless on stein2: - 2560x1440: main thread 712 -> 373 ms/s, texts rendered 694 -> 65 - 1280x800: main thread 429 -> 225 ms/s What is left in the profile is Pixi's render pass and the pointer events, which hovering pays as well. Tests: - Unit wave-view: 12 green (1 new), counter-probe red - e2e 34 (stein2, melektaus-atx was down): ALL PASSED, 92 checks (1 new). Counter-probe with the labels given out by position: red, 7 labels set anew instead of 1. - Gate (stein2): 272 files, 4210 tests green Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
6.6 KiB
TypeScript
156 lines
6.6 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,
|
||
coversShown,
|
||
draggedSpan,
|
||
drawnSeconds,
|
||
edgeAt,
|
||
fileReach,
|
||
fitWave,
|
||
levelMarks,
|
||
MAX_LEVEL,
|
||
MIN_LEVEL,
|
||
rulerLabel,
|
||
rulerStep,
|
||
secondsToX,
|
||
shownSeconds,
|
||
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');
|
||
});
|
||
|
||
it('draws the wave past the area by a margin, and a pan within it keeps that drawing (#360)', () => {
|
||
// 20 s at 100 px/s, 1000 px across: seconds 5 to 15 shown, 0 to 20 with 500 px either side.
|
||
const view = { panX: -500, pxPerSecond: 100 };
|
||
expect(shownSeconds(view, 20, 1000)).toEqual({ start: 5, end: 15 });
|
||
const drawn = drawnSeconds(view, 20, 1000, 500);
|
||
expect(drawn).toEqual({ start: 0, end: 20 });
|
||
const near = drawnSeconds({ ...view, panX: -1000 }, 20, 1000, 300);
|
||
expect(near).toEqual({ start: 7, end: 20 });
|
||
// Panned 200 px it still covers what is shown, 400 px past it no longer.
|
||
expect(coversShown(near, shownSeconds({ ...view, panX: -800 }, 20, 1000))).toBe(true);
|
||
expect(coversShown(near, shownSeconds({ ...view, panX: -600 }, 20, 1000))).toBe(false);
|
||
// Nothing of the file in view: nothing to cover, and nothing drawn.
|
||
expect(shownSeconds({ panX: 1200, pxPerSecond: 100 }, 20, 1000)).toBeNull();
|
||
expect(coversShown(null, null)).toBe(true);
|
||
expect(coversShown(null, { start: 0, end: 1 })).toBe(false);
|
||
});
|
||
});
|
||
|
||
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 });
|
||
});
|
||
});
|