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
@@ -1,5 +1,6 @@
|
||||
import type { Graph } from '../graph/Graph';
|
||||
import type { PatternNote } from '../music/pattern';
|
||||
import type { SongItem, SongTrack } from '../music/song';
|
||||
import type { LoopRange } from '../music/sequence';
|
||||
import type { PropertyDef } from '../properties/PropertySchema';
|
||||
import type { AssetType } from './AssetTypes';
|
||||
@@ -341,6 +342,18 @@ export interface PatternAsset extends AssetBase {
|
||||
loop?: LoopRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sounds and patterns on tracks along one time axis (#334). The tempo, the meter and the grid are
|
||||
* properties; the tracks, their items and the loop are the asset's own data, the whole song while
|
||||
* no loop is set.
|
||||
*/
|
||||
export interface SongAsset extends AssetBase {
|
||||
type: 'song';
|
||||
tracks: SongTrack[];
|
||||
items: SongItem[];
|
||||
loop?: LoopRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* A tile a tilemap paints with: `pictures` names one block per animation step (a texture id for a
|
||||
* sheet that holds nothing else, a frame id for one block cut out of a sheet), and `props` say
|
||||
@@ -596,6 +609,7 @@ export type Asset =
|
||||
| SoundFxAsset
|
||||
| InstrumentAsset
|
||||
| PatternAsset
|
||||
| SongAsset
|
||||
| ParticlesAsset;
|
||||
|
||||
/** The assets whose `variables` are declared by the user – see `requireVariableOwner`. */
|
||||
|
||||
@@ -27,6 +27,7 @@ export const ASSET_TYPES = [
|
||||
'soundfx',
|
||||
'instrument',
|
||||
'pattern',
|
||||
'song',
|
||||
'particles',
|
||||
] as const;
|
||||
|
||||
@@ -61,6 +62,7 @@ export const ASSET_TYPE_LABELS: Record<AssetType, string> = {
|
||||
soundfx: 'Sound FX',
|
||||
instrument: 'Instrument',
|
||||
pattern: 'Pattern',
|
||||
song: 'Song',
|
||||
particles: 'Particles',
|
||||
};
|
||||
|
||||
@@ -74,7 +76,7 @@ export const ASSET_TYPE_LABELS: Record<AssetType, string> = {
|
||||
* A struct is a shape that variables and fields take on (#253), nothing that exists on its own.
|
||||
* A shader function is part of other shaders (#303), worn by nothing itself. A sound FX is what a
|
||||
* sound is played through (#311), heard but never seen, an instrument what a note is played
|
||||
* with (#331) and a pattern the notes it plays (#333).
|
||||
* with (#331), a pattern the notes it plays (#333) and a song what lays them out (#334).
|
||||
*/
|
||||
export type PlaceableAssetType = Exclude<
|
||||
AssetType,
|
||||
@@ -94,6 +96,7 @@ export type PlaceableAssetType = Exclude<
|
||||
| 'soundfx'
|
||||
| 'instrument'
|
||||
| 'pattern'
|
||||
| 'song'
|
||||
| 'variables'
|
||||
| 'struct'
|
||||
>;
|
||||
@@ -115,6 +118,7 @@ const NOT_PLACEABLE: ReadonlySet<AssetType> = new Set([
|
||||
'soundfx',
|
||||
'instrument',
|
||||
'pattern',
|
||||
'song',
|
||||
'variables',
|
||||
'struct',
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Asset } from './Asset';
|
||||
import { isPlaceableType } from './AssetTypes';
|
||||
import { ASSET_TYPE_LABELS, isPlaceableType } from './AssetTypes';
|
||||
import { containerReaches } from './container';
|
||||
import type { ObjectHolder, Project } from './Project';
|
||||
|
||||
@@ -9,7 +9,9 @@ import type { ObjectHolder, Project } from './Project';
|
||||
* promise a placement the command then refuses.
|
||||
*/
|
||||
export function placementRejection(project: Project, holder: ObjectHolder, asset: Asset): string {
|
||||
if (!isPlaceableType(asset.type)) return 'A scene cannot be placed into a scene';
|
||||
// A pattern or a sound is dragged out of the panel for a song (#334), and may pass over a scene.
|
||||
if (!isPlaceableType(asset.type))
|
||||
return `A ${ASSET_TYPE_LABELS[asset.type].toLowerCase()} cannot be placed into a scene`;
|
||||
if (asset.type === 'container' && containerReaches(project, asset, holder.id))
|
||||
return `${asset.name} cannot contain ${holder.name}: that nests forever`;
|
||||
return '';
|
||||
|
||||
Reference in new issue
Block a user