entangled-worlds-orbiters-shared 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ import type { TrackDataLike } from './track-metadata.js';
2
+ export interface MeterPreset {
3
+ id: string;
4
+ numerator: number;
5
+ denominator: number;
6
+ }
7
+ /** Quick-pick reference meters. `id` is the persisted/display value, not a closed enum. */
8
+ export declare const METER_PRESETS: MeterPreset[];
9
+ export declare const DEFAULT_METER_ID = "4/4";
10
+ export declare const MIN_METER_NUMERATOR = 1;
11
+ export declare const MAX_METER_NUMERATOR = 64;
12
+ export declare const VALID_METER_DENOMINATORS: readonly number[];
13
+ interface MeterFraction {
14
+ numerator: number;
15
+ denominator: number;
16
+ }
17
+ export interface ParsedMeter {
18
+ id: string;
19
+ numerator: number;
20
+ denominator: number;
21
+ beatsPerBar: number;
22
+ beatUnit: number;
23
+ sharedBeatsPerBar: number;
24
+ quarterBeatsPerBar: number;
25
+ clickIntervalQuarterBeats: number;
26
+ clicksPerBar: number;
27
+ }
28
+ export declare function formatMeterId(value?: Partial<MeterFraction>): string;
29
+ /** True when `value` is a valid arbitrary meter string or `{ numerator, denominator }`. */
30
+ export declare function isValidMeterId(value: unknown): boolean;
31
+ /** Normalize an arbitrary value to a persisted meter id (`n/d`), falling back to the default. */
32
+ export declare function normalizeMeterId(value: unknown): string;
33
+ /**
34
+ * Resolve a meter id to its numbers, including `sharedBeatsPerBar` — the count of shared (quarter-note)
35
+ * beats in one bar, which is what the engine uses for the downbeat accent and the launch bar.
36
+ */
37
+ export declare function parseMeter(value: unknown): ParsedMeter;
38
+ /** Shared (quarter-note) beats per bar for a meter id — the engine's bar length. */
39
+ export declare function sharedBeatsPerBar(id: unknown): number;
40
+ /**
41
+ * Resolve the per-track meter from assembled track data — the LOAD side, walking the SAME precedence
42
+ * as the shared tempo resolver (`resolveTrackBpmFromTrackData`): owner CANONICAL first, then the
43
+ * per-user override, then auto-detected analysis, then the default. First valid wins:
44
+ * 1) owner canonical — `metadata.meter` → `metadata.effectiveSettings.sync.meter`
45
+ * 2) per-user — `trackUserSettings.sync.meter`
46
+ * 3) auto-detected — `metadata.audioAnalysis.analysis` time signature
47
+ * 4) default (4/4)
48
+ */
49
+ export declare function resolveTrackMeterFromTrackData(trackData?: TrackDataLike | null): string;
50
+ export {};
@@ -0,0 +1,145 @@
1
+ // ./clock/meter — time signature / meter model. A per-track musical property (how many beats make a
2
+ // bar), resolved with the SAME owner → per-user → auto-detected → default precedence as the tempo
3
+ // resolver in `./clock/track-metadata`. Promoted out of the orbiters app so EW reuses it verbatim.
4
+ // Framework-free: no three, no react.
5
+ //
6
+ // Units: `sharedBeatsPerBar` is quarter-note beats per bar = numerator * 4 / denominator — the same
7
+ // beat unit `MusicalProjection` (./clock/musical) counts, so a resolved meter feeds its `signatureMap`
8
+ // directly: `{ startBar: 0, numerator, denominator }`.
9
+ /** Quick-pick reference meters. `id` is the persisted/display value, not a closed enum. */
10
+ export const METER_PRESETS = [
11
+ { id: '4/4', numerator: 4, denominator: 4 },
12
+ { id: '3/4', numerator: 3, denominator: 4 },
13
+ { id: '6/8', numerator: 6, denominator: 8 },
14
+ ];
15
+ export const DEFAULT_METER_ID = '4/4';
16
+ export const MIN_METER_NUMERATOR = 1;
17
+ export const MAX_METER_NUMERATOR = 64;
18
+ export const VALID_METER_DENOMINATORS = [1, 2, 4, 8, 16, 32];
19
+ const BY_ID = new Map(METER_PRESETS.map((m) => [m.id, m]));
20
+ const DENOMINATORS = new Set(VALID_METER_DENOMINATORS);
21
+ function isValidNumerator(n) {
22
+ return Number.isInteger(n) && n >= MIN_METER_NUMERATOR;
23
+ }
24
+ function isValidDenominator(n) {
25
+ return Number.isInteger(n) && DENOMINATORS.has(n);
26
+ }
27
+ function parseMeterString(value) {
28
+ if (typeof value !== 'string')
29
+ return null;
30
+ const match = value.trim().match(/^(\d+)\s*\/\s*(\d+)$/);
31
+ if (!match)
32
+ return null;
33
+ const numerator = Number(match[1]);
34
+ const denominator = Number(match[2]);
35
+ if (!isValidNumerator(numerator) || !isValidDenominator(denominator))
36
+ return null;
37
+ return { numerator, denominator };
38
+ }
39
+ function parseMeterObject(value) {
40
+ if (!value || typeof value !== 'object')
41
+ return null;
42
+ const numerator = Number(value.numerator);
43
+ const denominator = Number(value.denominator);
44
+ if (!isValidNumerator(numerator) || !isValidDenominator(denominator))
45
+ return null;
46
+ return { numerator, denominator };
47
+ }
48
+ export function formatMeterId(value = {}) {
49
+ const n = Number(value.numerator);
50
+ const d = Number(value.denominator);
51
+ return isValidNumerator(n) && isValidDenominator(d) ? `${n}/${d}` : DEFAULT_METER_ID;
52
+ }
53
+ /** True when `value` is a valid arbitrary meter string or `{ numerator, denominator }`. */
54
+ export function isValidMeterId(value) {
55
+ return parseMeterString(value) !== null || parseMeterObject(value) !== null;
56
+ }
57
+ /** Normalize an arbitrary value to a persisted meter id (`n/d`), falling back to the default. */
58
+ export function normalizeMeterId(value) {
59
+ const parsed = parseMeterString(value) ?? parseMeterObject(value);
60
+ return parsed ? formatMeterId(parsed) : DEFAULT_METER_ID;
61
+ }
62
+ /**
63
+ * Resolve a meter id to its numbers, including `sharedBeatsPerBar` — the count of shared (quarter-note)
64
+ * beats in one bar, which is what the engine uses for the downbeat accent and the launch bar.
65
+ */
66
+ export function parseMeter(value) {
67
+ const id = normalizeMeterId(value);
68
+ const preset = BY_ID.get(id);
69
+ const parsed = preset ?? parseMeterString(id) ?? parseMeterString(DEFAULT_METER_ID);
70
+ const numerator = parsed.numerator;
71
+ const denominator = parsed.denominator;
72
+ const sharedBeatsPerBar = (numerator * 4) / denominator;
73
+ const clickIntervalQuarterBeats = 4 / denominator;
74
+ return {
75
+ id,
76
+ numerator,
77
+ denominator,
78
+ beatsPerBar: numerator,
79
+ beatUnit: denominator,
80
+ sharedBeatsPerBar,
81
+ quarterBeatsPerBar: sharedBeatsPerBar,
82
+ clickIntervalQuarterBeats,
83
+ clicksPerBar: numerator,
84
+ };
85
+ }
86
+ function resolveAnalysisMeter(analysis) {
87
+ if (!analysis || typeof analysis !== 'object')
88
+ return null;
89
+ const a = analysis;
90
+ const candidates = [
91
+ a.time_signature,
92
+ a.timeSignature,
93
+ a.meter,
94
+ a.time_signature?.meter,
95
+ a.timeSignature?.meter,
96
+ a.time_signature?.value,
97
+ a.timeSignature?.value,
98
+ ];
99
+ for (const candidate of candidates) {
100
+ if (isValidMeterId(candidate)) {
101
+ return normalizeMeterId(candidate);
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+ /** Shared (quarter-note) beats per bar for a meter id — the engine's bar length. */
107
+ export function sharedBeatsPerBar(id) {
108
+ return parseMeter(id).sharedBeatsPerBar;
109
+ }
110
+ /**
111
+ * Resolve the per-track meter from assembled track data — the LOAD side, walking the SAME precedence
112
+ * as the shared tempo resolver (`resolveTrackBpmFromTrackData`): owner CANONICAL first, then the
113
+ * per-user override, then auto-detected analysis, then the default. First valid wins:
114
+ * 1) owner canonical — `metadata.meter` → `metadata.effectiveSettings.sync.meter`
115
+ * 2) per-user — `trackUserSettings.sync.meter`
116
+ * 3) auto-detected — `metadata.audioAnalysis.analysis` time signature
117
+ * 4) default (4/4)
118
+ */
119
+ export function resolveTrackMeterFromTrackData(trackData = null) {
120
+ const td = trackData;
121
+ const candidates = [
122
+ // 1) owner canonical — top-level metadata (mirrors tempo's `metadata.trackBpm`)
123
+ td?.track?.metadata?.meter,
124
+ td?.release?.metadata?.meter,
125
+ td?.metadata?.meter,
126
+ // 1) owner canonical — effectiveSettings (mirrors tempo's `effectiveSettings.sync.trackBpm`)
127
+ td?.track?.metadata?.effectiveSettings?.sync?.meter,
128
+ td?.release?.metadata?.effectiveSettings?.sync?.meter,
129
+ td?.metadata?.effectiveSettings?.sync?.meter,
130
+ td?.track?.effectiveSettings?.sync?.meter,
131
+ td?.release?.effectiveSettings?.sync?.meter,
132
+ td?.effectiveSettings?.sync?.meter,
133
+ // 2) per-user override (mirrors tempo's `trackUserSettings.sync.trackBpm`)
134
+ td?.trackUserSettings?.sync?.meter,
135
+ // 3) auto-detected analysis (mirrors tempo's `audioAnalysis.analysis.tempo_bpm`)
136
+ resolveAnalysisMeter(td?.track?.metadata?.audioAnalysis?.analysis),
137
+ resolveAnalysisMeter(td?.release?.metadata?.audioAnalysis?.analysis),
138
+ resolveAnalysisMeter(td?.metadata?.audioAnalysis?.analysis),
139
+ ];
140
+ for (const c of candidates) {
141
+ if (isValidMeterId(c))
142
+ return normalizeMeterId(c);
143
+ }
144
+ return DEFAULT_METER_ID;
145
+ }
@@ -1,11 +1,14 @@
1
1
  /**
2
- * The slice of a track's data these resolvers read. Every field is optional and untyped at the leaf
3
- * because the runtime objects come from different app layers (release metadata, effective owner
4
- * settings, per-user overrides, audio analysis). Resolvers coerce leaves with Number(...) and ignore
5
- * anything non-finite, so a permissive shape here is intentional — not every consumer fills every path.
2
+ * The slice of a track's data these resolvers read (tempo here; meter in `./clock/meter`). Every field
3
+ * is optional and untyped at the leaf because the runtime objects come from different app layers
4
+ * (release metadata, effective owner settings, per-user overrides, audio analysis). Resolvers coerce
5
+ * leaves with Number(...)/String(...) and ignore anything invalid, so a permissive shape here is
6
+ * intentional — not every consumer fills every path.
6
7
  */
7
8
  export interface SyncSettingsLike {
8
9
  trackBpm?: unknown;
10
+ /** Per-track time signature (e.g. `'4/4'` or `{ numerator, denominator }`) — see `./clock/meter`. */
11
+ meter?: unknown;
9
12
  primaryGridMarkerTimeSec?: unknown;
10
13
  gridMarkers?: ReadonlyArray<{
11
14
  sourceTimeSec?: unknown;
@@ -14,6 +17,8 @@ export interface SyncSettingsLike {
14
17
  }
15
18
  export interface TrackMetadataLike {
16
19
  trackBpm?: unknown;
20
+ /** Per-track time signature (owner canonical) — see `./clock/meter`. */
21
+ meter?: unknown;
17
22
  primaryGridMarkerTimeSec?: unknown;
18
23
  effectiveSettings?: {
19
24
  sync?: SyncSettingsLike | null;
@@ -21,6 +26,8 @@ export interface TrackMetadataLike {
21
26
  audioAnalysis?: {
22
27
  analysis?: {
23
28
  tempo_bpm?: unknown;
29
+ time_signature?: unknown;
30
+ timeSignature?: unknown;
24
31
  } | null;
25
32
  } | null;
26
33
  wrap?: {
@@ -33,11 +40,20 @@ export interface TrackMetadataLike {
33
40
  export interface TrackDataLike {
34
41
  track?: {
35
42
  metadata?: TrackMetadataLike | null;
43
+ effectiveSettings?: {
44
+ sync?: SyncSettingsLike | null;
45
+ } | null;
36
46
  } | null;
37
47
  release?: {
38
48
  metadata?: TrackMetadataLike | null;
49
+ effectiveSettings?: {
50
+ sync?: SyncSettingsLike | null;
51
+ } | null;
39
52
  } | null;
40
53
  metadata?: TrackMetadataLike | null;
54
+ effectiveSettings?: {
55
+ sync?: SyncSettingsLike | null;
56
+ } | null;
41
57
  trackUserSettings?: {
42
58
  sync?: SyncSettingsLike | null;
43
59
  } | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entangled-worlds-orbiters-shared",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Shared library for entangled worlds orbiters - contains core world rendering, moons system, and UI components",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -25,6 +25,10 @@
25
25
  "types": "./dist/clock/track-metadata.d.ts",
26
26
  "import": "./dist/clock/track-metadata.js"
27
27
  },
28
+ "./clock/meter": {
29
+ "types": "./dist/clock/meter.d.ts",
30
+ "import": "./dist/clock/meter.js"
31
+ },
28
32
  "./clock/sync": {
29
33
  "types": "./dist/clock/sync.d.ts",
30
34
  "import": "./dist/clock/sync.js"
@@ -0,0 +1,177 @@
1
+ // ./clock/meter — time signature / meter model. A per-track musical property (how many beats make a
2
+ // bar), resolved with the SAME owner → per-user → auto-detected → default precedence as the tempo
3
+ // resolver in `./clock/track-metadata`. Promoted out of the orbiters app so EW reuses it verbatim.
4
+ // Framework-free: no three, no react.
5
+ //
6
+ // Units: `sharedBeatsPerBar` is quarter-note beats per bar = numerator * 4 / denominator — the same
7
+ // beat unit `MusicalProjection` (./clock/musical) counts, so a resolved meter feeds its `signatureMap`
8
+ // directly: `{ startBar: 0, numerator, denominator }`.
9
+
10
+ import type { TrackDataLike } from './track-metadata.js';
11
+
12
+ export interface MeterPreset {
13
+ id: string;
14
+ numerator: number;
15
+ denominator: number;
16
+ }
17
+
18
+ /** Quick-pick reference meters. `id` is the persisted/display value, not a closed enum. */
19
+ export const METER_PRESETS: MeterPreset[] = [
20
+ { id: '4/4', numerator: 4, denominator: 4 },
21
+ { id: '3/4', numerator: 3, denominator: 4 },
22
+ { id: '6/8', numerator: 6, denominator: 8 },
23
+ ];
24
+
25
+ export const DEFAULT_METER_ID = '4/4';
26
+ export const MIN_METER_NUMERATOR = 1;
27
+ export const MAX_METER_NUMERATOR = 64;
28
+ export const VALID_METER_DENOMINATORS: readonly number[] = [1, 2, 4, 8, 16, 32];
29
+
30
+ const BY_ID = new Map(METER_PRESETS.map((m) => [m.id, m]));
31
+ const DENOMINATORS = new Set(VALID_METER_DENOMINATORS);
32
+
33
+ interface MeterFraction {
34
+ numerator: number;
35
+ denominator: number;
36
+ }
37
+
38
+ export interface ParsedMeter {
39
+ id: string;
40
+ numerator: number;
41
+ denominator: number;
42
+ beatsPerBar: number;
43
+ beatUnit: number;
44
+ sharedBeatsPerBar: number;
45
+ quarterBeatsPerBar: number;
46
+ clickIntervalQuarterBeats: number;
47
+ clicksPerBar: number;
48
+ }
49
+
50
+ function isValidNumerator(n: number): boolean {
51
+ return Number.isInteger(n) && n >= MIN_METER_NUMERATOR;
52
+ }
53
+
54
+ function isValidDenominator(n: number): boolean {
55
+ return Number.isInteger(n) && DENOMINATORS.has(n);
56
+ }
57
+
58
+ function parseMeterString(value: unknown): MeterFraction | null {
59
+ if (typeof value !== 'string') return null;
60
+ const match = value.trim().match(/^(\d+)\s*\/\s*(\d+)$/);
61
+ if (!match) return null;
62
+ const numerator = Number(match[1]);
63
+ const denominator = Number(match[2]);
64
+ if (!isValidNumerator(numerator) || !isValidDenominator(denominator)) return null;
65
+ return { numerator, denominator };
66
+ }
67
+
68
+ function parseMeterObject(value: unknown): MeterFraction | null {
69
+ if (!value || typeof value !== 'object') return null;
70
+ const numerator = Number((value as Record<string, unknown>).numerator);
71
+ const denominator = Number((value as Record<string, unknown>).denominator);
72
+ if (!isValidNumerator(numerator) || !isValidDenominator(denominator)) return null;
73
+ return { numerator, denominator };
74
+ }
75
+
76
+ export function formatMeterId(value: Partial<MeterFraction> = {}): string {
77
+ const n = Number(value.numerator);
78
+ const d = Number(value.denominator);
79
+ return isValidNumerator(n) && isValidDenominator(d) ? `${n}/${d}` : DEFAULT_METER_ID;
80
+ }
81
+
82
+ /** True when `value` is a valid arbitrary meter string or `{ numerator, denominator }`. */
83
+ export function isValidMeterId(value: unknown): boolean {
84
+ return parseMeterString(value) !== null || parseMeterObject(value) !== null;
85
+ }
86
+
87
+ /** Normalize an arbitrary value to a persisted meter id (`n/d`), falling back to the default. */
88
+ export function normalizeMeterId(value: unknown): string {
89
+ const parsed = parseMeterString(value) ?? parseMeterObject(value);
90
+ return parsed ? formatMeterId(parsed) : DEFAULT_METER_ID;
91
+ }
92
+
93
+ /**
94
+ * Resolve a meter id to its numbers, including `sharedBeatsPerBar` — the count of shared (quarter-note)
95
+ * beats in one bar, which is what the engine uses for the downbeat accent and the launch bar.
96
+ */
97
+ export function parseMeter(value: unknown): ParsedMeter {
98
+ const id = normalizeMeterId(value);
99
+ const preset = BY_ID.get(id);
100
+ const parsed = preset ?? parseMeterString(id) ?? (parseMeterString(DEFAULT_METER_ID) as MeterFraction);
101
+ const numerator = parsed.numerator;
102
+ const denominator = parsed.denominator;
103
+ const sharedBeatsPerBar = (numerator * 4) / denominator;
104
+ const clickIntervalQuarterBeats = 4 / denominator;
105
+ return {
106
+ id,
107
+ numerator,
108
+ denominator,
109
+ beatsPerBar: numerator,
110
+ beatUnit: denominator,
111
+ sharedBeatsPerBar,
112
+ quarterBeatsPerBar: sharedBeatsPerBar,
113
+ clickIntervalQuarterBeats,
114
+ clicksPerBar: numerator,
115
+ };
116
+ }
117
+
118
+ function resolveAnalysisMeter(analysis: unknown): string | null {
119
+ if (!analysis || typeof analysis !== 'object') return null;
120
+ const a = analysis as Record<string, any>;
121
+ const candidates = [
122
+ a.time_signature,
123
+ a.timeSignature,
124
+ a.meter,
125
+ a.time_signature?.meter,
126
+ a.timeSignature?.meter,
127
+ a.time_signature?.value,
128
+ a.timeSignature?.value,
129
+ ];
130
+ for (const candidate of candidates) {
131
+ if (isValidMeterId(candidate)) {
132
+ return normalizeMeterId(candidate);
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+
138
+ /** Shared (quarter-note) beats per bar for a meter id — the engine's bar length. */
139
+ export function sharedBeatsPerBar(id: unknown): number {
140
+ return parseMeter(id).sharedBeatsPerBar;
141
+ }
142
+
143
+ /**
144
+ * Resolve the per-track meter from assembled track data — the LOAD side, walking the SAME precedence
145
+ * as the shared tempo resolver (`resolveTrackBpmFromTrackData`): owner CANONICAL first, then the
146
+ * per-user override, then auto-detected analysis, then the default. First valid wins:
147
+ * 1) owner canonical — `metadata.meter` → `metadata.effectiveSettings.sync.meter`
148
+ * 2) per-user — `trackUserSettings.sync.meter`
149
+ * 3) auto-detected — `metadata.audioAnalysis.analysis` time signature
150
+ * 4) default (4/4)
151
+ */
152
+ export function resolveTrackMeterFromTrackData(trackData: TrackDataLike | null = null): string {
153
+ const td = trackData;
154
+ const candidates = [
155
+ // 1) owner canonical — top-level metadata (mirrors tempo's `metadata.trackBpm`)
156
+ td?.track?.metadata?.meter,
157
+ td?.release?.metadata?.meter,
158
+ td?.metadata?.meter,
159
+ // 1) owner canonical — effectiveSettings (mirrors tempo's `effectiveSettings.sync.trackBpm`)
160
+ td?.track?.metadata?.effectiveSettings?.sync?.meter,
161
+ td?.release?.metadata?.effectiveSettings?.sync?.meter,
162
+ td?.metadata?.effectiveSettings?.sync?.meter,
163
+ td?.track?.effectiveSettings?.sync?.meter,
164
+ td?.release?.effectiveSettings?.sync?.meter,
165
+ td?.effectiveSettings?.sync?.meter,
166
+ // 2) per-user override (mirrors tempo's `trackUserSettings.sync.trackBpm`)
167
+ td?.trackUserSettings?.sync?.meter,
168
+ // 3) auto-detected analysis (mirrors tempo's `audioAnalysis.analysis.tempo_bpm`)
169
+ resolveAnalysisMeter(td?.track?.metadata?.audioAnalysis?.analysis),
170
+ resolveAnalysisMeter(td?.release?.metadata?.audioAnalysis?.analysis),
171
+ resolveAnalysisMeter(td?.metadata?.audioAnalysis?.analysis),
172
+ ];
173
+ for (const c of candidates) {
174
+ if (isValidMeterId(c)) return normalizeMeterId(c);
175
+ }
176
+ return DEFAULT_METER_ID;
177
+ }
@@ -5,13 +5,16 @@
5
5
  // startSec: gridSec, bpm }] })`. Promoted out of the orbiters app (ORB-137) so EW reuses it verbatim.
6
6
 
7
7
  /**
8
- * The slice of a track's data these resolvers read. Every field is optional and untyped at the leaf
9
- * because the runtime objects come from different app layers (release metadata, effective owner
10
- * settings, per-user overrides, audio analysis). Resolvers coerce leaves with Number(...) and ignore
11
- * anything non-finite, so a permissive shape here is intentional — not every consumer fills every path.
8
+ * The slice of a track's data these resolvers read (tempo here; meter in `./clock/meter`). Every field
9
+ * is optional and untyped at the leaf because the runtime objects come from different app layers
10
+ * (release metadata, effective owner settings, per-user overrides, audio analysis). Resolvers coerce
11
+ * leaves with Number(...)/String(...) and ignore anything invalid, so a permissive shape here is
12
+ * intentional — not every consumer fills every path.
12
13
  */
13
14
  export interface SyncSettingsLike {
14
15
  trackBpm?: unknown;
16
+ /** Per-track time signature (e.g. `'4/4'` or `{ numerator, denominator }`) — see `./clock/meter`. */
17
+ meter?: unknown;
15
18
  primaryGridMarkerTimeSec?: unknown;
16
19
  gridMarkers?: ReadonlyArray<{ sourceTimeSec?: unknown } | null | undefined> | null;
17
20
  gridStartTimeSec?: unknown;
@@ -19,9 +22,13 @@ export interface SyncSettingsLike {
19
22
 
20
23
  export interface TrackMetadataLike {
21
24
  trackBpm?: unknown;
25
+ /** Per-track time signature (owner canonical) — see `./clock/meter`. */
26
+ meter?: unknown;
22
27
  primaryGridMarkerTimeSec?: unknown;
23
28
  effectiveSettings?: { sync?: SyncSettingsLike | null } | null;
24
- audioAnalysis?: { analysis?: { tempo_bpm?: unknown } | null } | null;
29
+ audioAnalysis?: {
30
+ analysis?: { tempo_bpm?: unknown; time_signature?: unknown; timeSignature?: unknown } | null;
31
+ } | null;
25
32
  wrap?: {
26
33
  gridMarkers?: ReadonlyArray<{ sourceTimeSec?: unknown } | null | undefined> | null;
27
34
  gridStartTimeSec?: unknown;
@@ -29,9 +36,16 @@ export interface TrackMetadataLike {
29
36
  }
30
37
 
31
38
  export interface TrackDataLike {
32
- track?: { metadata?: TrackMetadataLike | null } | null;
33
- release?: { metadata?: TrackMetadataLike | null } | null;
39
+ track?: {
40
+ metadata?: TrackMetadataLike | null;
41
+ effectiveSettings?: { sync?: SyncSettingsLike | null } | null;
42
+ } | null;
43
+ release?: {
44
+ metadata?: TrackMetadataLike | null;
45
+ effectiveSettings?: { sync?: SyncSettingsLike | null } | null;
46
+ } | null;
34
47
  metadata?: TrackMetadataLike | null;
48
+ effectiveSettings?: { sync?: SyncSettingsLike | null } | null;
35
49
  trackUserSettings?: { sync?: SyncSettingsLike | null } | null;
36
50
  }
37
51
 
@@ -175,19 +175,19 @@ export async function loadCubeMap({ faceKeys, skycubeName, preferKTX2 = true, fo
175
175
  try {
176
176
  return await loadFacesKTX2(resolvedUrls, renderer);
177
177
  } catch (e) {
178
- console.warn('[CubeMapLoader] KTX2 load failed, trying JPG fallback:', e);
179
- // Fall through to JPG attempt
178
+ console.warn('[CubeMapLoader] KTX2 load failed, trying PNG fallback:', e);
179
+ // Fall through to PNG attempt
180
180
  }
181
181
  }
182
182
 
183
- // Try JPG fallback (convert URLs if needed)
184
- const jpgUrls = {};
183
+ // Try PNG fallback (convert URLs if needed)
184
+ const pngUrls = {};
185
185
  FACE_ORDER.forEach(face => {
186
- jpgUrls[face] = resolvedUrls[face].replace(/\.ktx2$/, '.jpg');
186
+ pngUrls[face] = resolvedUrls[face].replace(/\.ktx2$/, '.png');
187
187
  });
188
188
 
189
189
  try {
190
- return await loadFacesJPG(jpgUrls);
190
+ return await loadFacesJPG(pngUrls);
191
191
  } catch (e) {
192
192
  console.error('[CubeMapLoader] All loading attempts failed:', e);
193
193
  throw new Error('Failed to load cube map with any format');