@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.
- package/README.md +136 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +145 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -0
- package/src/Clock.ts +58 -0
- package/src/Position.ts +95 -0
- package/src/Scheduler.ts +94 -0
- package/src/Tempo.ts +36 -0
- package/src/Timeline.ts +60 -0
- package/src/Timer.ts +49 -0
- package/src/Transport.ts +276 -0
- package/src/audio-context-timers.ts +125 -0
- package/src/index.ts +11 -0
- package/src/types.ts +16 -0
- package/src/utils.ts +106 -0
|
@@ -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
|
+
}
|