diff --git a/src/core/music/embedded.ts b/src/core/music/embedded.ts index 7efc18b6..ec0ee016 100644 --- a/src/core/music/embedded.ts +++ b/src/core/music/embedded.ts @@ -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)) }; } /** diff --git a/src/core/music/pattern.ts b/src/core/music/pattern.ts index fb1aa8be..8f770b87 100644 --- a/src/core/music/pattern.ts +++ b/src/core/music/pattern.ts @@ -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 { diff --git a/src/core/music/song.ts b/src/core/music/song.ts index 43f18bf6..eb2730ed 100644 --- a/src/core/music/song.ts +++ b/src/core/music/song.ts @@ -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 { diff --git a/src/core/music/time.ts b/src/core/music/time.ts index 6cbb77d0..a330ee58 100644 --- a/src/core/music/time.ts +++ b/src/core/music/time.ts @@ -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; diff --git a/src/core/properties/PropertySchema.ts b/src/core/properties/PropertySchema.ts index 988f38a9..4440a1fb 100644 --- a/src/core/properties/PropertySchema.ts +++ b/src/core/properties/PropertySchema.ts @@ -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 = { 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)', diff --git a/tests/core/embedded.test.ts b/tests/core/embedded.test.ts index 037e479a..67f578f5 100644 --- a/tests/core/embedded.test.ts +++ b/tests/core/embedded.test.ts @@ -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, diff --git a/tests/core/pattern.test.ts b/tests/core/pattern.test.ts index 10cf83f9..5739f58c 100644 --- a/tests/core/pattern.test.ts +++ b/tests/core/pattern.test.ts @@ -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 }, diff --git a/tools/e2e/64-pattern.mjs b/tools/e2e/64-pattern.mjs index e74cf0a8..1b5709b4 100644 --- a/tools/e2e/64-pattern.mjs +++ b/tools/e2e/64-pattern.mjs @@ -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, { diff --git a/tools/e2e/65-song.mjs b/tools/e2e/65-song.mjs index 9f6d5bce..4c8a2657 100644 --- a/tools/e2e/65-song.mjs +++ b/tools/e2e/65-song.mjs @@ -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,