0xray 4.0.12 → 4.0.14
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/.grok-plugin/plugin.json +1 -1
- package/.xray/features.json +1 -1
- package/.xray/features.schema.json +1 -1
- package/AGENTS-consumer.md +4 -2
- package/AGENTS.md +8 -1
- package/README.md +1 -1
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +8 -1
- package/dist/CHANGELOG.md +25 -1
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/integrations/cursor/README.md +61 -0
- package/dist/integrations/cursor/hooks/after-file-edit.js +38 -0
- package/dist/integrations/cursor/hooks/cursor-hook-utils.js +170 -0
- package/dist/integrations/cursor/hooks/cursor-usage-receipt.js +298 -0
- package/dist/integrations/cursor/hooks/hooks.json +20 -0
- package/dist/integrations/cursor/hooks/pre-compact.js +88 -0
- package/dist/integrations/cursor/hooks/pre-tool-use.js +105 -0
- package/dist/integrations/hooks/station-hook-runtime.cjs +89 -1
- package/dist/integrations/hooks/station-hook-runtime.mjs +2 -0
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/nucleus/suit-temperament.d.ts +2 -2
- package/dist/nucleus/suit-temperament.js +3 -1
- package/llms.txt +6 -4
- package/package.json +4 -2
- package/scripts/foundry/CHANGELOG.md +37 -1
- package/scripts/foundry/README.md +13 -1
- package/scripts/foundry/assert-packed-dist-cli.mjs +97 -0
- package/scripts/foundry/blip-render.cjs +1237 -0
- package/scripts/foundry/blip-rippel.cjs +1963 -0
- package/scripts/foundry/blip.mjs +109 -0
- package/scripts/foundry/cli.mjs +8 -4
- package/scripts/foundry/inspect.mjs +117 -7
- package/scripts/foundry/mint-suit.cjs +215 -14
- package/scripts/foundry/package.json +7 -1
- package/scripts/foundry/plant/agents/blip-inspect.yml +5 -0
- package/scripts/foundry/plant/agents/blip.yml +5 -0
- package/scripts/foundry/plant/agents/inspect.yml +1 -1
- package/scripts/foundry/plant/agents/mill.yml +1 -1
- package/scripts/foundry/plant/agents/sound-inspect.yml +5 -0
- package/scripts/foundry/plant/agents/sound.yml +5 -0
- package/scripts/foundry/plant/motions/MOTION-REGISTRY.md +35 -0
- package/scripts/foundry/plant/motions/registry.json +81 -0
- package/scripts/foundry/plant/skills/blip/SKILL.md +39 -0
- package/scripts/foundry/plant/skills/blip-inspect/SKILL.md +32 -0
- package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -2
- package/scripts/foundry/plant/skills/sound/SKILL.md +29 -0
- package/scripts/foundry/plant/skills/sound-inspect/SKILL.md +28 -0
- package/scripts/foundry/release-gate.mjs +18 -10
- package/scripts/foundry/release.mjs +3 -0
- package/scripts/foundry/sound-bed.cjs +454 -0
- package/scripts/foundry/sound-rippel.cjs +1434 -0
- package/scripts/foundry/sound.mjs +93 -0
- package/scripts/node/assert-packed-dist-cli.mjs +4 -0
- package/scripts/node/consumer-install-smoke.mjs +20 -1
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/xray/features.json +1 -1
- package/xray/features.schema.json +1 -1
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory-sound spine: brief → checksum seed → genre → headless wav → metrics gate.
|
|
3
|
+
* Render is the Rippel prototype mill (membrane / metal / mixer), crystal-clear mix.
|
|
4
|
+
* Not mill inspect.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const crypto = require("crypto");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const rippel = require("./sound-rippel.cjs");
|
|
11
|
+
|
|
12
|
+
const SPINE = 2;
|
|
13
|
+
const SAMPLE_RATE = 44100;
|
|
14
|
+
const DEFAULT_SECONDS = 4;
|
|
15
|
+
const WINDOW_SEC = 0.25;
|
|
16
|
+
const INTRO_SEC = 0.75;
|
|
17
|
+
const FADE_SEC = 1;
|
|
18
|
+
const PEAK_LIMIT = 0.95;
|
|
19
|
+
const CHOP_MAX = 0.1;
|
|
20
|
+
const CHOP_STRONG = 0.05;
|
|
21
|
+
const LEVELS_RATIO_MIN = 1.5;
|
|
22
|
+
const LEVELS_RATIO_MAX = 3;
|
|
23
|
+
const BODY_MIN_RMS = 0.05;
|
|
24
|
+
const RECEIPT_REL = path.join(".xray", "sound-bed-receipt.json");
|
|
25
|
+
const WAV_REL = path.join(".xray", "sound", "bed.wav");
|
|
26
|
+
|
|
27
|
+
const GENRE_ALIASES = rippel.GENRE_ALIASES;
|
|
28
|
+
const GENRES = rippel.GENRES;
|
|
29
|
+
|
|
30
|
+
function canonicalJson(value) {
|
|
31
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
32
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
33
|
+
const keys = Object.keys(value).sort();
|
|
34
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function seedFromBrief(brief, genre) {
|
|
38
|
+
const payload = canonicalJson({
|
|
39
|
+
brief: String(brief || "").trim(),
|
|
40
|
+
genre: resolveGenre(genre).id,
|
|
41
|
+
spine: SPINE,
|
|
42
|
+
});
|
|
43
|
+
const digest = crypto.createHash("sha256").update(payload, "utf8").digest("hex");
|
|
44
|
+
return `0x${digest}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mulberry32(seedU32) {
|
|
48
|
+
let t = seedU32 >>> 0;
|
|
49
|
+
return function next() {
|
|
50
|
+
t += 0x6d2b79f5;
|
|
51
|
+
let x = t;
|
|
52
|
+
x = Math.imul(x ^ (x >>> 15), x | 1);
|
|
53
|
+
x ^= x + Math.imul(x ^ (x >>> 7), x | 61);
|
|
54
|
+
return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function seedU32(seedHex) {
|
|
59
|
+
const hex = String(seedHex || "").replace(/^0x/, "").slice(0, 8);
|
|
60
|
+
const n = Number.parseInt(hex, 16);
|
|
61
|
+
return Number.isFinite(n) ? n >>> 0 : 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveGenre(name) {
|
|
65
|
+
return rippel.resolveGenre(name);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderSamples({ brief, genre, seconds, seed, syncopate }) {
|
|
69
|
+
const dur = Number(seconds) > 0 ? Number(seconds) : DEFAULT_SECONDS;
|
|
70
|
+
const g = resolveGenre(genre);
|
|
71
|
+
const seedHex = seed || seedFromBrief(brief, g.id);
|
|
72
|
+
const rng = mulberry32(seedU32(seedHex));
|
|
73
|
+
return rippel.renderRippelBed({
|
|
74
|
+
brief,
|
|
75
|
+
genre,
|
|
76
|
+
seconds: dur,
|
|
77
|
+
seedHex,
|
|
78
|
+
rng,
|
|
79
|
+
sampleRate: SAMPLE_RATE,
|
|
80
|
+
syncopate: Boolean(syncopate),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeWav16Mono(file, samples, sampleRate = SAMPLE_RATE) {
|
|
85
|
+
const n = samples.length;
|
|
86
|
+
const dataSize = n * 2;
|
|
87
|
+
const buf = Buffer.alloc(44 + dataSize);
|
|
88
|
+
buf.write("RIFF", 0);
|
|
89
|
+
buf.writeUInt32LE(36 + dataSize, 4);
|
|
90
|
+
buf.write("WAVE", 8);
|
|
91
|
+
buf.write("fmt ", 12);
|
|
92
|
+
buf.writeUInt32LE(16, 16);
|
|
93
|
+
buf.writeUInt16LE(1, 20);
|
|
94
|
+
buf.writeUInt16LE(1, 22);
|
|
95
|
+
buf.writeUInt32LE(sampleRate, 24);
|
|
96
|
+
buf.writeUInt32LE(sampleRate * 2, 28);
|
|
97
|
+
buf.writeUInt16LE(2, 32);
|
|
98
|
+
buf.writeUInt16LE(16, 34);
|
|
99
|
+
buf.write("data", 36);
|
|
100
|
+
buf.writeUInt32LE(dataSize, 40);
|
|
101
|
+
for (let i = 0; i < n; i++) {
|
|
102
|
+
const x = Math.max(-1, Math.min(1, samples[i]));
|
|
103
|
+
buf.writeInt16LE(Math.round(x * 32767), 44 + i * 2);
|
|
104
|
+
}
|
|
105
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
106
|
+
fs.writeFileSync(file, buf);
|
|
107
|
+
return file;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readWav16(file) {
|
|
111
|
+
if (!file || !fs.existsSync(file)) {
|
|
112
|
+
const err = new Error("wav missing");
|
|
113
|
+
err.code = "SOUND_WAV_MISSING";
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
const buf = fs.readFileSync(file);
|
|
117
|
+
if (buf.length < 44 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") {
|
|
118
|
+
const err = new Error("wav not riff/wave");
|
|
119
|
+
err.code = "SOUND_WAV_BAD";
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
let offset = 12;
|
|
123
|
+
let audioFormat = 0;
|
|
124
|
+
let channels = 0;
|
|
125
|
+
let sampleRate = 0;
|
|
126
|
+
let bits = 0;
|
|
127
|
+
let dataOffset = -1;
|
|
128
|
+
let dataSize = 0;
|
|
129
|
+
while (offset + 8 <= buf.length) {
|
|
130
|
+
const id = buf.toString("ascii", offset, offset + 4);
|
|
131
|
+
const size = buf.readUInt32LE(offset + 4);
|
|
132
|
+
const start = offset + 8;
|
|
133
|
+
if (id === "fmt ") {
|
|
134
|
+
audioFormat = buf.readUInt16LE(start);
|
|
135
|
+
channels = buf.readUInt16LE(start + 2);
|
|
136
|
+
sampleRate = buf.readUInt32LE(start + 4);
|
|
137
|
+
bits = buf.readUInt16LE(start + 14);
|
|
138
|
+
} else if (id === "data") {
|
|
139
|
+
dataOffset = start;
|
|
140
|
+
dataSize = size;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
offset = start + size + (size % 2);
|
|
144
|
+
}
|
|
145
|
+
if (audioFormat !== 1 || bits !== 16 || channels < 1 || dataOffset < 0) {
|
|
146
|
+
const err = new Error("wav must be 16-bit PCM");
|
|
147
|
+
err.code = "SOUND_WAV_BAD";
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
const frames = Math.floor(dataSize / (2 * channels));
|
|
151
|
+
const samples = new Float64Array(frames);
|
|
152
|
+
for (let i = 0; i < frames; i++) {
|
|
153
|
+
samples[i] = buf.readInt16LE(dataOffset + i * 2 * channels) / 32768;
|
|
154
|
+
}
|
|
155
|
+
return { samples, sampleRate };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function rmsOf(samples, start = 0, end = samples.length) {
|
|
159
|
+
const a = Math.max(0, start);
|
|
160
|
+
const b = Math.min(samples.length, end);
|
|
161
|
+
if (b <= a) return 0;
|
|
162
|
+
let acc = 0;
|
|
163
|
+
for (let i = a; i < b; i++) acc += samples[i] * samples[i];
|
|
164
|
+
return Math.sqrt(acc / (b - a));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function windowRms(samples, sampleRate, windowSec = WINDOW_SEC) {
|
|
168
|
+
const win = Math.max(1, Math.floor(sampleRate * windowSec));
|
|
169
|
+
const out = [];
|
|
170
|
+
for (let i = 0; i + win <= samples.length; i += win) {
|
|
171
|
+
out.push(rmsOf(samples, i, i + win));
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function goertzelPower(samples, start, end, sampleRate, freq) {
|
|
177
|
+
const n = end - start;
|
|
178
|
+
if (n < 8) return 0;
|
|
179
|
+
const w = (2 * Math.PI * freq) / sampleRate;
|
|
180
|
+
const coeff = 2 * Math.cos(w);
|
|
181
|
+
let s0 = 0;
|
|
182
|
+
let s1 = 0;
|
|
183
|
+
let s2 = 0;
|
|
184
|
+
for (let i = start; i < end; i++) {
|
|
185
|
+
s0 = samples[i] + coeff * s1 - s2;
|
|
186
|
+
s2 = s1;
|
|
187
|
+
s1 = s0;
|
|
188
|
+
}
|
|
189
|
+
const power = s1 * s1 + s2 * s2 - coeff * s1 * s2;
|
|
190
|
+
return power > 0 ? power : 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function humBins() {
|
|
194
|
+
const bins = [];
|
|
195
|
+
const lo = Math.log(60);
|
|
196
|
+
const hi = Math.log(1800);
|
|
197
|
+
for (let i = 0; i < 20; i++) {
|
|
198
|
+
bins.push(Math.exp(lo + ((hi - lo) * i) / 19));
|
|
199
|
+
}
|
|
200
|
+
return bins;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function failClosed(reason, extra) {
|
|
204
|
+
return {
|
|
205
|
+
status: "FAIL",
|
|
206
|
+
failClosed: true,
|
|
207
|
+
reason,
|
|
208
|
+
...extra,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function evaluateMetrics(samples, sampleRate) {
|
|
213
|
+
if (!samples || samples.length < Math.floor(sampleRate * 2)) {
|
|
214
|
+
return failClosed("too-short", {
|
|
215
|
+
metrics: { chop: { ok: false }, levels: { ok: false }, peak: { ok: false }, hum: { ok: false } },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const rms = windowRms(samples, sampleRate);
|
|
220
|
+
let chopMean = null;
|
|
221
|
+
if (rms.length >= 2) {
|
|
222
|
+
let acc = 0;
|
|
223
|
+
for (let i = 1; i < rms.length; i++) acc += Math.abs(rms[i] - rms[i - 1]);
|
|
224
|
+
chopMean = acc / (rms.length - 1);
|
|
225
|
+
}
|
|
226
|
+
const chop = {
|
|
227
|
+
meanAbsDeltaRms: chopMean,
|
|
228
|
+
max: CHOP_MAX,
|
|
229
|
+
strong: CHOP_STRONG,
|
|
230
|
+
ok: chopMean !== null && chopMean <= CHOP_MAX,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const introN = Math.floor(INTRO_SEC * sampleRate);
|
|
234
|
+
const fadeN = Math.floor(FADE_SEC * sampleRate);
|
|
235
|
+
const bodyEnd = samples.length - fadeN;
|
|
236
|
+
const introRms = rmsOf(samples, 0, introN);
|
|
237
|
+
const bodyMean = rmsOf(samples, introN, bodyEnd);
|
|
238
|
+
const bodyWindows = windowRms(samples.subarray(introN, Math.max(introN, bodyEnd)), sampleRate);
|
|
239
|
+
const bodyMin = bodyWindows.length ? Math.min(...bodyWindows) : 0;
|
|
240
|
+
const ratio = introRms > 1e-9 ? bodyMean / introRms : Number.POSITIVE_INFINITY;
|
|
241
|
+
const levels = {
|
|
242
|
+
bodyIntroRatio: Number.isFinite(ratio) ? ratio : null,
|
|
243
|
+
bodyMinRms: bodyMin,
|
|
244
|
+
bodyMeanRms: bodyMean,
|
|
245
|
+
introMeanRms: introRms,
|
|
246
|
+
ratioMin: LEVELS_RATIO_MIN,
|
|
247
|
+
ratioMax: LEVELS_RATIO_MAX,
|
|
248
|
+
bodyMinRequired: BODY_MIN_RMS,
|
|
249
|
+
ok:
|
|
250
|
+
Number.isFinite(ratio) &&
|
|
251
|
+
ratio >= LEVELS_RATIO_MIN &&
|
|
252
|
+
ratio <= LEVELS_RATIO_MAX &&
|
|
253
|
+
bodyMin >= BODY_MIN_RMS,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
let peak = 0;
|
|
257
|
+
for (let i = 0; i < samples.length; i++) {
|
|
258
|
+
const a = Math.abs(samples[i]);
|
|
259
|
+
if (a > peak) peak = a;
|
|
260
|
+
}
|
|
261
|
+
const peakGate = { maxAbs: peak, max: PEAK_LIMIT, ok: peak < PEAK_LIMIT };
|
|
262
|
+
|
|
263
|
+
const bins = humBins();
|
|
264
|
+
const win = Math.max(1, Math.floor(sampleRate * WINDOW_SEC));
|
|
265
|
+
const dominant = [];
|
|
266
|
+
const centroids = [];
|
|
267
|
+
let same = 0;
|
|
268
|
+
let shareAcc = 0;
|
|
269
|
+
let lastBin = -1;
|
|
270
|
+
for (let i = 0; i + win <= samples.length; i += win) {
|
|
271
|
+
let total = 0;
|
|
272
|
+
let best = 0;
|
|
273
|
+
let bestIdx = -1;
|
|
274
|
+
let weighted = 0;
|
|
275
|
+
for (let b = 0; b < bins.length; b++) {
|
|
276
|
+
const p = goertzelPower(samples, i, i + win, sampleRate, bins[b]);
|
|
277
|
+
total += p;
|
|
278
|
+
weighted += p * bins[b];
|
|
279
|
+
if (p > best) {
|
|
280
|
+
best = p;
|
|
281
|
+
bestIdx = b;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const share = total > 0 ? best / total : 0;
|
|
285
|
+
if (total > 0) centroids.push(weighted / total);
|
|
286
|
+
dominant.push({ bin: bestIdx, share });
|
|
287
|
+
if (lastBin === bestIdx) same += 1;
|
|
288
|
+
lastBin = bestIdx;
|
|
289
|
+
shareAcc += share;
|
|
290
|
+
}
|
|
291
|
+
const windows = dominant.length;
|
|
292
|
+
const sameRatio = windows > 1 ? same / (windows - 1) : 1;
|
|
293
|
+
const meanShare = windows ? shareAcc / windows : 1;
|
|
294
|
+
let centroidMean = 0;
|
|
295
|
+
for (const c of centroids) centroidMean += c;
|
|
296
|
+
centroidMean = centroids.length ? centroidMean / centroids.length : 0;
|
|
297
|
+
let centroidVar = 0;
|
|
298
|
+
for (const c of centroids) centroidVar += (c - centroidMean) * (c - centroidMean);
|
|
299
|
+
const centroidCv =
|
|
300
|
+
centroids.length > 1 && centroidMean > 0
|
|
301
|
+
? Math.sqrt(centroidVar / centroids.length) / centroidMean
|
|
302
|
+
: 0;
|
|
303
|
+
// Continuous drone: one bin owns the piece AND the centroid barely moves.
|
|
304
|
+
// Kick membranes can own a low bin without being a flat sine.
|
|
305
|
+
const drone = sameRatio >= 0.8 && meanShare >= 0.5 && centroidCv <= 0.045;
|
|
306
|
+
const hum = {
|
|
307
|
+
sameDominantRatio: sameRatio,
|
|
308
|
+
meanDominantShare: meanShare,
|
|
309
|
+
centroidCv,
|
|
310
|
+
ok: !drone,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const gates = { chop: chop.ok, levels: levels.ok, peak: peakGate.ok, hum: hum.ok };
|
|
314
|
+
const ok = gates.chop && gates.levels && gates.peak && gates.hum;
|
|
315
|
+
return {
|
|
316
|
+
status: ok ? "PASS" : "FAIL",
|
|
317
|
+
failClosed: true,
|
|
318
|
+
metrics: { chop, levels, peak: peakGate, hum },
|
|
319
|
+
gates,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function evaluateWavFile(file) {
|
|
324
|
+
try {
|
|
325
|
+
const wav = readWav16(file);
|
|
326
|
+
return { ...evaluateMetrics(wav.samples, wav.sampleRate), wav: file, sampleRate: wav.sampleRate };
|
|
327
|
+
} catch (err) {
|
|
328
|
+
return failClosed(err instanceof Error ? err.message : String(err), {
|
|
329
|
+
wav: file || null,
|
|
330
|
+
metrics: { chop: { ok: false }, levels: { ok: false }, peak: { ok: false }, hum: { ok: false } },
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function buildReceipt(input, evaled) {
|
|
336
|
+
const genre = input.genre || { id: "ambient", bpm: 72, voices: ["pad", "air"] };
|
|
337
|
+
return {
|
|
338
|
+
kind: "sound-bed",
|
|
339
|
+
status: evaled.status,
|
|
340
|
+
failClosed: true,
|
|
341
|
+
brief: String(input.brief || ""),
|
|
342
|
+
seed: input.seed,
|
|
343
|
+
genre: genre.id,
|
|
344
|
+
alias: genre.alias || null,
|
|
345
|
+
tempo: genre.bpm,
|
|
346
|
+
voices: genre.voices,
|
|
347
|
+
engine: input.engine || rippel.ENGINE,
|
|
348
|
+
mix: input.mix || rippel.MIX,
|
|
349
|
+
ssot: input.ssot || rippel.SSOT,
|
|
350
|
+
topology: input.topology || rippel.TOPOLOGY,
|
|
351
|
+
toneOffline: input.toneOffline || rippel.TONE_OFFLINE_BLOCKER,
|
|
352
|
+
wav: input.wavRel || input.wav,
|
|
353
|
+
durationSec: input.seconds,
|
|
354
|
+
sampleRate: input.sampleRate || SAMPLE_RATE,
|
|
355
|
+
metrics: evaled.metrics,
|
|
356
|
+
gates: evaled.gates || {
|
|
357
|
+
chop: false,
|
|
358
|
+
levels: false,
|
|
359
|
+
peak: false,
|
|
360
|
+
hum: false,
|
|
361
|
+
},
|
|
362
|
+
reason: evaled.reason || null,
|
|
363
|
+
spine: SPINE,
|
|
364
|
+
writtenAt: new Date().toISOString(),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function receiptPath(root) {
|
|
369
|
+
return path.join(root, RECEIPT_REL);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function defaultWavPath(root) {
|
|
373
|
+
return path.join(root, WAV_REL);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function writeReceipt(root, receipt) {
|
|
377
|
+
const file = receiptPath(root);
|
|
378
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
379
|
+
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
380
|
+
return file;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function readReceipt(root) {
|
|
384
|
+
const file = receiptPath(root);
|
|
385
|
+
if (!fs.existsSync(file)) return null;
|
|
386
|
+
try {
|
|
387
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
388
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
389
|
+
} catch {
|
|
390
|
+
return { status: "FAIL", failClosed: true, reason: "receipt-unreadable" };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function renderBed(opts = {}) {
|
|
395
|
+
const root = opts.root || process.cwd();
|
|
396
|
+
const brief = String(opts.brief || "factory-sound bed");
|
|
397
|
+
const seconds = Number(opts.seconds) > 0 ? Number(opts.seconds) : DEFAULT_SECONDS;
|
|
398
|
+
const wav = opts.out ? path.resolve(root, opts.out) : defaultWavPath(root);
|
|
399
|
+
const rendered = renderSamples({ brief, genre: opts.genre, seconds, seed: opts.seed });
|
|
400
|
+
writeWav16Mono(wav, rendered.samples, rendered.sampleRate);
|
|
401
|
+
const evaled = evaluateMetrics(rendered.samples, rendered.sampleRate);
|
|
402
|
+
const receipt = buildReceipt(
|
|
403
|
+
{
|
|
404
|
+
brief,
|
|
405
|
+
seed: rendered.seed,
|
|
406
|
+
genre: rendered.genre,
|
|
407
|
+
engine: rendered.engine,
|
|
408
|
+
mix: rendered.mix,
|
|
409
|
+
ssot: rendered.ssot,
|
|
410
|
+
topology: rendered.topology,
|
|
411
|
+
toneOffline: rendered.toneOffline,
|
|
412
|
+
wav,
|
|
413
|
+
wavRel: path.relative(root, wav) || wav,
|
|
414
|
+
seconds: rendered.seconds,
|
|
415
|
+
sampleRate: rendered.sampleRate,
|
|
416
|
+
},
|
|
417
|
+
evaled,
|
|
418
|
+
);
|
|
419
|
+
writeReceipt(root, receipt);
|
|
420
|
+
return { receipt, wav, seed: rendered.seed };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
module.exports = {
|
|
424
|
+
SPINE,
|
|
425
|
+
SAMPLE_RATE,
|
|
426
|
+
DEFAULT_SECONDS,
|
|
427
|
+
RECEIPT_REL,
|
|
428
|
+
WAV_REL,
|
|
429
|
+
GENRE_ALIASES,
|
|
430
|
+
GENRES,
|
|
431
|
+
seedFromBrief,
|
|
432
|
+
resolveGenre,
|
|
433
|
+
tempoFromSeed: rippel.tempoFromSeed,
|
|
434
|
+
motionGrid: rippel.motionGrid,
|
|
435
|
+
renderSamples,
|
|
436
|
+
writeWav16Mono,
|
|
437
|
+
readWav16,
|
|
438
|
+
evaluateMetrics,
|
|
439
|
+
evaluateWavFile,
|
|
440
|
+
renderBed,
|
|
441
|
+
receiptPath,
|
|
442
|
+
defaultWavPath,
|
|
443
|
+
writeReceipt,
|
|
444
|
+
readReceipt,
|
|
445
|
+
buildReceipt,
|
|
446
|
+
TONE_OFFLINE_BLOCKER: rippel.TONE_OFFLINE_BLOCKER,
|
|
447
|
+
MIX: rippel.MIX,
|
|
448
|
+
SSOT: rippel.SSOT,
|
|
449
|
+
RIPPEL: rippel.RIPPEL,
|
|
450
|
+
bandEnergy: rippel.bandEnergy,
|
|
451
|
+
highpassEnergy: rippel.highpassEnergy,
|
|
452
|
+
zeroCrossRate: rippel.zeroCrossRate,
|
|
453
|
+
crestFactor: rippel.crestFactor,
|
|
454
|
+
};
|