@octoseq/mir 0.1.0-main.4ecb074 → 0.1.0-main.5fdb072
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/dist/chunk-HF3QHCRK.js +3234 -0
- package/dist/chunk-HF3QHCRK.js.map +1 -0
- package/dist/index.d.ts +2122 -47
- package/dist/index.js +3524 -39
- package/dist/index.js.map +1 -1
- package/dist/runMir-CnJQbBr8.d.ts +187 -0
- package/dist/runner/runMir.d.ts +2 -2
- package/dist/runner/runMir.js +1 -1
- package/dist/runner/workerProtocol.d.ts +8 -1
- package/dist/runner/workerProtocol.js.map +1 -1
- package/dist/types-DqH4umN8.d.ts +761 -0
- package/package.json +2 -2
- package/src/dsp/activity.ts +544 -0
- package/src/dsp/bandCqt.ts +662 -0
- package/src/dsp/bandEvents.ts +351 -0
- package/src/dsp/bandMask.ts +225 -0
- package/src/dsp/bandMir.ts +524 -0
- package/src/dsp/bandProposal.ts +552 -0
- package/src/dsp/beatCandidates.ts +299 -0
- package/src/dsp/cqt.ts +386 -0
- package/src/dsp/cqtSignals.ts +462 -0
- package/src/dsp/customSignalReduction.ts +841 -0
- package/src/dsp/eventToSignal.ts +531 -0
- package/src/dsp/frequencyBand.ts +956 -0
- package/src/dsp/mel.ts +56 -3
- package/src/dsp/musicalTime.ts +240 -0
- package/src/dsp/onset.ts +296 -30
- package/src/dsp/peakPicking.ts +519 -0
- package/src/dsp/phaseAlignment.ts +153 -0
- package/src/dsp/pitch.ts +289 -0
- package/src/dsp/resample.ts +44 -0
- package/src/dsp/signalTransforms.ts +660 -0
- package/src/dsp/silenceGating.ts +511 -0
- package/src/dsp/spectral.ts +124 -4
- package/src/dsp/tempoHypotheses.ts +395 -0
- package/src/gpu/bufferPool.ts +266 -0
- package/src/gpu/context.ts +30 -6
- package/src/gpu/helpers.ts +85 -0
- package/src/gpu/melProject.ts +83 -0
- package/src/index.ts +366 -5
- package/src/runner/runMir.ts +234 -2
- package/src/runner/workerProtocol.ts +9 -1
- package/src/types.ts +768 -3
- package/dist/chunk-DUWYCAVG.js +0 -1525
- package/dist/chunk-DUWYCAVG.js.map +0 -1
- package/dist/runMir-CSIBwNZ3.d.ts +0 -84
- package/dist/types-BE3py4fZ.d.ts +0 -83
package/src/dsp/mel.ts
CHANGED
|
@@ -24,15 +24,68 @@ function assertPositiveInt(name: string, value: number): void {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Convert frequency in Hz to mel scale.
|
|
29
|
+
* Uses Slaney-style (HTK-like) approximation.
|
|
30
|
+
*/
|
|
31
|
+
export function hzToMel(hz: number): number {
|
|
29
32
|
return 2595 * Math.log10(1 + hz / 700);
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Convert mel scale value to frequency in Hz.
|
|
37
|
+
* Inverse of hzToMel.
|
|
38
|
+
*/
|
|
39
|
+
export function melToHz(mel: number): number {
|
|
33
40
|
return 700 * (Math.pow(10, mel / 2595) - 1);
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Configuration for mel frequency conversions.
|
|
45
|
+
*/
|
|
46
|
+
export type MelConversionConfig = {
|
|
47
|
+
nMels: number;
|
|
48
|
+
fMin: number;
|
|
49
|
+
fMax: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Convert a frequency in Hz to a feature index (0 to nMels-1).
|
|
54
|
+
* The mapping is linear in mel space.
|
|
55
|
+
*
|
|
56
|
+
* @param hz - Frequency in Hz
|
|
57
|
+
* @param config - Mel configuration with nMels, fMin, fMax
|
|
58
|
+
* @returns Feature index as a continuous value (may be fractional)
|
|
59
|
+
*/
|
|
60
|
+
export function hzToFeatureIndex(hz: number, config: MelConversionConfig): number {
|
|
61
|
+
const melMin = hzToMel(config.fMin);
|
|
62
|
+
const melMax = hzToMel(config.fMax);
|
|
63
|
+
const melHz = hzToMel(hz);
|
|
64
|
+
|
|
65
|
+
// Map mel value to 0..1 range, then to 0..(nMels-1)
|
|
66
|
+
const normalized = (melHz - melMin) / (melMax - melMin);
|
|
67
|
+
return normalized * (config.nMels - 1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Convert a feature index (0 to nMels-1) to frequency in Hz.
|
|
72
|
+
* Inverse of hzToFeatureIndex.
|
|
73
|
+
*
|
|
74
|
+
* @param index - Feature index (can be fractional)
|
|
75
|
+
* @param config - Mel configuration with nMels, fMin, fMax
|
|
76
|
+
* @returns Frequency in Hz
|
|
77
|
+
*/
|
|
78
|
+
export function featureIndexToHz(index: number, config: MelConversionConfig): number {
|
|
79
|
+
const melMin = hzToMel(config.fMin);
|
|
80
|
+
const melMax = hzToMel(config.fMax);
|
|
81
|
+
|
|
82
|
+
// Map index from 0..(nMels-1) to mel space
|
|
83
|
+
const normalized = index / (config.nMels - 1);
|
|
84
|
+
const mel = melMin + normalized * (melMax - melMin);
|
|
85
|
+
|
|
86
|
+
return melToHz(mel);
|
|
87
|
+
}
|
|
88
|
+
|
|
36
89
|
function buildMelFilterBank(
|
|
37
90
|
sampleRate: number,
|
|
38
91
|
fftSize: number,
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Musical Time utilities for B4.
|
|
3
|
+
*
|
|
4
|
+
* These functions compute derived values from musical time structures.
|
|
5
|
+
* All computations are deterministic and pure.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { BeatPosition, MusicalTimeSegment, MusicalTimeStructure, BeatGrid } from "../types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find the segment containing a given time.
|
|
12
|
+
* Segments are non-overlapping, so at most one segment contains any time.
|
|
13
|
+
*
|
|
14
|
+
* @param time - Time in seconds from track start
|
|
15
|
+
* @param segments - Ordered list of segments (by startTime ascending)
|
|
16
|
+
* @returns The containing segment, or null if time is outside all segments
|
|
17
|
+
*/
|
|
18
|
+
export function findSegmentAtTime(
|
|
19
|
+
time: number,
|
|
20
|
+
segments: MusicalTimeSegment[]
|
|
21
|
+
): MusicalTimeSegment | null {
|
|
22
|
+
// Binary search would be more efficient for many segments,
|
|
23
|
+
// but linear scan is fine for typical track segment counts (<10)
|
|
24
|
+
for (const segment of segments) {
|
|
25
|
+
if (time >= segment.startTime && time < segment.endTime) {
|
|
26
|
+
return segment;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compute the beat position at a given time.
|
|
34
|
+
*
|
|
35
|
+
* Beat position formula:
|
|
36
|
+
* period = 60 / bpm
|
|
37
|
+
* beatsFromStart = (time - phaseOffset) / period
|
|
38
|
+
* beatIndex = floor(beatsFromStart)
|
|
39
|
+
* beatPhase = beatsFromStart - beatIndex (fractional part, 0-1)
|
|
40
|
+
* beatPosition = beatsFromStart (continuous)
|
|
41
|
+
*
|
|
42
|
+
* @param time - Time in seconds from track start
|
|
43
|
+
* @param segments - Ordered list of segments (by startTime ascending)
|
|
44
|
+
* @returns BeatPosition if time is within a segment, null otherwise
|
|
45
|
+
*/
|
|
46
|
+
export function computeBeatPosition(
|
|
47
|
+
time: number,
|
|
48
|
+
segments: MusicalTimeSegment[]
|
|
49
|
+
): BeatPosition | null {
|
|
50
|
+
const segment = findSegmentAtTime(time, segments);
|
|
51
|
+
if (!segment) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const period = 60 / segment.bpm;
|
|
56
|
+
const beatsFromStart = (time - segment.phaseOffset) / period;
|
|
57
|
+
|
|
58
|
+
// Handle edge case: time before first beat in segment
|
|
59
|
+
// beatIndex should never be negative within a properly defined segment
|
|
60
|
+
const beatIndex = Math.floor(beatsFromStart);
|
|
61
|
+
const beatPhase = beatsFromStart - beatIndex;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
segmentId: segment.id,
|
|
65
|
+
beatIndex,
|
|
66
|
+
beatPhase,
|
|
67
|
+
beatPosition: beatsFromStart,
|
|
68
|
+
bpm: segment.bpm,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compute beat position from a MusicalTimeStructure.
|
|
74
|
+
* Convenience wrapper for computeBeatPosition.
|
|
75
|
+
*/
|
|
76
|
+
export function computeBeatPositionFromStructure(
|
|
77
|
+
time: number,
|
|
78
|
+
structure: MusicalTimeStructure | null
|
|
79
|
+
): BeatPosition | null {
|
|
80
|
+
if (!structure || structure.segments.length === 0) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return computeBeatPosition(time, structure.segments);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate a unique segment ID.
|
|
88
|
+
*/
|
|
89
|
+
export function generateSegmentId(): string {
|
|
90
|
+
return `seg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a new MusicalTimeSegment from a BeatGrid.
|
|
95
|
+
*
|
|
96
|
+
* @param grid - The beat grid to promote
|
|
97
|
+
* @param startTime - Segment start boundary in seconds
|
|
98
|
+
* @param endTime - Segment end boundary in seconds
|
|
99
|
+
* @returns A new MusicalTimeSegment
|
|
100
|
+
*/
|
|
101
|
+
export function createSegmentFromGrid(
|
|
102
|
+
grid: BeatGrid,
|
|
103
|
+
startTime: number,
|
|
104
|
+
endTime: number
|
|
105
|
+
): MusicalTimeSegment {
|
|
106
|
+
const effectivePhaseOffset = grid.phaseOffset + grid.userNudge;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id: generateSegmentId(),
|
|
110
|
+
bpm: grid.bpm,
|
|
111
|
+
phaseOffset: effectivePhaseOffset,
|
|
112
|
+
startTime,
|
|
113
|
+
endTime,
|
|
114
|
+
confidence: grid.confidence,
|
|
115
|
+
provenance: {
|
|
116
|
+
source: "promoted_from_hypothesis",
|
|
117
|
+
sourceHypothesisId: grid.sourceHypothesisId,
|
|
118
|
+
promotedAt: new Date().toISOString(),
|
|
119
|
+
userNudge: grid.userNudge,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a new empty MusicalTimeStructure.
|
|
126
|
+
*/
|
|
127
|
+
export function createMusicalTimeStructure(): MusicalTimeStructure {
|
|
128
|
+
const now = new Date().toISOString();
|
|
129
|
+
return {
|
|
130
|
+
version: 1,
|
|
131
|
+
segments: [],
|
|
132
|
+
createdAt: now,
|
|
133
|
+
modifiedAt: now,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Validate that segments are non-overlapping and ordered.
|
|
139
|
+
*
|
|
140
|
+
* @param segments - List of segments to validate
|
|
141
|
+
* @returns Array of validation errors (empty if valid)
|
|
142
|
+
*/
|
|
143
|
+
export function validateSegments(segments: MusicalTimeSegment[]): string[] {
|
|
144
|
+
const errors: string[] = [];
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < segments.length; i++) {
|
|
147
|
+
const seg = segments[i];
|
|
148
|
+
if (!seg) continue;
|
|
149
|
+
|
|
150
|
+
// Check segment bounds
|
|
151
|
+
if (seg.startTime >= seg.endTime) {
|
|
152
|
+
errors.push(`Segment ${seg.id}: startTime (${seg.startTime}) >= endTime (${seg.endTime})`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (seg.bpm <= 0) {
|
|
156
|
+
errors.push(`Segment ${seg.id}: bpm (${seg.bpm}) must be positive`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check ordering and non-overlap with next segment
|
|
160
|
+
if (i < segments.length - 1) {
|
|
161
|
+
const next = segments[i + 1];
|
|
162
|
+
if (!next) continue;
|
|
163
|
+
if (seg.startTime >= next.startTime) {
|
|
164
|
+
errors.push(`Segment ${seg.id} not ordered before ${next.id}`);
|
|
165
|
+
}
|
|
166
|
+
if (seg.endTime > next.startTime) {
|
|
167
|
+
errors.push(`Segment ${seg.id} overlaps with ${next.id}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return errors;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Sort segments by startTime ascending.
|
|
177
|
+
* Returns a new array (does not mutate input).
|
|
178
|
+
*/
|
|
179
|
+
export function sortSegments(segments: MusicalTimeSegment[]): MusicalTimeSegment[] {
|
|
180
|
+
return [...segments].sort((a, b) => a.startTime - b.startTime);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Split a segment at a given time.
|
|
185
|
+
*
|
|
186
|
+
* @param segment - The segment to split
|
|
187
|
+
* @param splitTime - Time in seconds where to split
|
|
188
|
+
* @returns Tuple of [beforeSegment, afterSegment]
|
|
189
|
+
* @throws If splitTime is outside segment bounds
|
|
190
|
+
*/
|
|
191
|
+
export function splitSegment(
|
|
192
|
+
segment: MusicalTimeSegment,
|
|
193
|
+
splitTime: number
|
|
194
|
+
): [MusicalTimeSegment, MusicalTimeSegment] {
|
|
195
|
+
if (splitTime <= segment.startTime || splitTime >= segment.endTime) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Split time ${splitTime} must be within segment bounds [${segment.startTime}, ${segment.endTime})`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const beforeSegment: MusicalTimeSegment = {
|
|
202
|
+
...segment,
|
|
203
|
+
id: generateSegmentId(),
|
|
204
|
+
endTime: splitTime,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const afterSegment: MusicalTimeSegment = {
|
|
208
|
+
...segment,
|
|
209
|
+
id: generateSegmentId(),
|
|
210
|
+
startTime: splitTime,
|
|
211
|
+
// phaseOffset stays the same - beat grid continues seamlessly
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return [beforeSegment, afterSegment];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Generate beat times within a segment.
|
|
219
|
+
* Used for rendering beat markers.
|
|
220
|
+
*
|
|
221
|
+
* @param segment - The segment to generate beats for
|
|
222
|
+
* @returns Array of beat times in seconds
|
|
223
|
+
*/
|
|
224
|
+
export function generateSegmentBeatTimes(segment: MusicalTimeSegment): number[] {
|
|
225
|
+
const period = 60 / segment.bpm;
|
|
226
|
+
const times: number[] = [];
|
|
227
|
+
|
|
228
|
+
// Find first beat at or after segment start
|
|
229
|
+
const beatsBeforeStart = Math.ceil((segment.startTime - segment.phaseOffset) / period);
|
|
230
|
+
let time = segment.phaseOffset + beatsBeforeStart * period;
|
|
231
|
+
|
|
232
|
+
while (time < segment.endTime) {
|
|
233
|
+
if (time >= segment.startTime) {
|
|
234
|
+
times.push(time);
|
|
235
|
+
}
|
|
236
|
+
time += period;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return times;
|
|
240
|
+
}
|