A sound asset now opens in a tab that shows its file as a waveform. Two handles set where the sound starts and ends in the file, and that stretch is the sample: Play sound, a song item and an instrument's Sample all play it and nothing else. The file itself is never changed. - The asset has three new rows: Start (s), End (s) and Edge fade (ms). End 0 is the end of the file, the fade is 10 ms by default. Older assets have no values and play the whole file as before. - Every way of playing asks playedSoundOf() for a name instead of the file: the file, or <file>#<start>~<end>~<fade> for a cut. - SoundLibrary makes the cut from the decoded file the first time that name plays and ramps each edge inside the file over the fade. Length and peaks of a cut are read from the file without a copy. - Cut buffers can be made again at any time, so the library lets the oldest go past 64 Mi samples. A voice holds its buffers itself. - The tab draws min/max per pixel from 256-sample block peaks, and the samples as a line when zoomed in far. Wheel zooms, the middle button pans, as in the music tabs. Wave, cut and ruler sit under a mask, so a handle panned out of view does not draw over the panels beside. - Dragging a handle moves that edge, dragging elsewhere spans a new stretch. One drag is one undo step for both edges. Whole file puts the handles back, Play or Space plays the stretch with a playhead. - The inspector preview and the Sound FX preview play the cut too, and the song lanes draw and repeat it. Tests: - Unit: sounds 20, wave-view 8 (new), sound-library 68, plus 258 in the touched areas (instrument, song, pattern, inspector, tabs) - e2e 34 on melektaus-atx: ALL PASSED, 57 checks (7 new) - Gate on melektaus-atx: CHECK EXIT 0, 253 files, 3936 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
4.2 KiB
TypeScript
102 lines
4.2 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,
|
||
fitWave,
|
||
rulerLabel,
|
||
rulerStep,
|
||
secondsToX,
|
||
spanOf,
|
||
spanPeak,
|
||
storedCut,
|
||
wavePeaks,
|
||
xToSeconds,
|
||
zoomWave,
|
||
} from '../../src/editor/sound/waveView';
|
||
|
||
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 });
|
||
});
|
||
});
|