Cut a sound to a stretch of its file in its own Sound tab (#343)
game-maker / gate (push) Successful in 3m32s
game-maker / suite (push) Failing after 18m20s

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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-06 21:42:21 +02:00
1 parent 2fdf97f7de
commit f1f4a57589
24 files changed
+1442 -53

No files matched your search

+5 -5
View File
@@ -11,7 +11,7 @@ import { JOINT_CONNECTED_KEY } from '../../core/model/joint';
import { objectReference } from '../../core/model/objectRefs';
import { findAsset, findAssetOf, findScene, type Project } from '../../core/model/Project';
import { renderLayerAlong } from '../../core/model/renderLayers';
import { soundFileOf } from '../../core/model/sound';
import { playedSoundById, playedSoundOf } from '../../core/properties/playedSound';
import { hasRenderLayer } from '../../core/properties/PropertySchema';
import { resolveAssetProps } from '../../core/properties/resolve';
import { loopsOf, volumeOf } from '../../core/properties/sounds';
@@ -193,14 +193,14 @@ export function behaviorContext(
stopSong: (songId, fadeOut) => run.audio.stopSequence(songVoiceName(songId), false, heldFade(fadeOut)),
stopPlayback: (playback, fadeOut) => run.audio.stopPlayback(playback, heldFade(fadeOut)),
soundLength: (assetId) => {
const file = soundFileOf(project, assetId);
const file = playedSoundById(project, assetId);
return file ? run.audio.length(file) : 0;
},
// An id nothing resolves stops nothing, rather than silencing the whole run by accident;
// only the empty id means "everything", which is what the node promises.
soundTail: (voice) => run.audio.tail(voice),
stopSound: (assetId, letRing) => {
const file = assetId === '' ? '' : soundFileOf(project, assetId);
const file = playedSoundById(project, assetId);
if (assetId === '' || file) run.audio.stop(file, letRing);
},
stopVoice: (voice, letRing) => run.audio.stopVoice(voice, letRing),
@@ -270,7 +270,7 @@ function playSound(
const fx = fxId ? findAssetOf(project, fxId, 'soundfx') : undefined;
const plan = fx && soundFxBuildFor(fx).compiled;
return audio.play(
asset.file,
playedSoundOf(project, asset),
level,
loop ?? loopsOf(props),
owner,
@@ -404,7 +404,7 @@ export function playClip(
Math.min(dbToGain(SOUND_ITEM_MAX_DB), volumeOf(resolveAssetProps(project, asset)) * clip.volume),
);
return audio.playClip(
asset.file,
playedSoundOf(project, asset),
{ at: clip.at, offset: clip.offset, duration: clip.duration, volume, bus: clip.bus },
clipFx(project, clip.fx, clip.tempo),
);