Lay patterns and sounds out on tracks and play them as a song #334
Patterns could only be heard one at a time, in their own tab or from a behavior. A song lays patterns and sounds out on tracks along one time axis and plays them through the scheduler of #332, the third piece of the music tools of #330. - New asset type song under the heading Songs, with four empty tracks, 120 BPM in 4/4 and a grid of a bar. Tempo, meter and grid are rows in the inspector. Tracks, items and the loop are the asset's data, held to their rules on load and in every command. - A track has a name, volume, pan, mute and solo. Each is heard on a bus of its own, so a change reaches what is sounding at once. - A pattern item repeats its pattern for as long as it is, with a transpose and another instrument if wanted. A sound item plays its sound as a clip at its own speed, round and round, through a sound FX if wanted. Both have a volume on top of their track's. - Cut back at the left end or split, an item starts as far into what it plays as the cut is: ticks into a pattern, seconds into a sound. - The tab shows the heads with M, S, volume and pan, the ruler of the Pattern tab and the items, a pattern with its notes, a sound with its waveform, a missing asset red. Patterns and sounds are dragged in from the Assets panel. Body, right and left end move, lengthen and cut back, Alt leaves the grid. Ctrl+C/V/D/A, Ctrl+E splits where playing starts, Delete, arrows. A double-click opens a pattern item in its tab. - What is picked is a new selection of its own, so the inspector shows the item or the track, with a button to delete it. - The scheduler plans clips beside notes. Where the playhead starts or jumps back inside a clip, it is taken up that far into its sound. A clip fades out at the end of its item or of the loop. - The editor plays one piece at a time: a pattern started stops the song and the other way round. - The rules are pure functions in core/music/song, editor/song/ songView and songEdit. Findings: - The suites of4ea752fand2a3b90bwere red at e2e 54: the Pattern pushed Struct below the fold of the + menu. The module now scrolls to it, as e2e 47 does for Vector art. It also left the menu open, which made e2e 55 fail behind it. - The Assets panel only dragged assets a scene can place. Patterns and sounds are dragged now as well, and a scene says why it does not take one instead of calling every asset a scene. Tests: - song.test.ts (new, 14) and song-edit.test.ts (new, 9): model, round trip, sequence of patterns and clips, mute and solo, cuts, clips on the scheduler, moving, sizing, trimming, splitting, duplicating, hits, head parts. sound-library.test.ts +5: clip offset, end fade, pitched clip, track bus, peaks. - e2e 65 (new) on melektaus-atx: ALL PASSED, 18 checks. e2e 54 and 55 with the fix: ALL PASSED, 33 checks. e2e 4 with 30 asset types: ALL PASSED, 62 checks. - Gate on melektaus-atx: CHECK EXIT 0, 250 test files, 3884 tests. - Counter-checks: no taking up of a clip, no pattern offset, no clip end each fail 3, 1 and 1 tests. - Probe on stein2: the tab with four tracks, before and while playing, looked at in two screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2a3b90b017
commit
8f9c250eb6
43 files changed
+3863
-36
No files matched your search
+17
-2
@@ -75,6 +75,7 @@ import { ShaderEditor } from './shader/ShaderEditor';
|
||||
import { SoundFxEditor } from './soundfx/SoundFxEditor';
|
||||
import { InstrumentEditor } from './instrument/InstrumentEditor';
|
||||
import { PatternEditor } from './pattern/PatternEditor';
|
||||
import { SongEditor } from './song/SongEditor';
|
||||
import { StateMachineEditor } from './statemachine/StateMachineEditor';
|
||||
import { installShortcuts } from './shortcuts';
|
||||
import type { PlayFrom } from '../core/model/playSettings';
|
||||
@@ -99,6 +100,7 @@ export type CentreTab =
|
||||
| 'soundfx'
|
||||
| 'instrument'
|
||||
| 'pattern'
|
||||
| 'song'
|
||||
| 'game'
|
||||
| 'texture'
|
||||
| 'vectorart'
|
||||
@@ -151,6 +153,7 @@ const PANE_ORDER = [
|
||||
'soundfx',
|
||||
'instrument',
|
||||
'pattern',
|
||||
'song',
|
||||
'texture',
|
||||
'vectorart',
|
||||
'pixelart',
|
||||
@@ -173,6 +176,7 @@ const TAB_LABELS: Record<CentreTab, string> = {
|
||||
soundfx: 'Sound FX',
|
||||
instrument: 'Instrument',
|
||||
pattern: 'Pattern',
|
||||
song: 'Song',
|
||||
texture: 'Texture',
|
||||
vectorart: 'Vector art',
|
||||
pixelart: 'Pixel art',
|
||||
@@ -224,6 +228,7 @@ export class Editor implements PlayHost {
|
||||
readonly soundFxEditor: SoundFxEditor;
|
||||
readonly instrumentEditor: InstrumentEditor;
|
||||
readonly patternEditor: PatternEditor;
|
||||
readonly songEditor: SongEditor;
|
||||
readonly textureEditor: TextureEditor;
|
||||
readonly vectorArtEditor: VectorArtEditor;
|
||||
readonly pixelArtEditor: PixelArtEditor;
|
||||
@@ -312,6 +317,7 @@ export class Editor implements PlayHost {
|
||||
this.soundFxEditor = new SoundFxEditor(state);
|
||||
this.instrumentEditor = new InstrumentEditor(state);
|
||||
this.patternEditor = new PatternEditor(state);
|
||||
this.songEditor = new SongEditor(state);
|
||||
this.textureEditor = new TextureEditor(state);
|
||||
this.vectorArtEditor = new VectorArtEditor(state);
|
||||
this.pixelArtEditor = new PixelArtEditor(state);
|
||||
@@ -423,6 +429,7 @@ export class Editor implements PlayHost {
|
||||
soundfx: this.soundFxEditor,
|
||||
instrument: this.instrumentEditor,
|
||||
pattern: this.patternEditor,
|
||||
song: this.songEditor,
|
||||
texture: this.textureEditor,
|
||||
vectorart: this.vectorArtEditor,
|
||||
pixelart: this.pixelArtEditor,
|
||||
@@ -495,6 +502,7 @@ export class Editor implements PlayHost {
|
||||
copy: () =>
|
||||
(this.tab === 'animation' && this.animationEditor.canvas.copyKeys()) ||
|
||||
(this.tab === 'pattern' && this.patternEditor.canvas.copy()) ||
|
||||
(this.tab === 'song' && this.songEditor.canvas.copy()) ||
|
||||
copySelection(state),
|
||||
paste: () => this.paste(),
|
||||
removePoint: () => this.removePoint(),
|
||||
@@ -514,7 +522,8 @@ export class Editor implements PlayHost {
|
||||
// creating it) is for – see the `open` event below.
|
||||
state.on('selection', () => {
|
||||
const s = state.selection;
|
||||
if (s?.kind === 'node' || s?.kind === 'frame') this.openDocument(`asset:${s.assetId}`);
|
||||
if (s?.kind === 'node' || s?.kind === 'frame' || s?.kind === 'song')
|
||||
this.openDocument(`asset:${s.assetId}`);
|
||||
if (s?.kind === 'object' && findAsset(state.project, s.holderId)?.type === 'container')
|
||||
this.openDocument(`asset:${s.holderId}`);
|
||||
});
|
||||
@@ -594,6 +603,8 @@ export class Editor implements PlayHost {
|
||||
return this.curveEditor.canvas.removePickedPoint();
|
||||
case 'pattern':
|
||||
return this.patternEditor.canvas.deleteSelection();
|
||||
case 'song':
|
||||
return this.songEditor.canvas.deleteSelection();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -622,6 +633,8 @@ export class Editor implements PlayHost {
|
||||
return this.instrumentEditor.canvas.paste();
|
||||
case 'pattern':
|
||||
return this.patternEditor.canvas.paste();
|
||||
case 'song':
|
||||
return this.songEditor.canvas.paste();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -1009,6 +1022,7 @@ export class Editor implements PlayHost {
|
||||
// The preview is the editor's own, so it runs on while the game is paused.
|
||||
if (this.tab === 'curve') this.curveEditor.tick(dt);
|
||||
if (this.tab === 'pattern') this.patternEditor.tick();
|
||||
if (this.tab === 'song') this.songEditor.tick();
|
||||
// Play repaints the machine tab itself (see `PlaySession.tick`); an editor run has no session
|
||||
// to do it, and its states move just the same.
|
||||
if (this.tab === 'statemachine' && this.editorRun.isRunning) this.stateMachineEditor.canvas.redraw();
|
||||
@@ -1042,7 +1056,8 @@ export class Editor implements PlayHost {
|
||||
(this.tab === 'animation' && this.animationEditor.isPlaying) ||
|
||||
(this.tab === 'tile' && this.tileEditor.isPlaying) ||
|
||||
(this.tab === 'curve' && this.curveEditor.isPlaying) ||
|
||||
(this.tab === 'pattern' && this.patternEditor.isPlaying)
|
||||
(this.tab === 'pattern' && this.patternEditor.isPlaying) ||
|
||||
(this.tab === 'song' && this.songEditor.isPlaying)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user