fractal-midi 0.1.0-alpha.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 (123) hide show
  1. package/LICENSE +200 -0
  2. package/NOTICE +28 -0
  3. package/README.md +147 -0
  4. package/dist/am4/applicability.d.ts +61 -0
  5. package/dist/am4/applicability.d.ts.map +1 -0
  6. package/dist/am4/applicability.js +285 -0
  7. package/dist/am4/blockTypes.d.ts +43 -0
  8. package/dist/am4/blockTypes.d.ts.map +1 -0
  9. package/dist/am4/blockTypes.js +48 -0
  10. package/dist/am4/cacheEnums.d.ts +46 -0
  11. package/dist/am4/cacheEnums.d.ts.map +1 -0
  12. package/dist/am4/cacheEnums.js +734 -0
  13. package/dist/am4/cacheParams.d.ts +3533 -0
  14. package/dist/am4/cacheParams.d.ts.map +1 -0
  15. package/dist/am4/cacheParams.js +1996 -0
  16. package/dist/am4/editorControlLabels.d.ts +45 -0
  17. package/dist/am4/editorControlLabels.d.ts.map +1 -0
  18. package/dist/am4/editorControlLabels.js +15894 -0
  19. package/dist/am4/index.d.ts +28 -0
  20. package/dist/am4/index.d.ts.map +1 -0
  21. package/dist/am4/index.js +31 -0
  22. package/dist/am4/ir/preset.d.ts +24 -0
  23. package/dist/am4/ir/preset.d.ts.map +1 -0
  24. package/dist/am4/ir/preset.js +12 -0
  25. package/dist/am4/ir/transpile.d.ts +9 -0
  26. package/dist/am4/ir/transpile.d.ts.map +1 -0
  27. package/dist/am4/ir/transpile.js +19 -0
  28. package/dist/am4/locations.d.ts +32 -0
  29. package/dist/am4/locations.d.ts.map +1 -0
  30. package/dist/am4/locations.js +58 -0
  31. package/dist/am4/paramNames.d.ts +55 -0
  32. package/dist/am4/paramNames.d.ts.map +1 -0
  33. package/dist/am4/paramNames.js +863 -0
  34. package/dist/am4/paramNamesGenerated.d.ts +41 -0
  35. package/dist/am4/paramNamesGenerated.d.ts.map +1 -0
  36. package/dist/am4/paramNamesGenerated.js +183 -0
  37. package/dist/am4/parameterBridge.d.ts +46 -0
  38. package/dist/am4/parameterBridge.d.ts.map +1 -0
  39. package/dist/am4/parameterBridge.js +300 -0
  40. package/dist/am4/params.d.ts +9577 -0
  41. package/dist/am4/params.d.ts.map +1 -0
  42. package/dist/am4/params.js +4537 -0
  43. package/dist/am4/setParam.d.ts +414 -0
  44. package/dist/am4/setParam.d.ts.map +1 -0
  45. package/dist/am4/setParam.js +819 -0
  46. package/dist/am4/shared/paramHelpers.d.ts +55 -0
  47. package/dist/am4/shared/paramHelpers.d.ts.map +1 -0
  48. package/dist/am4/shared/paramHelpers.js +146 -0
  49. package/dist/am4/symbolicIds.d.ts +11 -0
  50. package/dist/am4/symbolicIds.d.ts.map +1 -0
  51. package/dist/am4/symbolicIds.js +587 -0
  52. package/dist/am4/typeApplicability.d.ts +39 -0
  53. package/dist/am4/typeApplicability.d.ts.map +1 -0
  54. package/dist/am4/typeApplicability.js +466 -0
  55. package/dist/am4/variantResolverTables.d.ts +51 -0
  56. package/dist/am4/variantResolverTables.d.ts.map +1 -0
  57. package/dist/am4/variantResolverTables.js +3128 -0
  58. package/dist/axe-fx-ii/blockTypes.d.ts +45 -0
  59. package/dist/axe-fx-ii/blockTypes.d.ts.map +1 -0
  60. package/dist/axe-fx-ii/blockTypes.js +116 -0
  61. package/dist/axe-fx-ii/index.d.ts +5 -0
  62. package/dist/axe-fx-ii/index.d.ts.map +1 -0
  63. package/dist/axe-fx-ii/index.js +18 -0
  64. package/dist/axe-fx-ii/paramAliases.d.ts +54 -0
  65. package/dist/axe-fx-ii/paramAliases.d.ts.map +1 -0
  66. package/dist/axe-fx-ii/paramAliases.js +146 -0
  67. package/dist/axe-fx-ii/params.d.ts +11502 -0
  68. package/dist/axe-fx-ii/params.d.ts.map +1 -0
  69. package/dist/axe-fx-ii/params.js +2847 -0
  70. package/dist/axe-fx-ii/setParam.d.ts +560 -0
  71. package/dist/axe-fx-ii/setParam.d.ts.map +1 -0
  72. package/dist/axe-fx-ii/setParam.js +888 -0
  73. package/dist/axe-fx-iii/blockTypes.d.ts +87 -0
  74. package/dist/axe-fx-iii/blockTypes.d.ts.map +1 -0
  75. package/dist/axe-fx-iii/blockTypes.js +156 -0
  76. package/dist/axe-fx-iii/enumOverlay.d.ts +73 -0
  77. package/dist/axe-fx-iii/enumOverlay.d.ts.map +1 -0
  78. package/dist/axe-fx-iii/enumOverlay.js +236 -0
  79. package/dist/axe-fx-iii/index.d.ts +9 -0
  80. package/dist/axe-fx-iii/index.d.ts.map +1 -0
  81. package/dist/axe-fx-iii/index.js +20 -0
  82. package/dist/axe-fx-iii/params.d.ts +179 -0
  83. package/dist/axe-fx-iii/params.d.ts.map +1 -0
  84. package/dist/axe-fx-iii/params.js +6913 -0
  85. package/dist/axe-fx-iii/setParam.d.ts +460 -0
  86. package/dist/axe-fx-iii/setParam.d.ts.map +1 -0
  87. package/dist/axe-fx-iii/setParam.js +910 -0
  88. package/dist/index.d.ts +2 -0
  89. package/dist/index.d.ts.map +1 -0
  90. package/dist/index.js +12 -0
  91. package/dist/shared/checksum.d.ts +10 -0
  92. package/dist/shared/checksum.d.ts.map +1 -0
  93. package/dist/shared/checksum.js +14 -0
  94. package/dist/shared/device.d.ts +195 -0
  95. package/dist/shared/device.d.ts.map +1 -0
  96. package/dist/shared/device.js +27 -0
  97. package/dist/shared/index.d.ts +8 -0
  98. package/dist/shared/index.d.ts.map +1 -0
  99. package/dist/shared/index.js +11 -0
  100. package/dist/shared/lineage/amp-lineage.json +8313 -0
  101. package/dist/shared/lineage/axefx2-amp-lineage.json +5871 -0
  102. package/dist/shared/lineage/axefx2-delay-lineage.json +226 -0
  103. package/dist/shared/lineage/axefx2-drive-lineage.json +575 -0
  104. package/dist/shared/lineage/axefx2-reverb-lineage.json +467 -0
  105. package/dist/shared/lineage/cab-lineage.json +10777 -0
  106. package/dist/shared/lineage/chorus-lineage.json +173 -0
  107. package/dist/shared/lineage/compressor-lineage.json +338 -0
  108. package/dist/shared/lineage/delay-lineage.json +313 -0
  109. package/dist/shared/lineage/drive-lineage.json +1844 -0
  110. package/dist/shared/lineage/flanger-lineage.json +313 -0
  111. package/dist/shared/lineage/phaser-lineage.json +208 -0
  112. package/dist/shared/lineage/reverb-lineage.json +793 -0
  113. package/dist/shared/lineage/wah-lineage.json +117 -0
  114. package/dist/shared/lineageLookup.d.ts +69 -0
  115. package/dist/shared/lineageLookup.d.ts.map +1 -0
  116. package/dist/shared/lineageLookup.js +196 -0
  117. package/dist/shared/packValue.d.ts +40 -0
  118. package/dist/shared/packValue.d.ts.map +1 -0
  119. package/dist/shared/packValue.js +105 -0
  120. package/dist/shared/types.d.ts +23 -0
  121. package/dist/shared/types.d.ts.map +1 -0
  122. package/dist/shared/types.js +9 -0
  123. package/package.json +75 -0
