Pick a pattern's sign in one row, as a song's (#364)
A pattern had two rows for its meter, Beats per bar and Beat, while a song has had one row Sign since #363. Both now pick from the same list. - The list of signs and how a sign becomes a meter moved to time.ts. Song and pattern share it. - A pattern keeps its meter in `sign`. The fields `beats` and `beat` are gone, a sign that is not in the list counts as 4/4. - Create pattern in a song hands the song's sign to the new pattern. - The four patterns of cbbf160bb7e8 were moved to `sign` by hand (three 4/4, one 3/4). The other projects only had 4/4, which stays the default. Tests: - Unit pattern/embedded: 30 green (1 new, 2 rewritten), the counter-test reading `beats` again turns 2 red - e2e 64 + 65 (melektaus-atx): ALL PASSED, 85 checks - Gate (melektaus-atx): CHECK EXIT 0, 272 files, 4215 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d57a2f3c4
commit
8891f027db
9 files changed
+60
-67
No files matched your search
@@ -15,8 +15,9 @@ import { findAsset, findAssetOf, type Project } from '../model/Project';
|
||||
import { assetNamedBy, renewClipIds } from '../model/sound';
|
||||
import { ASSET_FX_KEY, assetFxOf, NO_FX } from '../properties/assetFx';
|
||||
import { nextNumberedName } from '../commands/naming';
|
||||
import { PATTERN_BEAT_KEY, PATTERN_BEATS_KEY, PATTERN_TEMPO_KEY, patternInstrument } from './pattern';
|
||||
import { PATTERN_SIGN_KEY, PATTERN_TEMPO_KEY, patternInstrument } from './pattern';
|
||||
import { type SongItem, songMeter, songTempo, type SongTrack } from './song';
|
||||
import { signOf } from './time';
|
||||
|
||||
export const EMBEDDABLE_TYPES: readonly AssetType[] = ['sound', 'pattern', 'soundfx'];
|
||||
|
||||
@@ -71,12 +72,7 @@ export function assetTitle(project: Project, asset: Asset): string {
|
||||
|
||||
/** The tempo and the meter of a song, as the props a pattern made in it starts with. */
|
||||
export function songClockProps(props: PropertyBag): PropertyBag {
|
||||
const meter = songMeter(props);
|
||||
return {
|
||||
[PATTERN_TEMPO_KEY]: songTempo(props),
|
||||
[PATTERN_BEATS_KEY]: meter.beats,
|
||||
[PATTERN_BEAT_KEY]: String(meter.unit),
|
||||
};
|
||||
return { [PATTERN_TEMPO_KEY]: songTempo(props), [PATTERN_SIGN_KEY]: signOf(songMeter(props)) };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ import { HIGHEST_NOTE, LOWEST_NOTE } from '../instrument/notes';
|
||||
import type { PatternAsset } from '../model/Asset';
|
||||
import { type PropertyBag, textOf } from '../model/Properties';
|
||||
import { heldLoop, type LoopRange, type Sequence } from './sequence';
|
||||
import { heldMeter, heldTempo, type Meter, ticksPerBar } from './time';
|
||||
import { DEFAULT_SIGN, heldTempo, type Meter, signMeter, ticksPerBar } from './time';
|
||||
|
||||
export interface PatternNote {
|
||||
id: string;
|
||||
@@ -24,8 +24,7 @@ export interface PatternNote {
|
||||
export const PATTERN_INSTRUMENT_KEY = 'instrument';
|
||||
export const PATTERN_BARS_KEY = 'bars';
|
||||
export const PATTERN_GRID_KEY = 'grid';
|
||||
export const PATTERN_BEATS_KEY = 'beats';
|
||||
export const PATTERN_BEAT_KEY = 'beat';
|
||||
export const PATTERN_SIGN_KEY = 'sign';
|
||||
export const PATTERN_TEMPO_KEY = 'tempo';
|
||||
|
||||
export const DEFAULT_BARS = 4;
|
||||
@@ -49,7 +48,6 @@ const GRIDS: readonly (readonly [string, number])[] = [
|
||||
];
|
||||
|
||||
export const GRID_OPTIONS = GRIDS.map(([value]) => ({ value, label: value }));
|
||||
export const BEAT_OPTIONS = [1, 2, 4, 8, 16, 32].map((unit) => ({ value: String(unit), label: `1/${unit}` }));
|
||||
|
||||
export function gridTicks(grid: string): number {
|
||||
return (GRIDS.find(([value]) => value === grid) ?? GRIDS[4]!)[1];
|
||||
@@ -66,10 +64,7 @@ function wholeNumber(
|
||||
}
|
||||
|
||||
export function patternMeter(props: PropertyBag): Meter {
|
||||
return heldMeter({
|
||||
beats: wholeNumber(props[PATTERN_BEATS_KEY], 4, 1, 32),
|
||||
unit: Number(textOf(props[PATTERN_BEAT_KEY] ?? '4')),
|
||||
});
|
||||
return signMeter(textOf(props[PATTERN_SIGN_KEY] ?? DEFAULT_SIGN));
|
||||
}
|
||||
|
||||
export function patternTempo(props: PropertyBag): number {
|
||||
|
||||
+2
-25
@@ -14,7 +14,7 @@ import { findSound } from '../model/sound';
|
||||
import { pickedFx } from '../properties/assetFx';
|
||||
import { gridTicks, GRID_OPTIONS, patternInstrument, patternLength } from './pattern';
|
||||
import { heldLoop, type LoopRange, type Sequence, type SequenceClip, type SequenceNote } from './sequence';
|
||||
import { heldMeter, heldTempo, type Meter, secondsOfTicks, ticksPerBar } from './time';
|
||||
import { DEFAULT_SIGN, heldTempo, type Meter, secondsOfTicks, signMeter, ticksPerBar } from './time';
|
||||
|
||||
export interface SongTrack {
|
||||
id: string;
|
||||
@@ -59,26 +59,6 @@ export const SONG_TEMPO_KEY = 'tempo';
|
||||
export const SONG_SIGN_KEY = 'sign';
|
||||
export const SONG_GRID_KEY = 'grid';
|
||||
|
||||
/** The time signatures a song is written in (#363), one row for both numbers. */
|
||||
export const SIGNS: readonly string[] = [
|
||||
'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',
|
||||
];
|
||||
export const DEFAULT_SIGN = '4/4';
|
||||
export const SIGN_OPTIONS = SIGNS.map((sign) => ({ value: sign, label: sign }));
|
||||
|
||||
export const DEFAULT_SONG_GRID = 'bar';
|
||||
export const SONG_GRID_OPTIONS = [{ value: 'bar', label: 'Bar' }, ...GRID_OPTIONS];
|
||||
/** A new song has this many tracks. */
|
||||
@@ -97,11 +77,8 @@ export function itemMaxDb(kind: SongItemKind): number {
|
||||
return kind === 'sound' ? SOUND_ITEM_MAX_DB : 0;
|
||||
}
|
||||
|
||||
/** The meter of a song's sign; 4/4 where it is not one of `SIGNS`. */
|
||||
export function songMeter(props: PropertyBag): Meter {
|
||||
const sign = textOf(props[SONG_SIGN_KEY] ?? DEFAULT_SIGN);
|
||||
const [beats, unit] = (SIGNS.includes(sign) ? sign : DEFAULT_SIGN).split('/');
|
||||
return heldMeter({ beats: Number(beats), unit: Number(unit) });
|
||||
return signMeter(textOf(props[SONG_SIGN_KEY] ?? DEFAULT_SIGN));
|
||||
}
|
||||
|
||||
export function songTempo(props: PropertyBag): number {
|
||||
|
||||
@@ -28,6 +28,36 @@ export function heldMeter(meter: Meter): Meter {
|
||||
return { beats, unit };
|
||||
}
|
||||
|
||||
/** The time signatures a song or a pattern is written in (#363, #364), one row for both numbers. */
|
||||
export const SIGNS: readonly string[] = [
|
||||
'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',
|
||||
];
|
||||
export const DEFAULT_SIGN = '4/4';
|
||||
export const SIGN_OPTIONS = SIGNS.map((sign) => ({ value: sign, label: sign }));
|
||||
|
||||
/** The meter of a sign; 4/4 where it is not one of `SIGNS`. */
|
||||
export function signMeter(sign: string): Meter {
|
||||
const [beats, unit] = (SIGNS.includes(sign) ? sign : DEFAULT_SIGN).split('/');
|
||||
return heldMeter({ beats: Number(beats), unit: Number(unit) });
|
||||
}
|
||||
|
||||
export function signOf(meter: Meter): string {
|
||||
return `${meter.beats}/${meter.unit}`;
|
||||
}
|
||||
|
||||
/** A tempo between `MIN_TEMPO` and `MAX_TEMPO`; 0 and anything that is not a number is the default. */
|
||||
export function heldTempo(tempo: number): number {
|
||||
if (!Number.isFinite(tempo) || tempo <= 0) return DEFAULT_TEMPO;
|
||||
|
||||
@@ -105,28 +105,24 @@ import { SHADER_KEY, shaderOf, shaderParamDefs } from './shaders';
|
||||
import { SHADER_RESOLUTION_KEY, SHADER_RESOLUTION_OPTIONS } from './cameraShader';
|
||||
import { LOOP_KEY as SOUND_LOOP_KEY, VOLUME_KEY as SOUND_VOLUME_KEY } from './sounds';
|
||||
import {
|
||||
BEAT_OPTIONS,
|
||||
DEFAULT_BARS,
|
||||
DEFAULT_GRID,
|
||||
GRID_OPTIONS,
|
||||
MAX_BARS,
|
||||
PATTERN_BARS_KEY,
|
||||
PATTERN_BEAT_KEY,
|
||||
PATTERN_BEATS_KEY,
|
||||
PATTERN_GRID_KEY,
|
||||
PATTERN_INSTRUMENT_KEY,
|
||||
PATTERN_SIGN_KEY,
|
||||
PATTERN_TEMPO_KEY,
|
||||
} from '../music/pattern';
|
||||
import {
|
||||
DEFAULT_SIGN,
|
||||
DEFAULT_SONG_GRID,
|
||||
SIGN_OPTIONS,
|
||||
SONG_GRID_KEY,
|
||||
SONG_GRID_OPTIONS,
|
||||
SONG_SIGN_KEY,
|
||||
SONG_TEMPO_KEY,
|
||||
} from '../music/song';
|
||||
import { DEFAULT_TEMPO, MAX_TEMPO, MIN_TEMPO } from '../music/time';
|
||||
import { DEFAULT_SIGN, DEFAULT_TEMPO, MAX_TEMPO, MIN_TEMPO, SIGN_OPTIONS } from '../music/time';
|
||||
import { ASSET_FX_KEY } from './assetFx';
|
||||
import { BUS_FX_KEY, BUS_INTO_KEY, BUS_KEY, BUS_VOLUME_KEY } from './buses';
|
||||
import { Theme } from '../../ui';
|
||||
@@ -737,17 +733,7 @@ const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
|
||||
decimals: 0,
|
||||
},
|
||||
{ key: PATTERN_GRID_KEY, label: 'Grid', kind: 'enum', default: DEFAULT_GRID, options: GRID_OPTIONS },
|
||||
{
|
||||
key: PATTERN_BEATS_KEY,
|
||||
label: 'Beats per bar',
|
||||
kind: 'number',
|
||||
default: 4,
|
||||
min: 1,
|
||||
max: 32,
|
||||
step: 1,
|
||||
decimals: 0,
|
||||
},
|
||||
{ key: PATTERN_BEAT_KEY, label: 'Beat', kind: 'enum', default: '4', options: BEAT_OPTIONS },
|
||||
{ key: PATTERN_SIGN_KEY, label: 'Sign', kind: 'enum', default: DEFAULT_SIGN, options: SIGN_OPTIONS },
|
||||
{
|
||||
key: PATTERN_TEMPO_KEY,
|
||||
label: 'Tempo (BPM)',
|
||||
|
||||
@@ -311,7 +311,7 @@ describe('a pattern made in a song (#355)', () => {
|
||||
it('hides only its bus, not its clock, length, instrument or sound FX (#363)', () => {
|
||||
const { make } = withPatterns();
|
||||
const own = make();
|
||||
const keys = ['bus', 'tempo', 'beats', 'beat', 'bars', 'instrument', 'fx'];
|
||||
const keys = ['bus', 'tempo', 'sign', 'bars', 'instrument', 'fx'];
|
||||
expect(keys.map((k) => isRowHiddenWhenEmbedded(own, k))).toEqual([
|
||||
true,
|
||||
false,
|
||||
@@ -319,7 +319,6 @@ describe('a pattern made in a song (#355)', () => {
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -348,7 +347,7 @@ describe('a pattern made in a song (#355)', () => {
|
||||
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, beats: 5 }, other.asset.id);
|
||||
const theirs = new CreateAssetCommand('pattern', 'Theirs', { bars: 1, sign: '5/4' }, other.asset.id);
|
||||
f.stack.execute(theirs);
|
||||
edit([
|
||||
...song().items,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
patternSequence,
|
||||
patternTempo,
|
||||
} from '../../src/core/music/pattern';
|
||||
import { SIGN_OPTIONS } from '../../src/core/music/time';
|
||||
import { schemaForAsset } from '../../src/core/properties/PropertySchema';
|
||||
import { makeProject, roundTrip } from '../helpers';
|
||||
|
||||
@@ -52,8 +53,7 @@ describe('pattern asset (#333)', () => {
|
||||
'instrument',
|
||||
'bars',
|
||||
'grid',
|
||||
'beats',
|
||||
'beat',
|
||||
'sign',
|
||||
'tempo',
|
||||
'fx',
|
||||
'bus',
|
||||
@@ -70,11 +70,22 @@ describe('pattern asset (#333)', () => {
|
||||
it('measures its length in bars of its meter: 3 bars of 6/8 are 4320 ticks', () => {
|
||||
const f = makePattern();
|
||||
f.set('bars', 3);
|
||||
f.set('beats', 6);
|
||||
f.set('beat', '8');
|
||||
f.set('sign', '6/8');
|
||||
expect(patternLength(f.pattern())).toBe(3 * 6 * 240);
|
||||
});
|
||||
|
||||
it('picks its meter from the signs a song has too, 4/4 for any other (#364)', () => {
|
||||
const f = makePattern();
|
||||
f.set('sign', '9/8');
|
||||
expect(patternMeter(f.pattern().props)).toEqual({ beats: 9, unit: 8 });
|
||||
f.set('sign', '12/8');
|
||||
expect(patternMeter(f.pattern().props)).toEqual({ beats: 4, unit: 4 });
|
||||
expect(schemaForAsset(f.project, f.pattern()).find((d) => d.key === 'sign')).toMatchObject({
|
||||
kind: 'enum',
|
||||
options: SIGN_OPTIONS,
|
||||
});
|
||||
});
|
||||
|
||||
it('holds notes to whole ticks, a tick at least, the keyboard and 0 to 1, sorted, ids unique', () => {
|
||||
const held = heldPatternNotes([
|
||||
{ id: 'b', tick: 480.4, length: 0.2, note: 64, velocity: 2 },
|
||||
|
||||
@@ -492,8 +492,7 @@ export async function pattern(t) {
|
||||
const p = __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(id)});
|
||||
p.props.tempo = 150;
|
||||
p.props.bars = 1;
|
||||
p.props.beats = 4;
|
||||
p.props.beat = '4';
|
||||
p.props.sign = '4/4';
|
||||
p.notes = [60, 62, 64, 65].map((note, i) => ({ id: 'pt64-n' + i, tick: i * 480, length: 240, note, velocity: 1 }));`);
|
||||
await showTab('scene');
|
||||
const rig = await scaffoldBehavior(t, {
|
||||
|
||||
@@ -1167,7 +1167,7 @@ export async function song(t) {
|
||||
madePattern.props.bars === 1 &&
|
||||
madePattern.props.instrument === 'sg65-ins' &&
|
||||
madePattern.props.tempo === 120 &&
|
||||
madePattern.props.beats === 4 &&
|
||||
madePattern.props.sign === '4/4' &&
|
||||
madeTab.tab === 'pattern' &&
|
||||
madeTab.title === `${songName} › Pattern 1` &&
|
||||
madeTab.row === null,
|
||||
|
||||
Reference in new issue
Block a user