Pick a song's sign in one row, and let its patterns keep theirs (#363)
A song had two rows for its meter, Beats per bar and Beat, and the patterns it kept to itself took both from it on every change. Their length is counted in bars, so a new sign cut or stretched them and their items. - The song has one row Sign with 1/2, 2/2, 3/2, 1/4 to 6/4 and 5/8 to 9/8, 4/4 by default. It is stored as `sign`. A value not in the list reads as 4/4. - An embedded pattern keeps its own tempo and meter, as a project pattern does. Its inspector shows Tempo, Beats per bar and Beat again and hides only Bus. - A pattern made in a song still starts with the song's tempo and sign, so its first bar fits the item it gets. - A copy (Embed a copy, a paste from another song) keeps its clock. `clockFollowers` and the follow step in `AssetPropertyCommand` are gone. - No load migration. The two songs of the user's project were moved from `beats`/`beat` to `sign` by hand. No other project had a song with a meter of its own. Tests: - Unit song + embedded + pattern: 44 green, 1 new, 4 rewritten. Counter-probes: a copy taking the song's clock and a sign outside the list both turn a test red. - e2e 65 (melektaus-atx): ALL PASSED, 57 checks (1 new: Sign picked from its list in the inspector). - Gate (melektaus-atx): CHECK EXIT 0, 272 files, 4214 tests. After it, `SIGNS` lost its `as const` and the cast went with it. Typecheck, lint and the 44 unit tests ran again, all green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d0b76ec355
commit
0d57a2f3c4
7 files changed
+109
-92
No files matched your search
+22
-31
@@ -10,7 +10,6 @@ import {
|
||||
DeleteAssetCommand,
|
||||
DuplicateAssetCommand,
|
||||
MakeProjectAssetCommand,
|
||||
ResetAssetPropertyCommand,
|
||||
SetAssetPropertyCommand,
|
||||
} from '../../src/core/commands/assetCommands';
|
||||
import { nextAssetName } from '../../src/core/commands/naming';
|
||||
@@ -29,7 +28,7 @@ import {
|
||||
songClockProps,
|
||||
} from '../../src/core/music/embedded';
|
||||
import { patternLength, patternMeter, patternTempo } from '../../src/core/music/pattern';
|
||||
import { newItem, type SongItem, songSequence } from '../../src/core/music/song';
|
||||
import { newItem, type SongItem, songMeter, songSequence } from '../../src/core/music/song';
|
||||
import { assetRefChoices } from '../../src/core/properties/assetRef';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
import { makeProject } from '../helpers';
|
||||
@@ -266,7 +265,7 @@ describe('a pattern made in a song (#355)', () => {
|
||||
const s = setup();
|
||||
const { f, songId } = s;
|
||||
f.stack.execute(new SetAssetPropertyCommand(songId, 'tempo', 140));
|
||||
f.stack.execute(new SetAssetPropertyCommand(songId, 'beats', 3));
|
||||
f.stack.execute(new SetAssetPropertyCommand(songId, 'sign', '3/4'));
|
||||
const loose = new CreateAssetCommand('pattern', 'Loose');
|
||||
f.stack.execute(loose);
|
||||
const lead = new CreateAssetCommand('instrument', 'Lead');
|
||||
@@ -296,40 +295,32 @@ describe('a pattern made in a song (#355)', () => {
|
||||
expect(make().name).toBe('Pattern 2');
|
||||
});
|
||||
|
||||
it('follows the tempo and the meter of its song in the same undo step, and nothing else', () => {
|
||||
const { f, songId, loose, make } = withPatterns();
|
||||
it('keeps its own tempo, meter and length when the song changes its own (#363)', () => {
|
||||
const { f, songId, make } = withPatterns();
|
||||
const own = make();
|
||||
f.stack.execute(new SetAssetPropertyCommand(songId, 'tempo', 90));
|
||||
expect([patternTempo(own.props), patternTempo(loose.props)]).toEqual([90, 120]);
|
||||
f.stack.execute(new SetAssetPropertyCommand(songId, 'beat', '8'));
|
||||
expect(patternMeter(own.props)).toEqual({ beats: 3, unit: 8 });
|
||||
f.stack.execute(new ResetAssetPropertyCommand(songId, 'beats'));
|
||||
expect(patternMeter(own.props).beats).toBe(4);
|
||||
const grid = own.props.grid;
|
||||
f.stack.execute(new SetAssetPropertyCommand(songId, 'grid', '1/8'));
|
||||
expect([own.props.grid, findAsset(f.project, songId)?.props.grid]).toEqual([grid, '1/8']);
|
||||
f.stack.undo();
|
||||
f.stack.undo();
|
||||
expect(patternMeter(own.props)).toEqual({ beats: 3, unit: 8 });
|
||||
f.stack.undo();
|
||||
f.stack.undo();
|
||||
expect([patternTempo(own.props), patternMeter(own.props).unit]).toEqual([140, 4]);
|
||||
f.stack.execute(new SetAssetPropertyCommand(songId, 'sign', '6/8'));
|
||||
expect(songMeter(findAsset(f.project, songId)!.props)).toEqual({ beats: 6, unit: 8 });
|
||||
expect([patternTempo(own.props), patternMeter(own.props), patternLength(own)]).toEqual([
|
||||
140,
|
||||
{ beats: 3, unit: 4 },
|
||||
3 * 480,
|
||||
]);
|
||||
});
|
||||
|
||||
it('hides its clock and its bus, not its length, instrument or sound FX', () => {
|
||||
const { loose, make } = withPatterns();
|
||||
it('hides only its bus, not its clock, length, instrument or sound FX (#363)', () => {
|
||||
const { make } = withPatterns();
|
||||
const own = make();
|
||||
const keys = ['tempo', 'beats', 'beat', 'bus', 'bars', 'instrument', 'fx'];
|
||||
const keys = ['bus', 'tempo', 'beats', 'beat', 'bars', 'instrument', 'fx'];
|
||||
expect(keys.map((k) => isRowHiddenWhenEmbedded(own, k))).toEqual([
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
]);
|
||||
expect(isRowHiddenWhenEmbedded(loose, 'tempo')).toBe(false);
|
||||
});
|
||||
|
||||
it('plays the instrument of the pattern on its track that starts last before it, else the first after', () => {
|
||||
@@ -351,13 +342,13 @@ describe('a pattern made in a song (#355)', () => {
|
||||
expect(near(0, 'elsewhere')).toBe('');
|
||||
});
|
||||
|
||||
it("takes the clock of the song it comes to as a copy, a project pattern's or another song's", () => {
|
||||
it("keeps its own clock as a copy, a project pattern's or another song's (#363)", () => {
|
||||
const { f, song, loose, track, edit } = withPatterns();
|
||||
const copy = embeddedCopy(loose, song()).copy as PatternAsset;
|
||||
expect([patternTempo(copy.props), patternTempo(loose.props)]).toEqual([140, 120]);
|
||||
expect([patternTempo(copy.props), patternMeter(copy.props).beats]).toEqual([120, 4]);
|
||||
const other = new CreateAssetCommand('song', 'Other');
|
||||
f.stack.execute(other);
|
||||
const theirs = new CreateAssetCommand('pattern', 'Theirs', { bars: 1 }, other.asset.id);
|
||||
const theirs = new CreateAssetCommand('pattern', 'Theirs', { bars: 1, beats: 5 }, other.asset.id);
|
||||
f.stack.execute(theirs);
|
||||
edit([
|
||||
...song().items,
|
||||
@@ -367,8 +358,8 @@ describe('a pattern made in a song (#355)', () => {
|
||||
expect(named.id).not.toBe(theirs.asset.id);
|
||||
expect([named.owner, patternTempo(named.props), patternMeter(named.props).beats]).toEqual([
|
||||
song().id,
|
||||
140,
|
||||
3,
|
||||
120,
|
||||
5,
|
||||
]);
|
||||
});
|
||||
});
|
||||
+33
-2
@@ -14,6 +14,7 @@ import {
|
||||
songGrid,
|
||||
songLength,
|
||||
songLoop,
|
||||
songMeter,
|
||||
songSequence,
|
||||
trackItemsBus,
|
||||
} from '../../src/core/music/song';
|
||||
@@ -72,8 +73,7 @@ describe('the song asset (#334)', () => {
|
||||
expect(f.song().items).toEqual([]);
|
||||
expect(schemaForAsset(f.project, f.song()).map((d) => d.key)).toEqual([
|
||||
'tempo',
|
||||
'beats',
|
||||
'beat',
|
||||
'sign',
|
||||
'grid',
|
||||
'fx',
|
||||
'bus',
|
||||
@@ -83,6 +83,37 @@ describe('the song asset (#334)', () => {
|
||||
expect(songGrid(f.song().props)).toBe(160);
|
||||
});
|
||||
|
||||
it('is written in one of the signs from 1/2 to 9/8, else in 4/4 (#363)', () => {
|
||||
const f = makeSong();
|
||||
const sign = schemaForAsset(f.project, f.song()).find((d) => d.key === 'sign');
|
||||
expect([sign?.label, sign?.default]).toEqual(['Sign', '4/4']);
|
||||
expect(sign?.kind === 'enum' && sign.options.map((o) => o.value)).toEqual([
|
||||
'1/2',
|
||||
'2/2',
|
||||
'3/2',
|
||||
'1/4',
|
||||
'2/4',
|
||||
'3/4',
|
||||
'4/4',
|
||||
'5/4',
|
||||
'6/4',
|
||||
'5/8',
|
||||
'6/8',
|
||||
'7/8',
|
||||
'8/8',
|
||||
'9/8',
|
||||
]);
|
||||
expect(songMeter(f.song().props)).toEqual({ beats: 4, unit: 4 });
|
||||
f.stack.execute(new SetAssetPropertyCommand(f.song().id, 'sign', '9/8'));
|
||||
expect([songMeter(f.song().props), songLength(f.song())]).toEqual([{ beats: 9, unit: 8 }, 9 * 240]);
|
||||
f.stack.execute(new SetAssetPropertyCommand(f.song().id, 'sign', '3/2'));
|
||||
expect(songMeter(f.song().props)).toEqual({ beats: 3, unit: 2 });
|
||||
expect([songMeter({ sign: '7/4' }), songMeter({ sign: 'x' })]).toEqual([
|
||||
{ beats: 4, unit: 4 },
|
||||
{ beats: 4, unit: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('is at least a bar long, else up to the bar line after its last item, and loops all of it', () => {
|
||||
const f = makeSong();
|
||||
expect(songLength(f.song())).toBe(BAR);
|
||||
|
||||
Reference in new issue
Block a user