@blibliki/transport 0.3.4

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,125 @@
1
+ function generateUniqueNumber(map: Map<number, () => void>): number {
2
+ let nextId = 1;
3
+
4
+ // Keep incrementing until we find an unused ID
5
+ while (map.has(nextId)) {
6
+ nextId++;
7
+ }
8
+
9
+ return nextId;
10
+ }
11
+
12
+ class WebAudioWrapper {
13
+ private _audioBuffer: AudioBuffer | null = null;
14
+ private _audioContext: AudioContext | null = null;
15
+ private _sampleDuration: number | null = null;
16
+
17
+ get audioContext(): AudioContext {
18
+ this._audioContext ??= new AudioContext();
19
+ return this._audioContext;
20
+ }
21
+
22
+ get audioBuffer(): AudioBuffer {
23
+ this._audioBuffer ??= new AudioBuffer({
24
+ length: 2,
25
+ sampleRate: this.audioContext.sampleRate,
26
+ });
27
+ return this._audioBuffer;
28
+ }
29
+
30
+ get sampleDuration(): number {
31
+ this._sampleDuration ??= 2 / this.audioContext.sampleRate;
32
+ return this._sampleDuration;
33
+ }
34
+ }
35
+
36
+ const webAudioWrapper = new WebAudioWrapper();
37
+
38
+ const SCHEDULED_TIMEOUT_FUNCTIONS = new Map<number, () => void>();
39
+ const SCHEDULED_INTERVAL_FUNCTIONS = new Map<number, () => void>();
40
+
41
+ enum TimerType {
42
+ interval = "interval",
43
+ timeout = "timeout",
44
+ }
45
+
46
+ const callIntervalFunction = (id: number, type: TimerType) => {
47
+ const functions =
48
+ type === TimerType.interval
49
+ ? SCHEDULED_INTERVAL_FUNCTIONS
50
+ : SCHEDULED_TIMEOUT_FUNCTIONS;
51
+
52
+ if (functions.has(id)) {
53
+ const func = functions.get(id);
54
+
55
+ if (func !== undefined) {
56
+ func();
57
+
58
+ if (type === TimerType.timeout) {
59
+ SCHEDULED_TIMEOUT_FUNCTIONS.delete(id);
60
+ }
61
+ }
62
+ }
63
+ };
64
+
65
+ const scheduleFunction = (id: number, delay: number, type: TimerType) => {
66
+ const now = performance.now();
67
+
68
+ const audioBufferSourceNode = new AudioBufferSourceNode(
69
+ webAudioWrapper.audioContext,
70
+ { buffer: webAudioWrapper.audioBuffer },
71
+ );
72
+
73
+ audioBufferSourceNode.onended = () => {
74
+ const elapsedTime = performance.now() - now;
75
+
76
+ if (elapsedTime >= delay) {
77
+ callIntervalFunction(id, type);
78
+ } else {
79
+ scheduleFunction(id, delay - elapsedTime, type);
80
+ }
81
+
82
+ audioBufferSourceNode.disconnect(webAudioWrapper.audioContext.destination);
83
+ };
84
+ audioBufferSourceNode.connect(webAudioWrapper.audioContext.destination);
85
+ audioBufferSourceNode.start(
86
+ Math.max(
87
+ 0,
88
+ webAudioWrapper.audioContext.currentTime +
89
+ delay / 1000 -
90
+ webAudioWrapper.sampleDuration,
91
+ ),
92
+ );
93
+ };
94
+
95
+ export const clearInterval = (id: number) => {
96
+ SCHEDULED_INTERVAL_FUNCTIONS.delete(id);
97
+ };
98
+
99
+ export const clearTimeout = (id: number) => {
100
+ SCHEDULED_TIMEOUT_FUNCTIONS.delete(id);
101
+ };
102
+
103
+ export const setInterval = (func: () => void, delay: number) => {
104
+ const id = generateUniqueNumber(SCHEDULED_INTERVAL_FUNCTIONS);
105
+
106
+ SCHEDULED_INTERVAL_FUNCTIONS.set(id, () => {
107
+ func();
108
+
109
+ scheduleFunction(id, delay, TimerType.interval);
110
+ });
111
+
112
+ scheduleFunction(id, delay, TimerType.interval);
113
+
114
+ return id;
115
+ };
116
+
117
+ export const setTimeout = (func: () => void, delay: number) => {
118
+ const id = generateUniqueNumber(SCHEDULED_TIMEOUT_FUNCTIONS);
119
+
120
+ SCHEDULED_TIMEOUT_FUNCTIONS.set(id, func);
121
+
122
+ scheduleFunction(id, delay, TimerType.timeout);
123
+
124
+ return id;
125
+ };
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { Transport, TransportState } from "./Transport";
2
+ export type { TransportEvent } from "./Transport";
3
+ export { Position } from "./Position";
4
+ export type {
5
+ Seconds,
6
+ Ticks,
7
+ BPM,
8
+ ContextTime,
9
+ ClockTime,
10
+ TimeSignature,
11
+ } from "./types";
package/src/types.ts ADDED
@@ -0,0 +1,16 @@
1
+ export type Seconds = number;
2
+
3
+ export type ClockTime = number;
4
+ export type ContextTime = number;
5
+ export type Ticks = number;
6
+
7
+ export type BPM = number;
8
+ export type TimeSignature = [number, TimeSignatureDenominator];
9
+ export type TimeSignatureDenominator = 2 | 4 | 8 | 16;
10
+ export type TPosition = { bars: number; beats: number; sixteenths: number };
11
+ export type TStringPosition = `${number}:${number}:${number}`;
12
+
13
+ /**
14
+ * A number that is between [0, 1]
15
+ */
16
+ export type NormalRange = number;
package/src/utils.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { BPM, NormalRange, Ticks } from "./types";
2
+
3
+ // Ticks per beat
4
+ export const TPB = 15360;
5
+
6
+ export const secondsPerTick = (tempo: BPM) => {
7
+ return 60 / tempo / TPB;
8
+ };
9
+
10
+ export const ticksPerSecond = (tempo: BPM) => {
11
+ return (tempo / 60) * TPB;
12
+ };
13
+
14
+ export const insertionIndexBy = <T>(
15
+ arr: T[],
16
+ n: T,
17
+ comparatorFn: (a: T, b: T) => number,
18
+ ) => {
19
+ const index = arr.findIndex((el) => comparatorFn(n, el) < 0);
20
+ return index === -1 ? arr.length : index;
21
+ };
22
+
23
+ export function lerp(t: number, a: number, b: number): number {
24
+ return a + t * (b - a);
25
+ }
26
+
27
+ export function unlerp(t: number, a: number, b: number): number {
28
+ return (t - a) / (b - a);
29
+ }
30
+
31
+ /**
32
+ * Compute swing.
33
+ *
34
+ * This function is an attempt at mirroring the behaviour in the Reason DAW,
35
+ * where events before the 50% eighth mark are "expanded" and events past the
36
+ * mark are "compressed". That is, this function transforms the underlying
37
+ * transport time grid:
38
+ *
39
+ * No swing |<--50%-->|<--50%-->|<--50%-->| ...
40
+ * 16ths 0 1 2 3 ...
41
+ * 8ths 0 | 1 | ...
42
+ * Notes X======X X===X | |
43
+ * | | | |
44
+ * | \ | \
45
+ * 60% swing |<---60%--->|<-40%->|<---60%--->| ...
46
+ * 16ths 0 1 2 3 ...
47
+ * 8ths 0 | 1 | ...
48
+ * Notes X=======X X==X | |
49
+ *
50
+ * If it is challenging to explain this mechanism to end users, you could
51
+ * use the approach taken in the Logic DAW, which has 6 "swing modes":
52
+ *
53
+ * 16A: 50%
54
+ * 16B: 54%
55
+ * 16C: 58%
56
+ * 16D: 62%
57
+ * 16E: 66%
58
+ * 16F: 71%
59
+ *
60
+ * For details, see
61
+ * https://www.attackmagazine.com/technique/passing-notes/daw-drum-machine-swing/2/
62
+ *
63
+ * @param time Time of event, in transport ticks (1/4 = 15360 pulses)
64
+ * @param amount Swing amount in range [0.5, 0.75].
65
+ * @returns New time of the event, with swing applied.
66
+ */
67
+ export function swing(time: Ticks, amount: NormalRange): Ticks {
68
+ if (amount < 0.5 || amount > 0.75) {
69
+ throw new Error("Invalid swing amount");
70
+ }
71
+
72
+ /*
73
+ Note lengths in ticks:
74
+
75
+ 1/4 = 15360
76
+ 1/8 = 7680
77
+ 1/16 = 3840
78
+ */
79
+
80
+ const t8 = time / 7680; // Input time in 8ths
81
+ const t8i = Math.floor(t8); // 8th in which the time sits
82
+ const t = t8 - t8i; // Percentage position in the 8th in which the time sits
83
+
84
+ /*
85
+ ### We could make this a bit more efficient; we can simplify the
86
+ <=0.5 case to
87
+
88
+ (t / 0.5) * amount
89
+
90
+ But the >0.5 case is more difficult:
91
+
92
+ amount + ((t - 0.5) / 0.5) * (1 - amount)
93
+
94
+ Using the lerp/unlerp functions here makes it clear what's going on,
95
+ though, so we'll keep them for now.
96
+ */
97
+ let tn = 0;
98
+ if (t <= 0.5) {
99
+ tn = lerp(unlerp(t, 0.0, 0.5), 0.0, amount);
100
+ } else {
101
+ tn = lerp(unlerp(t, 0.5, 1.0), amount, 1.0);
102
+ }
103
+
104
+ // Map transformed time back into ticks
105
+ return Math.floor((t8i + tn) * 7680);
106
+ }