@blibliki/transport 0.5.2 → 0.9.0
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.
- package/README.md +100 -23
- package/dist/index.d.ts +155 -31
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
- package/src/Clock.ts +15 -0
- package/src/Transport.ts +127 -75
- package/src/index.ts +25 -1
- package/src/sources/BaseSource.ts +70 -0
- package/src/sources/SourceManager.ts +54 -0
- package/src/sources/StepSequencerSource.ts +310 -0
- package/src/sources/index.ts +0 -0
- package/src/utils.ts +66 -0
package/src/index.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
export { Transport, TransportState } from "./Transport";
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
TransportEvent,
|
|
4
|
+
TransportProperty,
|
|
5
|
+
TransportPropertyChangeCallback,
|
|
6
|
+
} from "./Transport";
|
|
3
7
|
export { Position } from "./Position";
|
|
8
|
+
export {
|
|
9
|
+
TPB,
|
|
10
|
+
divisionToTicks,
|
|
11
|
+
divisionToFrequency,
|
|
12
|
+
divisionToMilliseconds,
|
|
13
|
+
} from "./utils";
|
|
14
|
+
export type { Division } from "./utils";
|
|
4
15
|
export type {
|
|
5
16
|
Seconds,
|
|
6
17
|
Ticks,
|
|
@@ -9,3 +20,16 @@ export type {
|
|
|
9
20
|
ClockTime,
|
|
10
21
|
TimeSignature,
|
|
11
22
|
} from "./types";
|
|
23
|
+
export {
|
|
24
|
+
StepSequencerSource,
|
|
25
|
+
Resolution,
|
|
26
|
+
PlaybackMode,
|
|
27
|
+
} from "./sources/StepSequencerSource";
|
|
28
|
+
export type {
|
|
29
|
+
StepSequencerSourceEvent,
|
|
30
|
+
IStep,
|
|
31
|
+
IStepNote,
|
|
32
|
+
IStepCC,
|
|
33
|
+
IPage,
|
|
34
|
+
IPattern,
|
|
35
|
+
} from "./sources/StepSequencerSource";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { uuidv4 } from "@blibliki/utils";
|
|
2
|
+
import { Transport, TransportEvent } from "@/Transport";
|
|
3
|
+
import { Ticks } from "@/types";
|
|
4
|
+
|
|
5
|
+
export interface SourceEvent extends TransportEvent {
|
|
6
|
+
eventSourceId: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface IBaseSource<T extends SourceEvent> {
|
|
10
|
+
id: string;
|
|
11
|
+
|
|
12
|
+
generator: (start: Ticks, end: Ticks) => readonly Readonly<T>[];
|
|
13
|
+
consumer: (event: Readonly<T>) => void;
|
|
14
|
+
|
|
15
|
+
// Optional lifecycle hooks
|
|
16
|
+
onStart: (ticks: Ticks) => void;
|
|
17
|
+
onStop: (ticks: Ticks) => void;
|
|
18
|
+
onJump: (ticks: Ticks) => void;
|
|
19
|
+
onSilence: (ticks: Ticks) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export abstract class BaseSource<
|
|
23
|
+
T extends SourceEvent,
|
|
24
|
+
> implements IBaseSource<T> {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
|
|
27
|
+
protected transport: Transport;
|
|
28
|
+
protected startedAt?: Ticks;
|
|
29
|
+
protected stoppedAt?: Ticks;
|
|
30
|
+
protected lastGeneratedTick?: Ticks;
|
|
31
|
+
|
|
32
|
+
constructor(transport: Transport) {
|
|
33
|
+
this.id = uuidv4();
|
|
34
|
+
this.transport = transport;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
abstract generator(start: Ticks, end: Ticks): readonly Readonly<T>[];
|
|
38
|
+
abstract consumer(event: Readonly<T>): void;
|
|
39
|
+
|
|
40
|
+
onStart(ticks: Ticks) {
|
|
41
|
+
this.startedAt = ticks;
|
|
42
|
+
this.stoppedAt = undefined;
|
|
43
|
+
this.lastGeneratedTick = undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onStop(ticks: Ticks) {
|
|
47
|
+
this.stoppedAt = ticks;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onJump(ticks: Ticks) {
|
|
51
|
+
this.lastGeneratedTick = ticks;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onSilence(_ticks: Ticks) {
|
|
55
|
+
// Not implemented yet
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected isPlaying(start: Ticks, end: Ticks) {
|
|
59
|
+
const isStarted = this.startedAt !== undefined && this.startedAt <= start;
|
|
60
|
+
if (!isStarted) return false;
|
|
61
|
+
|
|
62
|
+
return this.stoppedAt === undefined || this.stoppedAt >= end;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
protected shouldGenerate(eventTick: Ticks): boolean {
|
|
66
|
+
if (this.lastGeneratedTick === undefined) return true;
|
|
67
|
+
|
|
68
|
+
return eventTick > this.lastGeneratedTick;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Ticks } from "@/types";
|
|
2
|
+
import type { BaseSource, SourceEvent } from "./BaseSource";
|
|
3
|
+
|
|
4
|
+
export class SourceManager {
|
|
5
|
+
private activeSources = new Map<string, BaseSource<SourceEvent>>();
|
|
6
|
+
|
|
7
|
+
addSource<T extends SourceEvent>(source: BaseSource<T>) {
|
|
8
|
+
this.activeSources.set(source.id, source);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
removeSource(id: string) {
|
|
12
|
+
this.activeSources.delete(id);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
generator(start: Ticks, end: Ticks): readonly SourceEvent[] {
|
|
16
|
+
const events: SourceEvent[] = [];
|
|
17
|
+
|
|
18
|
+
this.activeSources.forEach((source) => {
|
|
19
|
+
events.push(...(source.generator(start, end) as SourceEvent[]));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return events;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
consumer(events: readonly SourceEvent[]) {
|
|
26
|
+
events.forEach((event) => {
|
|
27
|
+
this.activeSources.get(event.eventSourceId)?.consumer(event);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onStart(ticks: Ticks) {
|
|
32
|
+
this.activeSources.forEach((source) => {
|
|
33
|
+
source.onStart(ticks);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onStop(ticks: Ticks) {
|
|
38
|
+
this.activeSources.forEach((source) => {
|
|
39
|
+
source.onStop(ticks);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onJump(ticks: Ticks) {
|
|
44
|
+
this.activeSources.forEach((source) => {
|
|
45
|
+
source.onJump(ticks);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onSilence(ticks: Ticks) {
|
|
50
|
+
this.activeSources.forEach((source) => {
|
|
51
|
+
source.onSilence(ticks);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { Transport } from "@/Transport";
|
|
2
|
+
import { Ticks } from "@/types";
|
|
3
|
+
import { Division, TPB } from "@/utils";
|
|
4
|
+
import { BaseSource } from "./BaseSource";
|
|
5
|
+
import type { SourceEvent } from "./BaseSource";
|
|
6
|
+
|
|
7
|
+
export interface StepSequencerSourceEvent extends SourceEvent {
|
|
8
|
+
stepNo: number;
|
|
9
|
+
pageNo: number;
|
|
10
|
+
patternNo: number;
|
|
11
|
+
step: IStep;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type IStepNote = {
|
|
15
|
+
note: string; // "C4", "E4", "G4"
|
|
16
|
+
velocity: number; // 0-127
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type IStepCC = {
|
|
20
|
+
cc: number;
|
|
21
|
+
value: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Individual step
|
|
25
|
+
export type IStep = {
|
|
26
|
+
active: boolean; // Whether step is enabled/muted
|
|
27
|
+
notes: IStepNote[]; // Multiple notes for chords
|
|
28
|
+
ccMessages: IStepCC[]; // Multiple CC messages per step
|
|
29
|
+
probability: number; // 0-100% chance to trigger
|
|
30
|
+
microtimeOffset: number; // -50 to +50 ticks offset
|
|
31
|
+
duration: Division;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Page contains multiple steps
|
|
35
|
+
export type IPage = {
|
|
36
|
+
name: string;
|
|
37
|
+
steps: IStep[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Pattern contains multiple pages
|
|
41
|
+
export type IPattern = {
|
|
42
|
+
name: string;
|
|
43
|
+
pages: IPage[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export enum Resolution {
|
|
47
|
+
thirtysecond = "1/32",
|
|
48
|
+
sixteenth = "1/16",
|
|
49
|
+
eighth = "1/8",
|
|
50
|
+
quarter = "1/4",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export enum PlaybackMode {
|
|
54
|
+
loop = "loop",
|
|
55
|
+
oneShot = "oneShot",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface StepSequencerSourceProps {
|
|
59
|
+
onEvent: (event: StepSequencerSourceEvent) => void;
|
|
60
|
+
patterns: IPattern[];
|
|
61
|
+
stepsPerPage: number; // 1-16 steps per page
|
|
62
|
+
resolution: Resolution;
|
|
63
|
+
playbackMode: PlaybackMode;
|
|
64
|
+
patternSequence: string; // Pattern sequence notation (e.g., "2A4B2AC")
|
|
65
|
+
enableSequence: boolean; // Toggle to enable/disable sequence mode
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const RESOLUTION_TO_TICKS: Record<Resolution, number> = {
|
|
69
|
+
[Resolution.thirtysecond]: TPB / 8, // 1920 ticks
|
|
70
|
+
[Resolution.sixteenth]: TPB / 4, // 3840 ticks
|
|
71
|
+
[Resolution.eighth]: TPB / 2, // 7680 ticks
|
|
72
|
+
[Resolution.quarter]: TPB, // 15360 ticks
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function expandPatternSequence(input: string): string[] {
|
|
76
|
+
const result: string[] = [];
|
|
77
|
+
let num = "";
|
|
78
|
+
|
|
79
|
+
for (const ch of input) {
|
|
80
|
+
if (ch >= "0" && ch <= "9") {
|
|
81
|
+
num += ch;
|
|
82
|
+
} else {
|
|
83
|
+
const count = Number(num);
|
|
84
|
+
for (let i = 0; i < count; i++) {
|
|
85
|
+
result.push(ch);
|
|
86
|
+
}
|
|
87
|
+
num = "";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result.map((v) => v.toUpperCase());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class StepSequencerSource extends BaseSource<StepSequencerSourceEvent> {
|
|
95
|
+
props: StepSequencerSourceProps;
|
|
96
|
+
private expandedSequence: string[] = [];
|
|
97
|
+
private pageMapping: { patternNo: number; pageNo: number }[] = [];
|
|
98
|
+
|
|
99
|
+
constructor(transport: Transport, props: StepSequencerSourceProps) {
|
|
100
|
+
super(transport);
|
|
101
|
+
|
|
102
|
+
this.props = props;
|
|
103
|
+
this.expandedSequence = expandPatternSequence(props.patternSequence);
|
|
104
|
+
this.pageMapping = this.buildPageMapping();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build a mapping of absolute pages to (patternNo, pageNo) for one full sequence cycle
|
|
109
|
+
* Example: sequence "2A1B" with A having 2 pages, B having 1 page produces:
|
|
110
|
+
* [A0, A1, A0, A1, B0]
|
|
111
|
+
*/
|
|
112
|
+
private buildPageMapping(): { patternNo: number; pageNo: number }[] {
|
|
113
|
+
const mapping: { patternNo: number; pageNo: number }[] = [];
|
|
114
|
+
|
|
115
|
+
if (this.props.enableSequence && this.expandedSequence.length > 0) {
|
|
116
|
+
// For each pattern letter in the expanded sequence
|
|
117
|
+
for (const patternLetter of this.expandedSequence) {
|
|
118
|
+
// Find the pattern by name
|
|
119
|
+
const patternNo = this.props.patterns.findIndex(
|
|
120
|
+
(p) => p.name.toUpperCase() === patternLetter,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (patternNo === -1) continue;
|
|
124
|
+
|
|
125
|
+
const pattern = this.props.patterns[patternNo];
|
|
126
|
+
if (!pattern) continue;
|
|
127
|
+
|
|
128
|
+
// Add all pages of this pattern to the mapping
|
|
129
|
+
for (let pageNo = 0; pageNo < pattern.pages.length; pageNo++) {
|
|
130
|
+
mapping.push({ patternNo, pageNo });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
// No sequence mode - build mapping for all patterns sequentially
|
|
135
|
+
for (
|
|
136
|
+
let patternNo = 0;
|
|
137
|
+
patternNo < this.props.patterns.length;
|
|
138
|
+
patternNo++
|
|
139
|
+
) {
|
|
140
|
+
const pattern = this.props.patterns[patternNo];
|
|
141
|
+
if (!pattern) continue;
|
|
142
|
+
|
|
143
|
+
for (let pageNo = 0; pageNo < pattern.pages.length; pageNo++) {
|
|
144
|
+
mapping.push({ patternNo, pageNo });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return mapping;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get stepResolution(): Ticks {
|
|
153
|
+
return RESOLUTION_TO_TICKS[this.props.resolution];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
onStart(ticks: Ticks) {
|
|
157
|
+
// Quantize to the start of the next bar
|
|
158
|
+
const timeSignature = this.transport.timeSignature;
|
|
159
|
+
const ticksPerBar = TPB * timeSignature[0];
|
|
160
|
+
|
|
161
|
+
if (ticks % ticksPerBar === 0) {
|
|
162
|
+
super.onStart(ticks);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Calculate which bar we're in and round up to the next bar
|
|
167
|
+
const currentBar = Math.floor(ticks / ticksPerBar);
|
|
168
|
+
const nextBarTicks = (currentBar + 1) * ticksPerBar;
|
|
169
|
+
|
|
170
|
+
super.onStart(nextBarTicks);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
extractStepsTicks(start: Ticks, end: Ticks): Ticks[] {
|
|
174
|
+
const result: number[] = [];
|
|
175
|
+
const stepResolution = this.stepResolution;
|
|
176
|
+
|
|
177
|
+
// Calculate which step we should be at, then convert back to absolute ticks
|
|
178
|
+
const stepsSinceStart = Math.floor(
|
|
179
|
+
(start - this.startedAt!) / stepResolution,
|
|
180
|
+
);
|
|
181
|
+
const actualStart = this.startedAt! + stepsSinceStart * stepResolution;
|
|
182
|
+
|
|
183
|
+
for (let value = actualStart; value <= end; value += stepResolution) {
|
|
184
|
+
if (!this.shouldGenerate(value)) continue;
|
|
185
|
+
|
|
186
|
+
result.push(value);
|
|
187
|
+
this.lastGeneratedTick = value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private getStep(patternNo: number, pageNo: number, stepNo: number): IStep {
|
|
194
|
+
const pattern = this.props.patterns[patternNo];
|
|
195
|
+
if (!pattern) throw Error("Pattern not found");
|
|
196
|
+
|
|
197
|
+
const page = pattern.pages[pageNo];
|
|
198
|
+
if (!page) throw Error("Page not found");
|
|
199
|
+
|
|
200
|
+
const step = page.steps[stepNo];
|
|
201
|
+
if (!step) throw Error("Step not found");
|
|
202
|
+
|
|
203
|
+
return step;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Calculate ticks per page based on step resolution and steps per page
|
|
208
|
+
*/
|
|
209
|
+
get ticksPerPage(): Ticks {
|
|
210
|
+
return this.stepResolution * this.props.stepsPerPage;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Calculate absolute page number from tick position
|
|
215
|
+
*/
|
|
216
|
+
private getAbsolutePageFromTicks(ticks: Ticks): number {
|
|
217
|
+
if (this.startedAt === undefined) return 0;
|
|
218
|
+
|
|
219
|
+
const ticksSinceStart = ticks - this.startedAt;
|
|
220
|
+
return Math.floor(ticksSinceStart / this.ticksPerPage);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Calculate step number within the current page from tick position
|
|
225
|
+
*/
|
|
226
|
+
private getStepNoInPage(ticks: Ticks): number {
|
|
227
|
+
if (this.startedAt === undefined) return 0;
|
|
228
|
+
|
|
229
|
+
const ticksSinceStart = ticks - this.startedAt;
|
|
230
|
+
const ticksIntoPage = ticksSinceStart % this.ticksPerPage;
|
|
231
|
+
return Math.floor(ticksIntoPage / this.stepResolution);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Map absolute page number to actual pattern and page indices
|
|
236
|
+
* Takes into account sequence mode and playback mode (loop vs oneShot)
|
|
237
|
+
*/
|
|
238
|
+
private getPatternAndPageFromAbsolutePage(absolutePage: number): {
|
|
239
|
+
patternNo: number;
|
|
240
|
+
pageNo: number;
|
|
241
|
+
} {
|
|
242
|
+
if (this.pageMapping.length === 0) {
|
|
243
|
+
return { patternNo: 0, pageNo: 0 };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let index: number;
|
|
247
|
+
if (this.props.playbackMode === PlaybackMode.loop) {
|
|
248
|
+
index = absolutePage % this.pageMapping.length;
|
|
249
|
+
} else {
|
|
250
|
+
// oneShot mode - stop at the last page
|
|
251
|
+
index = Math.min(absolutePage, this.pageMapping.length - 1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return this.pageMapping[index] ?? { patternNo: 0, pageNo: 0 };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
generator(
|
|
258
|
+
start: Ticks,
|
|
259
|
+
end: Ticks,
|
|
260
|
+
): readonly Readonly<StepSequencerSourceEvent>[] {
|
|
261
|
+
if (!this.isPlaying(start, end) || this.startedAt === undefined) return [];
|
|
262
|
+
|
|
263
|
+
const stepTicks = this.extractStepsTicks(start, end);
|
|
264
|
+
|
|
265
|
+
// Check if we should stop in oneShot mode
|
|
266
|
+
if (
|
|
267
|
+
this.props.playbackMode === PlaybackMode.oneShot &&
|
|
268
|
+
stepTicks.length > 0
|
|
269
|
+
) {
|
|
270
|
+
const lastTick = stepTicks[stepTicks.length - 1];
|
|
271
|
+
if (lastTick !== undefined) {
|
|
272
|
+
const absolutePage = this.getAbsolutePageFromTicks(lastTick);
|
|
273
|
+
const stepNo = this.getStepNoInPage(lastTick);
|
|
274
|
+
|
|
275
|
+
// If we've reached or passed the last page and last step, stop the source
|
|
276
|
+
if (
|
|
277
|
+
absolutePage >= this.pageMapping.length - 1 &&
|
|
278
|
+
stepNo >= this.props.stepsPerPage - 1
|
|
279
|
+
) {
|
|
280
|
+
// Stop after this final step
|
|
281
|
+
this.onStop(lastTick + this.stepResolution);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return stepTicks.map((ticks) => {
|
|
287
|
+
const absolutePage = this.getAbsolutePageFromTicks(ticks);
|
|
288
|
+
const { patternNo, pageNo } =
|
|
289
|
+
this.getPatternAndPageFromAbsolutePage(absolutePage);
|
|
290
|
+
const stepNo = this.getStepNoInPage(ticks);
|
|
291
|
+
|
|
292
|
+
const step = this.getStep(patternNo, pageNo, stepNo);
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
ticks,
|
|
296
|
+
time: 0,
|
|
297
|
+
contextTime: 0,
|
|
298
|
+
eventSourceId: this.id,
|
|
299
|
+
stepNo,
|
|
300
|
+
pageNo,
|
|
301
|
+
patternNo,
|
|
302
|
+
step,
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
consumer(event: Readonly<StepSequencerSourceEvent>) {
|
|
308
|
+
this.props.onEvent(event);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
File without changes
|
package/src/utils.ts
CHANGED
|
@@ -104,3 +104,69 @@ export function swing(time: Ticks, amount: NormalRange): Ticks {
|
|
|
104
104
|
// Map transformed time back into ticks
|
|
105
105
|
return Math.floor((t8i + tn) * 7680);
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
// We will update the type to be more flexible when there is a need for it
|
|
109
|
+
export type Division =
|
|
110
|
+
| "1/64"
|
|
111
|
+
| "1/48"
|
|
112
|
+
| "1/32"
|
|
113
|
+
| "1/24"
|
|
114
|
+
| "1/16"
|
|
115
|
+
| "1/12"
|
|
116
|
+
| "1/8"
|
|
117
|
+
| "1/6"
|
|
118
|
+
| "3/16"
|
|
119
|
+
| "1/4"
|
|
120
|
+
| "5/16"
|
|
121
|
+
| "1/3"
|
|
122
|
+
| "3/8"
|
|
123
|
+
| "1/2"
|
|
124
|
+
| "3/4"
|
|
125
|
+
| "1"
|
|
126
|
+
| "1.5"
|
|
127
|
+
| "2"
|
|
128
|
+
| "3"
|
|
129
|
+
| "4"
|
|
130
|
+
| "6"
|
|
131
|
+
| "8"
|
|
132
|
+
| "16"
|
|
133
|
+
| "32"
|
|
134
|
+
| "infinity";
|
|
135
|
+
|
|
136
|
+
export function divisionToTicks(division: Division): Ticks {
|
|
137
|
+
// Defensive: handle invalid input (runtime safety for old data)
|
|
138
|
+
if (typeof division !== "string") {
|
|
139
|
+
console.error(`Invalid duration value, using default "8n."`);
|
|
140
|
+
division = "1/8";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let ticks: Ticks;
|
|
144
|
+
|
|
145
|
+
if (division === "infinity") return Infinity;
|
|
146
|
+
|
|
147
|
+
if (division.match(/\d\/\d/)) {
|
|
148
|
+
const [num, den] = division.split("/").map(Number);
|
|
149
|
+
if (!num || !den)
|
|
150
|
+
throw Error(`Note duration parsing error for value: ${division}`);
|
|
151
|
+
|
|
152
|
+
ticks = (TPB * num) / (den / 4);
|
|
153
|
+
} else {
|
|
154
|
+
ticks = TPB * Number(division) * 4;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return Math.round(ticks);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function divisionToFrequency(division: Division, bpm: BPM): number {
|
|
161
|
+
const ticks = divisionToTicks(division);
|
|
162
|
+
const beatsPerDivision = ticks / TPB;
|
|
163
|
+
const secondsPerDivision = beatsPerDivision * (60 / bpm);
|
|
164
|
+
return 1 / secondsPerDivision;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function divisionToMilliseconds(division: Division, bpm: BPM): number {
|
|
168
|
+
const ticksPerDivision = divisionToTicks(division);
|
|
169
|
+
const beatsPerDivision = ticksPerDivision / TPB;
|
|
170
|
+
const secondsPerDivision = beatsPerDivision * (60 / bpm);
|
|
171
|
+
return secondsPerDivision * 1000;
|
|
172
|
+
}
|