@map-audio/pam-mcp-server 1.25.0 → 1.28.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.
Files changed (75) hide show
  1. package/README.md +32 -10
  2. package/dist/audioAnalysis.d.ts +1 -1
  3. package/dist/audioAnalysis.js +3 -4
  4. package/dist/audioAnalysis.js.map +1 -1
  5. package/dist/bridge.d.ts +5 -1
  6. package/dist/bridge.js +48 -48
  7. package/dist/bridge.js.map +1 -1
  8. package/dist/daemon.d.ts +32 -0
  9. package/dist/daemon.js +871 -0
  10. package/dist/daemon.js.map +7 -0
  11. package/dist/drumSynth.d.ts +12 -0
  12. package/dist/drumSynth.js +15 -3
  13. package/dist/drumSynth.js.map +1 -1
  14. package/dist/drumSynthEngines.json +9 -1
  15. package/dist/{fgemma-format.d.ts → functiongemma-format.d.ts} +25 -7
  16. package/dist/{fgemma-format.js → functiongemma-format.js} +34 -11
  17. package/dist/functiongemma-format.js.map +1 -0
  18. package/dist/functiongemma-grammar.d.ts +14 -0
  19. package/dist/functiongemma-grammar.js +140 -0
  20. package/dist/functiongemma-grammar.js.map +1 -0
  21. package/dist/functiongemma-params.d.ts +33 -0
  22. package/dist/functiongemma-params.js +128 -0
  23. package/dist/functiongemma-params.js.map +1 -0
  24. package/dist/{fgemma-runner.d.ts → functiongemma-runner.d.ts} +8 -5
  25. package/dist/{fgemma-runner.js → functiongemma-runner.js} +107 -25
  26. package/dist/functiongemma-runner.js.map +1 -0
  27. package/dist/generated/kvWriteOptions.d.ts +20 -0
  28. package/dist/generated/kvWriteOptions.js +45 -0
  29. package/dist/generated/kvWriteOptions.js.map +1 -0
  30. package/dist/generated/variationSnapshot.js +0 -19
  31. package/dist/generated/variationSnapshot.js.map +1 -1
  32. package/dist/generativePlayerPresets.json +9 -0
  33. package/dist/generativePlayers.json +15 -1
  34. package/dist/launcher.d.ts +2 -0
  35. package/dist/launcher.js +36 -0
  36. package/dist/launcher.js.map +1 -0
  37. package/dist/manifest-mfx.json +12944 -311
  38. package/dist/manifest-msq.json +11446 -0
  39. package/dist/manifest.d.ts +2 -0
  40. package/dist/manifest.js +12 -27
  41. package/dist/manifest.js.map +1 -1
  42. package/dist/manifest.json +37047 -6487
  43. package/dist/mfxCombos.d.ts +2 -0
  44. package/dist/mfxCombos.js +179 -56
  45. package/dist/mfxCombos.js.map +1 -1
  46. package/dist/midiFile.d.ts +20 -6
  47. package/dist/midiFile.js +22 -14
  48. package/dist/midiFile.js.map +1 -1
  49. package/dist/mixAssist.d.ts +35 -0
  50. package/dist/mixAssist.js +250 -21
  51. package/dist/mixAssist.js.map +1 -1
  52. package/dist/patternLanes.d.ts +40 -0
  53. package/dist/patternLanes.js +108 -0
  54. package/dist/patternLanes.js.map +1 -0
  55. package/dist/players.d.ts +22 -0
  56. package/dist/players.js +70 -0
  57. package/dist/players.js.map +1 -1
  58. package/dist/presetSave.d.ts +3 -0
  59. package/dist/presetSave.js +13 -0
  60. package/dist/presetSave.js.map +1 -1
  61. package/dist/productProfile.d.ts +4 -4
  62. package/dist/productProfile.js +10 -1
  63. package/dist/productProfile.js.map +1 -1
  64. package/dist/qwen-format.d.ts +85 -0
  65. package/dist/qwen-format.js +355 -0
  66. package/dist/qwen-format.js.map +1 -0
  67. package/dist/registration.mjs +41 -0
  68. package/dist/server.js +2112 -470
  69. package/dist/server.js.map +1 -1
  70. package/dist/targetDenylist.json +20 -0
  71. package/dist/variationSessionKeys.json +7 -5
  72. package/package.json +13 -7
  73. package/dist/fgemma-format.js.map +0 -1
  74. package/dist/fgemma-runner.js.map +0 -1
  75. package/dist/manifest-cell1.json +0 -12968
package/dist/midiFile.js CHANGED
@@ -1,10 +1,10 @@
1
- // Standard MIDI File (type 1) writer for export_midi — pure Node over the
2
- // pattern state, mirroring PAMPluginProcessor::exportMidi's tiling and channel rule.
1
+ // Standard MIDI File (type 1) writer for export_midi — pure Node over the events PAM renders
2
+ // (notes and CC out), with a drawn-grid tiling fallback for builds that predate that route.
3
3
  export const MIDI_PPQ = 480;
4
4
  /** Must match syncUtils::patternSpeedMultipliers (modules/dsp-core/SyncUtils.h). */
5
5
  export const PATTERN_SPEED_MULTIPLIERS = [
6
6
  1 / 32, 1 / 16, 1 / 12, 1 / 8, 1 / 6, 1 / 5, 1 / 4, 1 / 3, 1 / 2, 1,
7
- 1.33, 1.5, 1.66, 2, 3, 4, 5, 6, 8, 12,
7
+ 4 / 3, 1.5, 5 / 3, 2, 3, 4, 5, 6, 8, 12,
8
8
  ];
