/** * 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, clipGrip, clipToPick, coversShown, draggedSpan, drawnSeconds, edgeAt, fileReach, fitWave, levelMarks, MAX_LEVEL, MIN_LEVEL, movedSpan, 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('what a press over the clips does (#403)', () => { const view = { panX: 0, pxPerSecond: 100 }; const picked = { start: 1, end: 2 }; it('spans a new clip anywhere while none is picked', () => { expect(clipGrip(view, null, true, 150)).toBe('new'); expect(clipGrip(view, null, false, 250)).toBe('new'); }); it('with one picked, takes an edge over the wave and the whole clip on its band, nothing else', () => { expect(clipGrip(view, picked, false, 101)).toBe('start'); expect(clipGrip(view, picked, false, 199)).toBe('end'); expect(clipGrip(view, picked, false, 150)).toBeNull(); expect(clipGrip(view, picked, false, 250)).toBeNull(); expect(clipGrip(view, picked, true, 150)).toBe('whole'); expect(clipGrip(view, picked, true, 101)).toBe('whole'); expect(clipGrip(view, picked, true, 250)).toBeNull(); }); it('picks the picked clip where it lies, else the band drawn last, else nothing', () => { const bands = [ { id: 'a', span: { start: 0, end: 1.5 } }, { id: 'b', span: { start: 1, end: 3 } }, ]; expect(clipToPick(view, bands, null, 120)).toBe('b'); expect(clipToPick(view, bands, 'a', 120)).toBe('a'); expect(clipToPick(view, bands, 'a', 200)).toBe('b'); expect(clipToPick(view, bands, 'b', 50)).toBe('a'); expect(clipToPick(view, bands, 'b', 350)).toBeNull(); }); it('moves a clip whole, its length kept and inside the file', () => { expect(movedSpan(picked, 0.5, 3)).toEqual({ start: 1.5, end: 2.5 }); expect(movedSpan(picked, 5, 3)).toEqual({ start: 2, end: 3 }); expect(movedSpan(picked, -5, 3)).toEqual({ start: 0, end: 1 }); }); }); 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 }); }); });