@@ -0,0 +1,117 @@
1
+ {
2
+ "_source": "Fractal Audio wiki — Wah_block.md",
3
+ "_extractedAt": "2026-04-26T02:14:32.329Z",
4
+ "_catalogSize": 9,
5
+ "_recordCount": 9,
6
+ "records": [
7
+ {
8
+ "am4Name": "FAS Wah",
9
+ "wikiName": "FAS Wah",
10
+ "description": "Fractal Audio's custom model.",
11
+ "descriptionSource": "fractal-wiki",
12
+ "fractalQuotes": [],
13
+ "flags": []
14
+ },
15
+ {
16
+ "am4Name": "Clyde",
17
+ "wikiName": "Clyde",
18
+ "description": "Based on original Vox Clyde McCoy wah pedal.",
19
+ "descriptionSource": "fractal-wiki",
20
+ "fractalQuotes": [],
21
+ "flags": [],
22
+ "basedOn": {
23
+ "primary": "Vox Clyde McCoy",
24
+ "manufacturer": "Vox",
25
+ "productName": "Clyde McCoy",
26
+ "source": "fractal-wiki"
27
+ }
28
+ },
29
+ {
30
+ "am4Name": "Cry Babe",
31
+ "wikiName": "Cry Babe",
32
+ "description": "Based on Dunlop Cry Baby.",
33
+ "descriptionSource": "fractal-wiki",
34
+ "fractalQuotes": [],
35
+ "flags": [],
36
+ "basedOn": {
37
+ "primary": "Dunlop Cry Baby",
38
+ "manufacturer": "Dunlop",
39
+ "productName": "Cry Baby",
40
+ "source": "fractal-wiki"
41
+ }
42
+ },
43
+ {
44
+ "am4Name": "VX846",
45
+ "wikiName": "VX846",
46
+ "description": "Based on Vox V846-HW handwired wah pedal (Stevie Ray Vaughan).",
47
+ "descriptionSource": "fractal-wiki",
48
+ "fractalQuotes": [],
49
+ "flags": [],
50
+ "basedOn": {
51
+ "primary": "Vox V846 -HW",
52
+ "manufacturer": "Vox",
53
+ "model": "V846",
54
+ "productName": "-HW",
55
+ "source": "fractal-wiki"
56
+ }
57
+ },
58
+ {
59
+ "am4Name": "Color-Tone",
60
+ "wikiName": "Color-Tone",
61
+ "description": "Based on Sola Colorsound wah pedal.",
62
+ "descriptionSource": "fractal-wiki",
63
+ "fractalQuotes": [],
64
+ "flags": [],
65
+ "basedOn": {
66
+ "primary": "Colorsound Sola",
67
+ "manufacturer": "Colorsound",
68
+ "productName": "Sola",
69
+ "source": "fractal-wiki"
70
+ }
71
+ },
72
+ {
73
+ "am4Name": "Funk Wah",
74
+ "wikiName": "Funk Wah",
75
+ "description": "Wah sound from the “Shaft” movie.",
76
+ "descriptionSource": "fractal-wiki",
77
+ "fractalQuotes": [],
78
+ "flags": []
79
+ },
80
+ {
81
+ "am4Name": "Mortal",
82
+ "wikiName": "Mortal",
83
+ "description": "Based on Morley wah/volume pedal.",
84
+ "descriptionSource": "fractal-wiki",
85
+ "fractalQuotes": [],
86
+ "flags": [],
87
+ "basedOn": {
88
+ "primary": "Morley wah/volume",
89
+ "manufacturer": "Morley",
90
+ "productName": "wah/volume",
91
+ "source": "fractal-wiki"
92
+ }
93
+ },
94
+ {
95
+ "am4Name": "VX845",
96
+ "fractalQuotes": [],
97
+ "flags": [
98
+ "VERIFY: no wiki entry found",
99
+ "VERIFY: no description or lineage identified"
100
+ ]
101
+ },
102
+ {
103
+ "am4Name": "Paragon",
104
+ "wikiName": "Paragon",
105
+ "description": "Based on Tycobrahe Parapedal.",
106
+ "descriptionSource": "fractal-wiki",
107
+ "fractalQuotes": [],
108
+ "flags": [],
109
+ "basedOn": {
110
+ "primary": "Tycobrahe Parapedal",
111
+ "manufacturer": "Tycobrahe",
112
+ "productName": "Parapedal",
113
+ "source": "fractal-wiki"
114
+ }
115
+ }
116
+ ]
117
+ }
@@ -0,0 +1,69 @@
1
+ export declare const LINEAGE_BLOCKS: readonly ["amp", "drive", "reverb", "delay", "compressor", "phaser", "chorus", "flanger", "wah"];
2
+ export type LineageBlock = typeof LINEAGE_BLOCKS[number];
3
+ export interface LineageRecord {
4
+ am4Name: string;
5
+ wikiName?: string;
6
+ basedOn?: {
7
+ primary: string;
8
+ manufacturer?: string;
9
+ model?: string;
10
+ productName?: string;
11
+ source: string;
12
+ };
13
+ description?: string;
14
+ descriptionSource?: string;
15
+ fractalQuotes?: Array<{
16
+ text: string;
17
+ url?: string;
18
+ attribution?: string;
19
+ }>;
20
+ flags?: string[];
21
+ family?: string;
22
+ powerTubes?: string;
23
+ matchingDynaCab?: string;
24
+ originalCab?: string;
25
+ categories?: string[];
26
+ clipTypes?: string[];
27
+ familyType?: string;
28
+ }
29
+ export declare function loadLineage(block: LineageBlock): LineageRecord[];
30
+ export declare function scoreRecord(rec: LineageRecord, query: string): number;
31
+ export declare function matchesStructured(rec: LineageRecord, filter: {
32
+ manufacturer?: string;
33
+ model?: string;
34
+ }): boolean;
35
+ export declare function formatLineageRecord(rec: LineageRecord, includeQuotes: boolean, maxQuotes?: number): string;
36
+ export type LineageLookupAsk = {
37
+ block_type: LineageBlock;
38
+ name?: string;
39
+ real_gear?: string;
40
+ manufacturer?: string;
41
+ model?: string;
42
+ };
43
+ export type LineageLookupHit = {
44
+ am4Name: string;
45
+ record: LineageRecord;
46
+ score?: number;
47
+ };
48
+ export type LineageLookupResult = {
49
+ ask: LineageLookupAsk;
50
+ found: true;
51
+ shape: 'forward' | 'reverse' | 'structured';
52
+ hits: LineageLookupHit[];
53
+ totalScanned: number;
54
+ } | {
55
+ ask: LineageLookupAsk;
56
+ found: false;
57
+ shape: 'forward' | 'reverse' | 'structured' | 'invalid';
58
+ reason: string;
59
+ totalScanned: number;
60
+ };
61
+ /**
62
+ * Run a single lineage lookup, returning a structured result. Throws
63
+ * `Error` only for shape-validation failures the single-ask tool already
64
+ * surfaces as a thrown error (exactly-one-call-shape, real_gear too
65
+ * short). Lineage misses return `{ found: false, reason }` so the batch
66
+ * caller can carry on processing the rest of the asks.
67
+ */
68
+ export declare function runLineageLookup(ask: LineageLookupAsk): LineageLookupResult;
69
+ //# sourceMappingURL=lineageLookup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lineageLookup.d.ts","sourceRoot":"","sources":["../../src/shared/lineageLookup.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,cAAc,kGAGjB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QACR,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAEjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAErB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,EAAE,CAahE;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAgBrE;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAChD,OAAO,CAQT;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,SAAI,GAAG,MAAM,CA6BrG;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,YAAY,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;IAGtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC3B;IACE,GAAG,EAAE,gBAAgB,CAAC;IACtB,KAAK,EAAE,IAAI,CAAC;IACZ,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC;IAG5C,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB,GACD;IACE,GAAG,EAAE,gBAAgB,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEN;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,gBAAgB,GAAG,mBAAmB,CAuF3E"}
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Lineage-lookup helpers shared between `am4_lookup_lineage` (single ask)
3
+ * and `am4_lookup_lineages` (batch). The actual lineage data lives in
4
+ * `src/fractal/shared/lineage/*-lineage.json` (sourced from the Fractal
5
+ * wiki + Blocks Guide PDF; only Fractal-authored content is stored).
6
+ *
7
+ * The MCP server (`src/server/index.ts`) wraps these helpers — single-ask
8
+ * formats hits as a hint-rich text block, the batch tool collects per-ask
9
+ * structured results so the agent can correlate without re-parsing prose.
10
+ *
11
+ * Tests cover this layer directly via `scripts/verify-msg.ts`, which is
12
+ * why the surface is exported instead of inlined in the server.
13
+ */
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const KNOWLEDGE_DIR = path.join(__dirname, 'lineage');
19
+ export const LINEAGE_BLOCKS = [
20
+ 'amp', 'drive', 'reverb', 'delay', 'compressor',
21
+ 'phaser', 'chorus', 'flanger', 'wah',
22
+ ];
23
+ const lineageCache = {};
24
+ export function loadLineage(block) {
25
+ const cached = lineageCache[block];
26
+ if (cached)
27
+ return cached;
28
+ const file = path.join(KNOWLEDGE_DIR, `${block}-lineage.json`);
29
+ if (!fs.existsSync(file)) {
30
+ throw new Error(`Lineage data missing at ${file}. Run \`npm run extract-lineage\` to regenerate from the wiki scrape + Blocks Guide PDF.`);
31
+ }
32
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
33
+ const records = parsed.records ?? [];
34
+ lineageCache[block] = records;
35
+ return records;
36
+ }
37
+ export function scoreRecord(rec, query) {
38
+ const q = query.toLowerCase();
39
+ let score = 0;
40
+ // Structured field hits score highest — they're deterministic, unlike
41
+ // substring matches on prose.
42
+ if (rec.basedOn?.manufacturer?.toLowerCase() === q)
43
+ score += 20;
44
+ if (rec.basedOn?.model?.toLowerCase() === q)
45
+ score += 20;
46
+ if (rec.basedOn?.productName?.toLowerCase().includes(q))
47
+ score += 12;
48
+ if (rec.am4Name.toLowerCase().includes(q))
49
+ score += 10;
50
+ if (rec.basedOn?.primary.toLowerCase().includes(q))
51
+ score += 8;
52
+ if (rec.wikiName && rec.wikiName.toLowerCase().includes(q))
53
+ score += 5;
54
+ if (rec.description && rec.description.toLowerCase().includes(q))
55
+ score += 5;
56
+ for (const qt of rec.fractalQuotes ?? []) {
57
+ if (qt.text.toLowerCase().includes(q))
58
+ score += 2;
59
+ }
60
+ return score;
61
+ }
62
+ export function matchesStructured(rec, filter) {
63
+ if (!rec.basedOn)
64
+ return false;
65
+ const ci = (a, b) => !b || (a?.toLowerCase() === b.toLowerCase());
66
+ return (ci(rec.basedOn.manufacturer, filter.manufacturer) &&
67
+ ci(rec.basedOn.model, filter.model));
68
+ }
69
+ export function formatLineageRecord(rec, includeQuotes, maxQuotes = 5) {
70
+ const lines = [`am4Name: ${rec.am4Name}`];
71
+ if (rec.wikiName && rec.wikiName !== rec.am4Name)
72
+ lines.push(`wikiName: ${rec.wikiName}`);
73
+ if (rec.family)
74
+ lines.push(`family: ${rec.family}`);
75
+ if (rec.familyType)
76
+ lines.push(`familyType: ${rec.familyType}`);
77
+ if (rec.categories?.length)
78
+ lines.push(`categories: ${rec.categories.join(', ')}`);
79
+ if (rec.clipTypes?.length)
80
+ lines.push(`clipTypes: ${rec.clipTypes.join(', ')}`);
81
+ if (rec.powerTubes)
82
+ lines.push(`powerTubes: ${rec.powerTubes}`);
83
+ if (rec.originalCab)
84
+ lines.push(`originalCab: ${rec.originalCab}`);
85
+ if (rec.matchingDynaCab)
86
+ lines.push(`matchingDynaCab: ${rec.matchingDynaCab}`);
87
+ if (rec.basedOn) {
88
+ const parts = [`basedOn: ${rec.basedOn.primary}`];
89
+ if (rec.basedOn.manufacturer)
90
+ parts.push(`manufacturer=${rec.basedOn.manufacturer}`);
91
+ if (rec.basedOn.model)
92
+ parts.push(`model=${rec.basedOn.model}`);
93
+ if (rec.basedOn.productName)
94
+ parts.push(`productName="${rec.basedOn.productName}"`);
95
+ parts.push(`source=${rec.basedOn.source}`);
96
+ lines.push(parts.join(' | '));
97
+ }
98
+ if (rec.description)
99
+ lines.push(`description: ${rec.description}`);
100
+ if (includeQuotes && rec.fractalQuotes?.length) {
101
+ const shown = rec.fractalQuotes.slice(0, maxQuotes);
102
+ lines.push(`fractalQuotes (${shown.length}/${rec.fractalQuotes.length}):`);
103
+ for (const q of shown) {
104
+ const url = q.url ? ` [${q.url}]` : '';
105
+ lines.push(` - "${q.text}"${url}`);
106
+ }
107
+ }
108
+ if (rec.flags?.length)
109
+ lines.push(`flags: ${rec.flags.join('; ')}`);
110
+ return lines.join('\n');
111
+ }
112
+ /**
113
+ * Run a single lineage lookup, returning a structured result. Throws
114
+ * `Error` only for shape-validation failures the single-ask tool already
115
+ * surfaces as a thrown error (exactly-one-call-shape, real_gear too
116
+ * short). Lineage misses return `{ found: false, reason }` so the batch
117
+ * caller can carry on processing the rest of the asks.
118
+ */
119
+ export function runLineageLookup(ask) {
120
+ const hasStructured = !!(ask.manufacturer || ask.model);
121
+ const shapeCount = [ask.name !== undefined, ask.real_gear !== undefined, hasStructured].filter(Boolean).length;
122
+ if (shapeCount !== 1) {
123
+ throw new Error('lookup_lineage requires exactly one call shape: `name` (forward), `real_gear` (fuzzy reverse), or at least one structured filter (`manufacturer` / `model`).');
124
+ }
125
+ const records = loadLineage(ask.block_type);
126
+ if (hasStructured) {
127
+ const matches = records.filter((r) => matchesStructured(r, { manufacturer: ask.manufacturer, model: ask.model }));
128
+ if (matches.length === 0) {
129
+ const filter = [
130
+ ask.manufacturer && `manufacturer="${ask.manufacturer}"`,
131
+ ask.model && `model="${ask.model}"`,
132
+ ].filter(Boolean).join(', ');
133
+ return {
134
+ ask,
135
+ found: false,
136
+ shape: 'structured',
137
+ reason: `No ${ask.block_type} records match ${filter}.`,
138
+ totalScanned: records.length,
139
+ };
140
+ }
141
+ return {
142
+ ask,
143
+ found: true,
144
+ shape: 'structured',
145
+ hits: matches.slice(0, 10).map((r) => ({ am4Name: r.am4Name, record: r })),
146
+ totalScanned: records.length,
147
+ };
148
+ }
149
+ if (ask.name !== undefined) {
150
+ const q = ask.name.toLowerCase().trim();
151
+ const exact = records.find((r) => r.am4Name.toLowerCase() === q || r.wikiName?.toLowerCase() === q);
152
+ const partial = exact ?? records.find((r) => r.am4Name.toLowerCase().includes(q) || r.wikiName?.toLowerCase().includes(q));
153
+ if (!partial) {
154
+ return {
155
+ ask,
156
+ found: false,
157
+ shape: 'forward',
158
+ reason: `No ${ask.block_type} lineage record matches "${ask.name}".`,
159
+ totalScanned: records.length,
160
+ };
161
+ }
162
+ return {
163
+ ask,
164
+ found: true,
165
+ shape: 'forward',
166
+ hits: [{ am4Name: partial.am4Name, record: partial }],
167
+ totalScanned: records.length,
168
+ };
169
+ }
170
+ // Reverse lookup.
171
+ const query = ask.real_gear.trim();
172
+ if (query.length < 2) {
173
+ throw new Error('`real_gear` query must be at least 2 characters.');
174
+ }
175
+ const scored = records
176
+ .map((r) => ({ r, score: scoreRecord(r, query) }))
177
+ .filter((x) => x.score > 0)
178
+ .sort((a, b) => b.score - a.score)
179
+ .slice(0, 10);
180
+ if (scored.length === 0) {
181
+ return {
182
+ ask,
183
+ found: false,
184
+ shape: 'reverse',
185
+ reason: `No ${ask.block_type} records mention "${query}".`,
186
+ totalScanned: records.length,
187
+ };
188
+ }
189
+ return {
190
+ ask,
191
+ found: true,
192
+ shape: 'reverse',
193
+ hits: scored.map(({ r, score }) => ({ am4Name: r.am4Name, record: r, score })),
194
+ totalScanned: records.length,
195
+ };
196
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * AM4 0x01 SET_PARAM value-field encoding.
3
+ *
4
+ * Reverse-engineered from `FUN_140156d10` (encoder) and `FUN_140156af0`
5
+ * (decoder) in AM4-Edit.exe — see docs/_private/SESSIONS.md session 05.
6
+ *
7
+ * Algorithm: sliding-window 8-to-7 bit-pack. N raw bytes become N+1 wire
8
+ * septets. Each iteration k=1..N takes the top (8-k) bits of the input
9
+ * byte for the current wire position (OR'd with the carry from the
10
+ * previous iteration), and saves the bottom k bits as carry for the next.
11
+ * All wire bytes have bit 7 = 0, satisfying the SysEx wire constraint.
12
+ *
13
+ * Verified round-trip on all 10 captured (param, value) samples — see
14
+ * scripts/verify-pack.ts.
15
+ */
16
+ export declare function packValue(raw: Uint8Array): Uint8Array;
17
+ export declare function unpackValue(wire: Uint8Array, rawLen: number): Uint8Array;
18
+ /** Pack a 32-bit IEEE 754 float (little-endian) into 5 wire septets. */
19
+ export declare function packFloat32LE(value: number): Uint8Array;
20
+ /**
21
+ * Chunked variant of `packValue` for payloads > 7 raw bytes. The sliding
22
+ * window in `packValue` restarts every 7 raw bytes — i.e. 7 raw → 8 packed,
23
+ * repeated. A trailing partial chunk of N<7 bytes produces N+1 packed
24
+ * bytes the same way a full standalone `packValue` call would.
25
+ *
26
+ * For ≤ 7 raw bytes this is identical to `packValue`, so small payloads
27
+ * (the 4-byte float used by SET_PARAM, the 4-byte slot uint used by
28
+ * SAVE_TO_SLOT) are unaffected. Needed for 36-byte preset-name payloads
29
+ * decoded Session 19; confirmed byte-exact against the captured rename.
30
+ */
31
+ export declare function packValueChunked(raw: Uint8Array): Uint8Array;
32
+ /**
33
+ * Inverse of `packValueChunked`: decode a chunked-packed wire buffer
34
+ * back into raw bytes. `rawLen` is the known total raw byte count
35
+ * (typically carried in hdr4 of the command the wire payload came from).
36
+ */
37
+ export declare function unpackValueChunked(wire: Uint8Array, rawLen: number): Uint8Array;
38
+ /** Inverse of packFloat32LE — decode 5 wire septets back to a float. */
39
+ export declare function unpackFloat32LE(wire: Uint8Array): number;
40
+ //# sourceMappingURL=packValue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packValue.d.ts","sourceRoot":"","sources":["../../src/shared/packValue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAWrD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAWxE;AAED,wEAAwE;AACxE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAIvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAgB5D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAiB/E;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAMxD"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * AM4 0x01 SET_PARAM value-field encoding.
3
+ *
4
+ * Reverse-engineered from `FUN_140156d10` (encoder) and `FUN_140156af0`
5
+ * (decoder) in AM4-Edit.exe — see docs/_private/SESSIONS.md session 05.
6
+ *
7
+ * Algorithm: sliding-window 8-to-7 bit-pack. N raw bytes become N+1 wire
8
+ * septets. Each iteration k=1..N takes the top (8-k) bits of the input
9
+ * byte for the current wire position (OR'd with the carry from the
10
+ * previous iteration), and saves the bottom k bits as carry for the next.
11
+ * All wire bytes have bit 7 = 0, satisfying the SysEx wire constraint.
12
+ *
13
+ * Verified round-trip on all 10 captured (param, value) samples — see
14
+ * scripts/verify-pack.ts.
15
+ */
16
+ export function packValue(raw) {
17
+ const out = new Uint8Array(raw.length + 1);
18
+ let carry = 0;
19
+ for (let i = 0; i < raw.length; i++) {
20
+ const k = i + 1;
21
+ const b = raw[i];
22
+ out[i] = (((b >> k) & 0x7f) | carry) & 0x7f;
23
+ carry = ((~(0x7f << k) & b) << (7 - k)) & 0x7f;
24
+ }
25
+ out[raw.length] = carry;
26
+ return out;
27
+ }
28
+ export function unpackValue(wire, rawLen) {
29
+ const out = new Uint8Array(rawLen);
30
+ for (let i = 0; i < wire.length; i++) {
31
+ const k = i + 1;
32
+ const b = wire[i] & 0x7f;
33
+ if (i > 0 && i - 1 < rawLen) {
34
+ out[i - 1] |= ((~(0x7f >> k) & b) >> (8 - k)) & 0xff;
35
+ }
36
+ if (i < rawLen)
37
+ out[i] = (b << k) & 0xff;
38
+ }
39
+ return out;
40
+ }
41
+ /** Pack a 32-bit IEEE 754 float (little-endian) into 5 wire septets. */
42
+ export function packFloat32LE(value) {
43
+ const buf = new ArrayBuffer(4);
44
+ new DataView(buf).setFloat32(0, value, true);
45
+ return packValue(new Uint8Array(buf));
46
+ }
47
+ /**
48
+ * Chunked variant of `packValue` for payloads > 7 raw bytes. The sliding
49
+ * window in `packValue` restarts every 7 raw bytes — i.e. 7 raw → 8 packed,
50
+ * repeated. A trailing partial chunk of N<7 bytes produces N+1 packed
51
+ * bytes the same way a full standalone `packValue` call would.
52
+ *
53
+ * For ≤ 7 raw bytes this is identical to `packValue`, so small payloads
54
+ * (the 4-byte float used by SET_PARAM, the 4-byte slot uint used by
55
+ * SAVE_TO_SLOT) are unaffected. Needed for 36-byte preset-name payloads
56
+ * decoded Session 19; confirmed byte-exact against the captured rename.
57
+ */
58
+ export function packValueChunked(raw) {
59
+ const CHUNK = 7;
60
+ // Full 7-byte chunks → 8 packed bytes each; trailing partial chunk of
61
+ // length R → R+1 packed bytes.
62
+ const fullChunks = Math.floor(raw.length / CHUNK);
63
+ const remainder = raw.length % CHUNK;
64
+ const outLen = fullChunks * 8 + (remainder > 0 ? remainder + 1 : 0);
65
+ const out = new Uint8Array(outLen);
66
+ let outPos = 0;
67
+ for (let offset = 0; offset < raw.length; offset += CHUNK) {
68
+ const end = Math.min(offset + CHUNK, raw.length);
69
+ const chunkPacked = packValue(raw.subarray(offset, end));
70
+ out.set(chunkPacked, outPos);
71
+ outPos += chunkPacked.length;
72
+ }
73
+ return out;
74
+ }
75
+ /**
76
+ * Inverse of `packValueChunked`: decode a chunked-packed wire buffer
77
+ * back into raw bytes. `rawLen` is the known total raw byte count
78
+ * (typically carried in hdr4 of the command the wire payload came from).
79
+ */
80
+ export function unpackValueChunked(wire, rawLen) {
81
+ const CHUNK_RAW = 7;
82
+ const CHUNK_WIRE = 8;
83
+ const out = new Uint8Array(rawLen);
84
+ let rawPos = 0;
85
+ let wirePos = 0;
86
+ while (rawPos < rawLen) {
87
+ const remainingRaw = rawLen - rawPos;
88
+ const thisChunkRaw = Math.min(CHUNK_RAW, remainingRaw);
89
+ const thisChunkWire = thisChunkRaw === CHUNK_RAW ? CHUNK_WIRE : thisChunkRaw + 1;
90
+ const chunk = wire.subarray(wirePos, wirePos + thisChunkWire);
91
+ const unpacked = unpackValue(chunk, thisChunkRaw);
92
+ out.set(unpacked, rawPos);
93
+ rawPos += thisChunkRaw;
94
+ wirePos += thisChunkWire;
95
+ }
96
+ return out;
97
+ }
98
+ /** Inverse of packFloat32LE — decode 5 wire septets back to a float. */
99
+ export function unpackFloat32LE(wire) {
100
+ if (wire.length !== 5) {
101
+ throw new Error(`unpackFloat32LE: expected 5 wire bytes, got ${wire.length}`);
102
+ }
103
+ const raw = unpackValue(wire, 4);
104
+ return new DataView(raw.buffer, raw.byteOffset, 4).getFloat32(0, true);
105
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared Fractal-protocol types used by multiple device packages.
3
+ *
4
+ * Lifted out of per-device files so device modules don't have to
5
+ * cross-import each other for fundamental protocol shapes (every
6
+ * Fractal device since the AM4 has used the same 14-bit pidLow/pidHigh
7
+ * addressing for parameters).
8
+ */
9
+ /**
10
+ * Two-coordinate address of a parameter in a Fractal device's wire
11
+ * protocol. Both fields are 14-bit unsigned ints, transmitted as
12
+ * septet pairs in the SysEx payload.
13
+ *
14
+ * AM4: `pidLow` selects the block/slot, `pidHigh` is the parameter
15
+ * register within that block.
16
+ * Axe-Fx II / III / FM3 / FM9 use the same shape with device-specific
17
+ * encoding details documented per device.
18
+ */
19
+ export interface ParamId {
20
+ pidLow: number;
21
+ pidHigh: number;
22
+ }
23
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;GASG;AACH,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared Fractal-protocol types used by multiple device packages.
3
+ *
4
+ * Lifted out of per-device files so device modules don't have to
5
+ * cross-import each other for fundamental protocol shapes (every
6
+ * Fractal device since the AM4 has used the same 14-bit pidLow/pidHigh
7
+ * addressing for parameters).
8
+ */
9
+ export {};
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "fractal-midi",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Pure-TypeScript codec and parameter dictionaries for Fractal Audio devices (AM4, Axe-Fx II, Axe-Fx III). No MIDI transport dependency — bring your own.",
5
+ "license": "Apache-2.0",
6
+ "author": "Stephen Staker <stephenstaker@gmail.com>",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./shared": {
16
+ "types": "./dist/shared/index.d.ts",
17
+ "default": "./dist/shared/index.js"
18
+ },
19
+ "./am4": {
20
+ "types": "./dist/am4/index.d.ts",
21
+ "default": "./dist/am4/index.js"
22
+ },
23
+ "./axe-fx-ii": {
24
+ "types": "./dist/axe-fx-ii/index.d.ts",
25
+ "default": "./dist/axe-fx-ii/index.js"
26
+ },
27
+ "./axe-fx-iii": {
28
+ "types": "./dist/axe-fx-iii/index.d.ts",
29
+ "default": "./dist/axe-fx-iii/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "LICENSE",
36
+ "NOTICE"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/TheAndrewStaker/fractal-midi.git"
41
+ },
42
+ "homepage": "https://github.com/TheAndrewStaker/fractal-midi#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/TheAndrewStaker/fractal-midi/issues"
45
+ },
46
+ "keywords": [
47
+ "fractal",
48
+ "fractal-audio",
49
+ "axe-fx",
50
+ "axe-fx-ii",
51
+ "axe-fx-iii",
52
+ "am4",
53
+ "fm3",
54
+ "fm9",
55
+ "midi",
56
+ "sysex",
57
+ "codec",
58
+ "guitar",
59
+ "amp-modeling"
60
+ ],
61
+ "scripts": {
62
+ "build": "tsc -p tsconfig.json && tsx scripts/copy-build-assets.ts",
63
+ "typecheck": "tsc --noEmit -p tsconfig.json",
64
+ "test": "tsx test/run-all.ts",
65
+ "preflight": "npm run typecheck && npm test && npm run build"
66
+ },
67
+ "devDependencies": {
68
+ "@types/node": "^20.0.0",
69
+ "tsx": "^4.19.0",
70
+ "typescript": "^5.0.0"
71
+ },
72
+ "engines": {
73
+ "node": ">=18.0.0"
74
+ }
75
+ }