9
9
  export const patternSpeedMultiplier = (index) => {
10
10
  const i = Math.round(Number(index));
@@ -61,27 +61,34 @@ function chunk(type, body) {
61
61
  header.writeUInt32BE(body.length, 4);
62
62
  return Buffer.concat([header, Buffer.from(body)]);
63
63
  }
64
- function metaTrack(bpm, name) {
64
+ /** Quarter-note beats in one bar of `meter`. */
65
+ export const beatsPerBar = ({ numerator, denominator }) => (numerator * 4) / denominator;
66
+ function metaTrack(bpm, name, meter) {
65
67
  const usPerQuarter = Math.round(60_000_000 / Math.max(1, bpm));
66
68
  const nameBytes = [...Buffer.from(name, "utf8")];
67
69
  const body = [
68
70
  ...varLen(0), 0xff, 0x03, ...varLen(nameBytes.length), ...nameBytes,
69
71
  ...varLen(0), 0xff, 0x51, 0x03, (usPerQuarter >> 16) & 0xff, (usPerQuarter >> 8) & 0xff, usPerQuarter & 0xff,
70
- ...varLen(0), 0xff, 0x58, 0x04, 4, 2, 24, 8,
72
+ ...varLen(0), 0xff, 0x58, 0x04, meter.numerator, Math.round(Math.log2(meter.denominator)), 24, 8,
71
73
  ...varLen(0), 0xff, 0x2f, 0x00,
72
74
  ];
73
75
  return chunk("MTrk", body);
74
76
  }
75
- function noteTrack(name, events) {
77
+ const channelBits = (channel) => Math.max(0, Math.min(15, Math.round(channel) - 1));
78
+ const dataByte = (value) => Math.max(0, Math.min(127, Math.round(value)));
79
+ function eventTrack({ name, events, cc = [] }, lengthBeats) {
76
80
  const messages = [];
81
+ // At one tick: note-offs (a retrigger is not swallowed), then controllers (a lane lock lands
82
+ // before its note), then note-ons, as PAM's MIDI out orders them.
77
83
  for (const e of events) {
78
- const status = Math.max(0, Math.min(15, e.channel - 1));
84
+ const status = channelBits(e.channel);
79
85
  const on = Math.round(e.startBeats * MIDI_PPQ);
80
86
  const off = Math.max(on + 1, Math.round((e.startBeats + e.durationBeats) * MIDI_PPQ));
81
- // Note-offs sort before note-ons at the same tick so a retrigger is not swallowed.
82
87
  messages.push({ tick: off, order: 0, bytes: [0x80 | status, e.note, 0] });
83
- messages.push({ tick: on, order: 1, bytes: [0x90 | status, e.note, e.velocity] });
88
+ messages.push({ tick: on, order: 2, bytes: [0x90 | status, e.note, e.velocity] });
84
89
  }
90
+ for (const e of cc)
91
+ messages.push({ tick: Math.max(0, Math.round(e.beats * MIDI_PPQ)), order: 1, bytes: [0xb0 | channelBits(e.channel), dataByte(e.controller), dataByte(e.value)] });
85
92
  messages.sort((a, b) => a.tick - b.tick || a.order - b.order);
86
93
  const nameBytes = [...Buffer.from(name, "utf8")];
87
94
  const body = [...varLen(0), 0xff, 0x03, ...varLen(nameBytes.length), ...nameBytes];
@@ -90,19 +97,20 @@ function noteTrack(name, events) {
90
97
  body.push(...varLen(m.tick - last), ...m.bytes);
91
98
  last = m.tick;
92
99
  }
93
- body.push(...varLen(0), 0xff, 0x2f, 0x00);
100
+ // End of Track at the export length, so a re-import loops at it rather than at the last note-off.
101
+ body.push(...varLen(Math.max(0, Math.round(lengthBeats * MIDI_PPQ) - last)), 0xff, 0x2f, 0x00);
94
102
  return chunk("MTrk", body);
95
103
  }
96
- /** A type-1 SMF: one tempo track plus one named track per entry. */
97
- export function buildMidiFile(bpm, tracks, title = "PAM") {
104
+ /** A type-1 SMF: one tempo and meter track plus one named track per entry. */
105
+ export function buildMidiFile(bpm, tracks, title = "PAM", meter = { numerator: 4, denominator: 4 }, lengthBeats = 0) {
98
106
  const header = Buffer.alloc(6);
99
107
  header.writeUInt16BE(1, 0);
100
108
  header.writeUInt16BE(tracks.length + 1, 2);
101
109
  header.writeUInt16BE(MIDI_PPQ, 4);
102
110
  return Buffer.concat([
103
111
  chunk("MThd", [...header]),
104
- metaTrack(bpm, title),
105
- ...tracks.map((track) => noteTrack(track.name, track.events)),
112
+ metaTrack(bpm, title, meter),
113
+ ...tracks.map((track) => eventTrack(track, lengthBeats)),
106
114
  ]);
107
115
  }
108
116
  //# sourceMappingURL=midiFile.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"midiFile.js","sourceRoot":"","sources":["../src/midiFile.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"midiFile.js","sourceRoot":"","sources":["../src/midiFile.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,7 @@ export interface CellStemAnalysis {
14
14
  onsetDensity: number;
15
15
  seconds: number;
16
16
  barkDb: number[];
17
+ coactivity?: Record<number, number>;
17
18
  }
18
19
  export interface MixAnalysis {
19
20
  epoch: number;
@@ -35,6 +36,8 @@ export interface CellContext {
35
36
  eqMidGain: number;
36
37
  eqHighGain: number;
37
38
  eqEnabled: boolean;
39
+ eqLowMidFreq?: number;
40
+ eqMidHighFreq?: number;
38
41
  compressorEnabled: boolean;
39
42
  compressorThreshold: number;
40
43
  compressorRatioAmount: number;
@@ -106,6 +109,7 @@ export declare const MAX_EQ_CUT_DB = 3;
106
109
  export declare const MAX_EQ_LOW_CUT_DB = 1.5;
107
110
  export declare const MAX_EQ_CUTS = 2;
108
111
  export declare const EQ_EPSILON_DB = 0.3;
112
+ export declare const MIN_EQ_CUT_SCORE = 0.2;
109
113
  export declare const CREST_ENGAGE_DB = 14;
110
114
  export declare const COMP_RATIO_MIN = 1.5;
111
115
  export declare const COMP_RATIO_MAX = 4;
@@ -113,16 +117,37 @@ export declare const COMP_EPSILON = 0.05;
113
117
  export declare const PERCUSSIVITY_FULL_ONSET_DENSITY = 8;
114
118
  export declare const MIX_PARAM_DEFAULTS: Record<string, number>;
115
119
  export declare function mixParamDefault(key: string): number;
120
+ export type RbjBiquadType = 'peaking' | 'lowshelf' | 'highshelf' | 'highpass' | 'lowpass' | 'notch';
121
+ export declare const gainToDb: (gain: number) => number;
122
+ /** |H(e^jw)| for one normalized RBJ biquad section. */
123
+ export declare function rbjBiquadMagnitude(frequencyHz: number, sampleRate: number, type: RbjBiquadType, centerHz: number, gainDb: number, q: number): number;
124
+ export declare const EQ_DEFAULT_LOW_MID_HZ = 880;
125
+ export declare const EQ_DEFAULT_MID_HZ = 2100;
126
+ export declare const EQ_DEFAULT_MID_HIGH_HZ = 5000;
127
+ export declare const EQ_LOW_MID_MIN_HZ = 80;
128
+ export declare const EQ_LOW_MID_MAX_HZ = 2000;
129
+ export declare const EQ_MID_HIGH_MIN_HZ = 2000;
130
+ export declare const EQ_MID_HIGH_MAX_HZ = 12000;
131
+ export declare const EQ_SAMPLE_RATE = 48000;
132
+ export declare const EQ_SHELF_Q = 0.5;
133
+ export declare const EQ_MID_Q = 0.7071;
134
+ export declare function eqMidFrequencyHz(lowMidHz: number, midHighHz: number): number;
135
+ /** Exact magnitude response of the native three cascaded Biquads. */
136
+ export declare function eqResponseDb(hz: number, lowGainDb: number, midGainDb: number, highGainDb: number, lowMidHz?: number, midHighHz?: number, sampleRate?: number): number;
116
137
  export declare const BARK_BAND_COUNT = 25;
117
138
  export declare const BARK_BAND_EDGES_HZ: readonly number[];
118
139
  export declare function bandForFrequency(hz: number): number;
119
140
  export declare function barkBandCenterHz(band: number): number;
120
141
  export declare const dbToPower: (db: number) => number;
142
+ export declare const BARK_K_WEIGHT: readonly number[];
143
+ export declare function weightedBandPower(barkDb: readonly number[], band: number): number;
121
144
  export declare function barkCentroidHz(barkDb: readonly number[]): number;
122
145
  export declare function lowFraction(barkDb: readonly number[], belowHz?: number): number;
123
146
  export declare function maskingOverlap(aDb: readonly number[], bDb: readonly number[]): number;
124
147
  export declare function spectralShapeOverlap(aDb: readonly number[], bDb: readonly number[]): number;
125
148
  export declare function maskingMatrix(barkDbByCell: readonly (readonly number[])[]): number[][];
149
+ export declare function coactivity(cell: CellStemAnalysis, otherCellId: number): number;
150
+ export declare function effectiveMaskingMatrix(cells: readonly CellStemAnalysis[]): number[][];
126
151
  export type OverlapRegion = 'low' | 'mid' | 'high';
127
152
  export declare function dominantOverlapRegion(aDb: readonly number[], bDb: readonly number[]): OverlapRegion;
128
153
  export declare function parseMixAnalysisPayload(raw: unknown): MixAnalysis | null;
@@ -158,4 +183,14 @@ export declare function classifyCellRole(a: CellStemAnalysis): CellRole;
158
183
  export declare function normalizeMovesToBus(deltasDb: number[], lufs: number[]): number[];
159
184
  export declare function normalizeMovesToBusBounded(rawDeltasDb: number[], lufs: number[], currentVolumes: number[]): number[];
160
185
  export declare function maskerWeight(pLufs: number, qLufs: number): number;
186
+ type EqKnob = 'eqLowGain' | 'eqMidGain' | 'eqHighGain';
187
+ export interface EqCutPlan {
188
+ key: EqKnob;
189
+ region: OverlapRegion;
190
+ label: string;
191
+ centerHz: number;
192
+ score: number;
193
+ }
194
+ export declare function planEqCut(maskedDb: readonly number[], maskerDb: readonly number[], lowMidHz?: number, midHighHz?: number): EqCutPlan | null;
161
195
  export declare function suggestMix(m: MixMeasurement, options: MixAssistOptions, current?: CurrentFlatState): MixSuggestion;
196
+ export {};
package/dist/mixAssist.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // GENERATED FILE — do not edit by hand.
2
2
  // Vendored from shared/mixAssist/ (server rootDir cannot import outside src/;
3
- // the fgemma-format.ts precedent). Regenerate with:
3
+ // the functiongemma-format.ts precedent). Regenerate with:
4
4
  // node scripts/sync-mixassist.mjs
5
5
  // Parity is pinned by interface/test/mixAssistServerParity.test.ts.
6
6
  /* eslint-disable */
@@ -85,6 +85,8 @@ export const MAX_EQ_CUT_DB = 3; // Hafezi bound
85
85
  export const MAX_EQ_LOW_CUT_DB = 1.5; // preserve low-end weight on non-bass maskers
86
86
  export const MAX_EQ_CUTS = 2;
87
87
  export const EQ_EPSILON_DB = 0.3;
88
+ // planEqCut score below which no EQ3 knob reaches the conflict usefully.
89
+ export const MIN_EQ_CUT_SCORE = 0.2;
88
90
  // --- dynamics (comp) stage ---
89
91
  export const CREST_ENGAGE_DB = 14; // plr above this makes a cell comp-eligible
90
92
  export const COMP_RATIO_MIN = 1.5;
@@ -111,6 +113,120 @@ export function mixParamDefault(key) {
111
113
  const suffix = key.replace(/^cell\d+_/, '');
112
114
  return MIX_PARAM_DEFAULTS[suffix] ?? 0;
113
115
  }
116
+ export const gainToDb = (gain) => 20 * Math.log10(Math.max(gain, 1e-9));
117
+ /** |H(e^jw)| for one normalized RBJ biquad section. */
118
+ export function rbjBiquadMagnitude(frequencyHz, sampleRate, type, centerHz, gainDb, q) {
119
+ const w = (2 * Math.PI * Math.min(Math.max(frequencyHz, 1e-6), sampleRate * 0.4999)) / sampleRate;
120
+ const w0 = (2 * Math.PI * Math.min(Math.max(centerHz, 1e-3), sampleRate * 0.4999)) / sampleRate;
121
+ const cos0 = Math.cos(w0);
122
+ const sin0 = Math.sin(w0);
123
+ const A = Math.pow(10, (gainDb || 0) / 40);
124
+ const alpha0 = sin0 / (2 * Math.max(1e-6, q));
125
+ let b0 = 1;
126
+ let b1 = 0;
127
+ let b2 = 0;
128
+ let a0 = 1;
129
+ let a1 = 0;
130
+ let a2 = 0;
131
+ switch (type) {
132
+ case 'peaking':
133
+ b0 = 1 + alpha0 * A;
134
+ b1 = -2 * cos0;
135
+ b2 = 1 - alpha0 * A;
136
+ a0 = 1 + alpha0 / A;
137
+ a1 = -2 * cos0;
138
+ a2 = 1 - alpha0 / A;
139
+ break;
140
+ case 'lowshelf': {
141
+ const sqrtA = Math.sqrt(A);
142
+ const two = 2 * sqrtA * alpha0;
143
+ b0 = A * (A + 1 - (A - 1) * cos0 + two);
144
+ b1 = 2 * A * (A - 1 - (A + 1) * cos0);
145
+ b2 = A * (A + 1 - (A - 1) * cos0 - two);
146
+ a0 = A + 1 + (A - 1) * cos0 + two;
147
+ a1 = -2 * (A - 1 + (A + 1) * cos0);
148
+ a2 = A + 1 + (A - 1) * cos0 - two;
149
+ break;
150
+ }
151
+ case 'highshelf': {
152
+ const sqrtA = Math.sqrt(A);
153
+ const two = 2 * sqrtA * alpha0;
154
+ b0 = A * (A + 1 + (A - 1) * cos0 + two);
155
+ b1 = -2 * A * (A - 1 + (A + 1) * cos0);
156
+ b2 = A * (A + 1 + (A - 1) * cos0 - two);
157
+ a0 = A + 1 - (A - 1) * cos0 + two;
158
+ a1 = 2 * (A - 1 - (A + 1) * cos0);
159
+ a2 = A + 1 - (A - 1) * cos0 - two;
160
+ break;
161
+ }
162
+ case 'lowpass':
163
+ b0 = (1 - cos0) / 2;
164
+ b1 = 1 - cos0;
165
+ b2 = (1 - cos0) / 2;
166
+ a0 = 1 + alpha0;
167
+ a1 = -2 * cos0;
168
+ a2 = 1 - alpha0;
169
+ break;
170
+ case 'highpass':
171
+ b0 = (1 + cos0) / 2;
172
+ b1 = -(1 + cos0);
173
+ b2 = (1 + cos0) / 2;
174
+ a0 = 1 + alpha0;
175
+ a1 = -2 * cos0;
176
+ a2 = 1 - alpha0;
177
+ break;
178
+ case 'notch':
179
+ b0 = 1;
180
+ b1 = -2 * cos0;
181
+ b2 = 1;
182
+ a0 = 1 + alpha0;
183
+ a1 = -2 * cos0;
184
+ a2 = 1 - alpha0;
185
+ break;
186
+ }
187
+ const bz0 = b0 / a0;
188
+ const bz1 = b1 / a0;
189
+ const bz2 = b2 / a0;
190
+ const az1 = a1 / a0;
191
+ const az2 = a2 / a0;
192
+ const cosw = Math.cos(w);
193
+ const sinw = Math.sin(w);
194
+ const cos2w = Math.cos(2 * w);
195
+ const sin2w = Math.sin(2 * w);
196
+ const numerator = (bz0 + bz1 * cosw + bz2 * cos2w) ** 2 + (bz1 * sinw + bz2 * sin2w) ** 2;
197
+ const denominator = (1 + az1 * cosw + az2 * cos2w) ** 2 + (az1 * sinw + az2 * sin2w) ** 2;
198
+ return Math.sqrt(numerator / Math.max(denominator, 1e-24));
199
+ }
200
+ // ---------------- EQ3 (the per-cell mixer EQ) ----------------
201
+ // Exact twins of modules/eq/EQ.h. The old engine was fixed at these frequencies;
202
+ // they remain the parameter defaults so old presets keep their sound.
203
+ export const EQ_DEFAULT_LOW_MID_HZ = 880;
204
+ export const EQ_DEFAULT_MID_HZ = 2100;
205
+ export const EQ_DEFAULT_MID_HIGH_HZ = 5000;
206
+ export const EQ_LOW_MID_MIN_HZ = 80;
207
+ export const EQ_LOW_MID_MAX_HZ = 2000;
208
+ export const EQ_MID_HIGH_MIN_HZ = 2000;
209
+ export const EQ_MID_HIGH_MAX_HZ = 12000;
210
+ export const EQ_SAMPLE_RATE = 48000;
211
+ export const EQ_SHELF_Q = 0.5;
212
+ export const EQ_MID_Q = 0.7071;
213
+ const clampHz = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
214
+ export function eqMidFrequencyHz(lowMidHz, midHighHz) {
215
+ const low = clampHz(lowMidHz, EQ_LOW_MID_MIN_HZ, EQ_LOW_MID_MAX_HZ);
216
+ const high = clampHz(midHighHz, EQ_MID_HIGH_MIN_HZ, EQ_MID_HIGH_MAX_HZ);
217
+ if (low === EQ_DEFAULT_LOW_MID_HZ && high === EQ_DEFAULT_MID_HIGH_HZ)
218
+ return EQ_DEFAULT_MID_HZ;
219
+ return EQ_DEFAULT_MID_HZ * Math.sqrt((low / EQ_DEFAULT_LOW_MID_HZ) * (high / EQ_DEFAULT_MID_HIGH_HZ));
220
+ }
221
+ /** Exact magnitude response of the native three cascaded Biquads. */
222
+ export function eqResponseDb(hz, lowGainDb, midGainDb, highGainDb, lowMidHz = EQ_DEFAULT_LOW_MID_HZ, midHighHz = EQ_DEFAULT_MID_HIGH_HZ, sampleRate = EQ_SAMPLE_RATE) {
223
+ const low = clampHz(lowMidHz, EQ_LOW_MID_MIN_HZ, EQ_LOW_MID_MAX_HZ);
224
+ const high = clampHz(midHighHz, EQ_MID_HIGH_MIN_HZ, EQ_MID_HIGH_MAX_HZ);
225
+ const magnitude = rbjBiquadMagnitude(hz, sampleRate, 'lowshelf', low, lowGainDb, EQ_SHELF_Q) *
226
+ rbjBiquadMagnitude(hz, sampleRate, 'peaking', eqMidFrequencyHz(low, high), midGainDb, EQ_MID_Q) *
227
+ rbjBiquadMagnitude(hz, sampleRate, 'highshelf', high, highGainDb, EQ_SHELF_Q);
228
+ return gainToDb(magnitude);
229
+ }
114
230
  // ===== shared/mixAssist/bark.ts =====
115
231
  // Bark band model — the TS mirror of modules/analysis/BarkScale.h. The 26
116
232
  // edge literals MUST match the C++ table verbatim; parity is pinned by
@@ -137,6 +253,33 @@ export function barkBandCenterHz(band) {
137
253
  return Math.sqrt(BARK_BAND_EDGES_HZ[b] * BARK_BAND_EDGES_HZ[b + 1]);
138
254
  }
139
255
  export const dbToPower = (db) => Math.pow(10, db / 10);
256
+ // BS.1770 K-weighting power gain per Bark band (48 kHz pre-filter + RLB
257
+ // high-pass), averaged over the band in log frequency. Masking decisions weigh
258
+ // band energy in the same domain the LUFS readings use, so raw sub-bass power
259
+ // cannot outvote what the ear hears in the mids.
260
+ function kWeightPowerAt(hz) {
261
+ const w = (2 * Math.PI * hz) / 48000;
262
+ const section = (b0, b1, b2, a1, a2) => {
263
+ const num = (b0 + b1 * Math.cos(w) + b2 * Math.cos(2 * w)) ** 2 + (b1 * Math.sin(w) + b2 * Math.sin(2 * w)) ** 2;
264
+ const den = (1 + a1 * Math.cos(w) + a2 * Math.cos(2 * w)) ** 2 + (a1 * Math.sin(w) + a2 * Math.sin(2 * w)) ** 2;
265
+ return num / den;
266
+ };
267
+ return (section(1.53512485958697, -2.69169618940638, 1.19839281085285, -1.69065929318241, 0.73248077421585) *
268
+ section(1, -2, 1, -1.99004745483398, 0.99007225036621));
269
+ }
270
+ export const BARK_K_WEIGHT = Array.from({ length: BARK_BAND_COUNT }, (_, b) => {
271
+ const lo = Math.log(BARK_BAND_EDGES_HZ[b]);
272
+ const hi = Math.log(BARK_BAND_EDGES_HZ[b + 1]);
273
+ const points = 16;
274
+ let sum = 0;
275
+ for (let i = 0; i < points; i++)
276
+ sum += kWeightPowerAt(Math.exp(lo + ((i + 0.5) / points) * (hi - lo)));
277
+ return sum / points;
278
+ });
279
+ // Band power as the loudness meter hears it (K-weighted), linear.
280
+ export function weightedBandPower(barkDb, band) {
281
+ return dbToPower(barkDb[band]) * BARK_K_WEIGHT[band];
282
+ }
140
283
  // Power-weighted spectral centroid over band centers, Hz. 0 when silent.
141
284
  export function barkCentroidHz(barkDb) {
142
285
  let power = 0;
@@ -148,7 +291,8 @@ export function barkCentroidHz(barkDb) {
148
291
  }
149
292
  return power > 0 ? weighted / power : 0;
150
293
  }
151
- // Fraction of LINEAR power in bands wholly below `belowHz`. 0 when silent.
294
+ // Fraction of LINEAR power in bands wholly below `belowHz`. 0 only at zero power: the analyzer's
295
+ // -120 dB silence floor (`kBarkFloorDb`) reads 2/25.
152
296
  export function lowFraction(barkDb, belowHz = 200) {
153
297
  let total = 0;
154
298
  let low = 0;
@@ -164,7 +308,7 @@ export function lowFraction(barkDb, belowHz = 200) {
164
308
  // Cross-cell Bark-band masking overlap (AutoMix M1). Deliberately simple:
165
309
  // textbook objective masking models track perceived masking poorly (Hafezi &
166
310
  // Reiss, JAES 2015) — a bounded band-overlap fraction works better as a rule
167
- // input than a psychoacoustic model.
311
+ // input than a psychoacoustic model. Band energy is K-weighted (bark.ts).
168
312
  // Fraction of A's energy that B overlaps: sum_b min(Pa, Pb) / sum_b Pa.
169
313
  // Asymmetric (a quiet cell under a loud one reads ~1; the loud one reads
170
314
  // less). 0 when A is silent.
@@ -173,8 +317,8 @@ export function maskingOverlap(aDb, bDb) {
173
317
  let shared = 0;
174
318
  const n = Math.min(aDb.length, bDb.length, BARK_BAND_COUNT);
175
319
  for (let b = 0; b < n; b++) {
176
- const pa = dbToPower(aDb[b]);
177
- const pb = dbToPower(bDb[b]);
320
+ const pa = weightedBandPower(aDb, b);
321
+ const pb = weightedBandPower(bDb, b);
178
322
  sumA += pa;
179
323
  shared += Math.min(pa, pb);
180
324
  }
@@ -197,14 +341,14 @@ export function spectralShapeOverlap(aDb, bDb) {
197
341
  let sumA = 0;
198
342
  let sumB = 0;
199
343
  for (let b = 0; b < n; b++) {
200
- sumA += dbToPower(aDb[b]);
201
- sumB += dbToPower(bDb[b]);
344
+ sumA += weightedBandPower(aDb, b);
345
+ sumB += weightedBandPower(bDb, b);
202
346
  }
203
347
  if (sumA <= 0 || sumB <= 0)
204
348
  return 0;
205
349
  let shared = 0;
206
350
  for (let b = 0; b < n; b++) {
207
- shared += Math.min(dbToPower(aDb[b]) / sumA, dbToPower(bDb[b]) / sumB);
351
+ shared += Math.min(weightedBandPower(aDb, b) / sumA, weightedBandPower(bDb, b) / sumB);
208
352
  }
209
353
  return shared;
210
354
  }
@@ -221,16 +365,34 @@ export function maskingMatrix(barkDbByCell) {
221
365
  }
222
366
  return m;
223
367
  }
368
+ // Fraction of `cell`'s sounding frames in which `otherCellId` also sounds.
369
+ // Payloads without the map read 1, the old always-simultaneous assumption.
370
+ export function coactivity(cell, otherCellId) {
371
+ const v = cell.coactivity?.[otherCellId];
372
+ return v === undefined ? 1 : Math.min(1, Math.max(0, v));
373
+ }
374
+ // maskingMatrix gated by co-activity: two parts that never sound together do
375
+ // not mask each other, however alike their long-term spectra are.
376
+ export function effectiveMaskingMatrix(cells) {
377
+ const m = maskingMatrix(cells.map((c) => c.barkDb));
378
+ for (let i = 0; i < cells.length; i++) {
379
+ for (let j = 0; j < cells.length; j++) {
380
+ if (i !== j)
381
+ m[i][j] *= coactivity(cells[i], cells[j].cellId);
382
+ }
383
+ }
384
+ return m;
385
+ }
224
386
  // Region carrying the largest shared power between two stems, split at
225
- // 300 Hz / 5 kHz — the mixer 3-band EQ's split, so a cut lands on the right
226
- // knob. Band membership by log-center.
387
+ // 300 Hz / 5 kHz by log-center. PAN reads it to keep bass-range conflicts
388
+ // centred; EQ cuts pick their knob from the real EQ3 curve instead.
227
389
  export function dominantOverlapRegion(aDb, bDb) {
228
390
  const shared = { low: 0, mid: 0, high: 0 };
229
391
  const n = Math.min(aDb.length, bDb.length, BARK_BAND_COUNT);
230
392
  for (let b = 0; b < n; b++) {
231
393
  const center = barkBandCenterHz(b);
232
394
  const region = center < 300 ? 'low' : center >= 5000 ? 'high' : 'mid';
233
- shared[region] += Math.min(dbToPower(aDb[b]), dbToPower(bDb[b]));
395
+ shared[region] += Math.min(weightedBandPower(aDb, b), weightedBandPower(bDb, b));
234
396
  }
235
397
  if (shared.low >= shared.mid && shared.low >= shared.high)
236
398
  return 'low';
@@ -240,6 +402,18 @@ export function dominantOverlapRegion(aDb, bDb) {
240
402
  }
241
403
  // ===== shared/mixAssist/parse.ts =====
242
404
  const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
405
+ function parseCoactivity(raw) {
406
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
407
+ return undefined;
408
+ const out = {};
409
+ for (const [key, value] of Object.entries(raw)) {
410
+ const id = Number(key);
411
+ const v = num(value);
412
+ if (Number.isInteger(id) && v !== undefined)
413
+ out[id] = Math.min(1, Math.max(0, v));
414
+ }
415
+ return out;
416
+ }
243
417
  function parseStem(raw) {
244
418
  // Number.isFinite per band: one NaN/Infinity entry would silently poison the
245
419
  // masking matrix (dbToPower propagates it through every overlap sum).
@@ -263,6 +437,7 @@ function parseStem(raw) {
263
437
  onsetDensity: num(raw.onsetDensity) ?? 0,
264
438
  seconds: num(raw.secs) ?? 0,
265
439
  barkDb: bark,
440
+ coactivity: parseCoactivity(raw.coact),
266
441
  };
267
442
  }
268
443
  // Maps the native analyzeMixStems payload (short keys, sentinel fields
@@ -353,6 +528,8 @@ export function buildMixMeasurementFromState(analysis, flatState, mods = {}, opt
353
528
  eqMidGain: numOr(k('eqMidGain'), 0),
354
529
  eqHighGain: numOr(k('eqHighGain'), 0),
355
530
  eqEnabled: numOr(k('eqEnabled'), 0) > 0.5,
531
+ eqLowMidFreq: numOr(k('eqLowMidFreq'), EQ_DEFAULT_LOW_MID_HZ),
532
+ eqMidHighFreq: numOr(k('eqMidHighFreq'), EQ_DEFAULT_MID_HIGH_HZ),
356
533
  compressorEnabled: numOr(k('compressorEnabled'), 0) > 0.5,
357
534
  compressorThreshold: numOr(k('compressorThreshold'), 0),
358
535
  compressorRatioAmount: numOr(k('compressorRatioAmount'), 2),
@@ -474,6 +651,53 @@ export function normalizeMovesToBusBounded(rawDeltasDb, lufs, currentVolumes) {
474
651
  export function maskerWeight(pLufs, qLufs) {
475
652
  return 1 / (1 + Math.pow(10, (pLufs - qLufs) / 10));
476
653
  }
654
+ const EQ_KNOBS = [
655
+ { key: 'eqLowGain', region: 'low', label: 'low shelf' },
656
+ { key: 'eqMidGain', region: 'mid', label: 'mid band' },
657
+ { key: 'eqHighGain', region: 'high', label: 'high shelf' },
658
+ ];
659
+ // Picks the masker's EQ3 knob from its REAL curve at this cell's crossovers.
660
+ // coverage = how hard a probe cut hits the shared (conflict) energy, collateral =
661
+ // how hard it hits the masker overall; score = coverage * min(1, coverage/collateral).
662
+ export function planEqCut(maskedDb, maskerDb, lowMidHz = EQ_DEFAULT_LOW_MID_HZ, midHighHz = EQ_DEFAULT_MID_HIGH_HZ) {
663
+ const n = Math.min(maskedDb.length, maskerDb.length, BARK_BAND_COUNT);
664
+ const shared = [];
665
+ const own = [];
666
+ let sharedSum = 0;
667
+ let ownSum = 0;
668
+ let logCenter = 0;
669
+ for (let b = 0; b < n; b++) {
670
+ const pk = weightedBandPower(maskerDb, b);
671
+ const s = Math.min(weightedBandPower(maskedDb, b), pk);
672
+ shared.push(s);
673
+ own.push(pk);
674
+ sharedSum += s;
675
+ ownSum += pk;
676
+ logCenter += s * Math.log(barkBandCenterHz(b));
677
+ }
678
+ if (sharedSum <= 0 || ownSum <= 0)
679
+ return null;
680
+ const probeDb = -MAX_EQ_CUT_DB;
681
+ const probeRemoval = 1 - Math.pow(10, probeDb / 10);
682
+ let best = null;
683
+ for (const knob of EQ_KNOBS) {
684
+ let hitShared = 0;
685
+ let hitOwn = 0;
686
+ for (let b = 0; b < n; b++) {
687
+ const responseDb = eqResponseDb(barkBandCenterHz(b), knob.key === 'eqLowGain' ? probeDb : 0, knob.key === 'eqMidGain' ? probeDb : 0, knob.key === 'eqHighGain' ? probeDb : 0, lowMidHz, midHighHz);
688
+ const removal = 1 - Math.pow(10, responseDb / 10);
689
+ hitShared += shared[b] * removal;
690
+ hitOwn += own[b] * removal;
691
+ }
692
+ const coverage = hitShared / (sharedSum * probeRemoval);
693
+ const collateral = hitOwn / (ownSum * probeRemoval);
694
+ const score = coverage * Math.min(1, coverage / Math.max(collateral, 1e-9));
695
+ if (!best || score > best.score) {
696
+ best = { key: knob.key, region: knob.region, label: knob.label, centerHz: Math.exp(logCenter / sharedSum), score };
697
+ }
698
+ }
699
+ return best;
700
+ }
477
701
  export function suggestMix(m, options, current = {}) {
478
702
  const moves = [];
479
703
  const guardsHit = [];
@@ -524,14 +748,15 @@ export function suggestMix(m, options, current = {}) {
524
748
  // feeding it into effectiveLufs made the quietest cell take the deepest
525
749
  // penalty and the largest compensating lift. spectralShapeOverlap answers
526
750
  // only "do they collide", and maskerWeight scales that by the level gap.
527
- const matrix = maskingMatrix(participants.map((p) => p.cell.analysis.barkDb));
751
+ const matrix = effectiveMaskingMatrix(participants.map((p) => p.cell.analysis));
528
752
  for (const p of participants) {
529
753
  let masked = 0;
530
754
  for (const q of participants) {
531
755
  if (q.index === p.index)
532
756
  continue;
533
757
  const shape = spectralShapeOverlap(p.cell.analysis.barkDb, q.cell.analysis.barkDb);
534
- masked = Math.max(masked, shape * maskerWeight(p.lufs, q.lufs));
758
+ const together = coactivity(p.cell.analysis, q.cell.context.cellId);
759
+ masked = Math.max(masked, shape * maskerWeight(p.lufs, q.lufs) * together);
535
760
  }
536
761
  p.maskedFraction = masked;
537
762
  p.effectiveLufs = p.lufs - MASK_PENALTY_DB_PER_UNIT * masked;
@@ -646,23 +871,24 @@ export function suggestMix(m, options, current = {}) {
646
871
  guardsHit.push(`cell ${id} mixer EQ is modulated — no masking cut`);
647
872
  continue;
648
873
  }
649
- const region = dominantOverlapRegion(c.masked.cell.analysis.barkDb, c.masker.cell.analysis.barkDb);
874
+ const plan = planEqCut(c.masked.cell.analysis.barkDb, c.masker.cell.analysis.barkDb, maskerCtx.eqLowMidFreq, maskerCtx.eqMidHighFreq);
875
+ if (!plan || plan.score < MIN_EQ_CUT_SCORE) {
876
+ guardsHit.push(`cell ${id} EQ cannot reach the conflict with cell ${c.masked.cell.context.cellId}`);
877
+ continue;
878
+ }
650
879
  // Never carve the fundamental of a low-dominant source. Kick and bass
651
880
  // conflicts belong to balance or sidechain, not a broad static low cut.
652
- // Non-low-dominant sources can still be trimmed gently when their low
653
- // spill masks another cell.
654
- if (region === 'low' && c.masker.lowDominant) {
881
+ if (plan.region === 'low' && c.masker.lowDominant) {
655
882
  guardsHit.push(`cell ${id} is low-dominant — its low band is left alone`);
656
883
  continue;
657
884
  }
658
- const band = region === 'low' ? 'eqLowGain' : region === 'high' ? 'eqHighGain' : 'eqMidGain';
659
- const key = `cell${id}_${band}`;
660
- const maxCutDb = region === 'low' ? MAX_EQ_LOW_CUT_DB : MAX_EQ_CUT_DB;
885
+ const key = `cell${id}_${plan.key}`;
886
+ const maxCutDb = plan.region === 'low' ? MAX_EQ_LOW_CUT_DB : MAX_EQ_CUT_DB;
661
887
  const cutDb = Math.min(maxCutDb, 6 * c.overlap - 2);
662
888
  if (cutDb <= EQ_EPSILON_DB)
663
889
  continue;
664
890
  const value = round1(cur(key) - cutDb);
665
- propose(id, key, value, `cell ${id} covers cell ${c.masked.cell.context.cellId} in the ${region} band — cut ${round1(cutDb)} dB`, EQ_EPSILON_DB);
891
+ propose(id, key, value, `cell ${id} covers cell ${c.masked.cell.context.cellId} around ${formatHz(plan.centerHz)} — cut ${round1(cutDb)} dB on the ${plan.label}`, EQ_EPSILON_DB);
666
892
  if (flat[key] !== undefined && !maskerCtx.eqEnabled) {
667
893
  flat[`cell${id}_eqEnabled`] = 1;
668
894
  guardsHit.push(`cell ${id} mixer EQ enabled for the cut`);
@@ -734,4 +960,7 @@ export function suggestMix(m, options, current = {}) {
734
960
  function cellId(p) {
735
961
  return p.cell.context.cellId;
736
962
  }
963
+ function formatHz(hz) {
964
+ return hz >= 1000 ? `${round1(hz / 1000)} kHz` : `${Math.round(hz / 10) * 10} Hz`;
965
+ }
737
966
  //# sourceMappingURL=mixAssist.js.map