@hraness/slopcamera 3.2.7 → 3.2.8

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 (67) hide show
  1. package/NOTICE.md +1 -7
  2. package/PRIVACY.md +16 -7
  3. package/README.md +37 -12
  4. package/apps/desktop/README.md +14 -130
  5. package/apps/desktop/application/context.ts +0 -9
  6. package/apps/desktop/application/default-registry.ts +0 -10
  7. package/apps/desktop/application/operation.ts +1 -5
  8. package/apps/desktop/application/operations/index.ts +0 -1
  9. package/apps/desktop/cli/args.ts +14 -86
  10. package/apps/desktop/cli/command-host-resources.ts +2 -1
  11. package/apps/desktop/cli/commands.ts +28 -67
  12. package/apps/desktop/cli/help.ts +12 -18
  13. package/apps/desktop/cli/html-scene.ts +139 -28
  14. package/apps/desktop/cli/io.ts +2 -1
  15. package/apps/desktop/cli/main.ts +23 -47
  16. package/apps/desktop/cli/menubar.ts +244 -0
  17. package/apps/desktop/cli/music-analysis-service.ts +61 -0
  18. package/apps/desktop/cli/portable-surface.ts +12 -4
  19. package/apps/desktop/code/application-node-planner.ts +0 -47
  20. package/apps/desktop/code/host-source-layout.ts +46 -0
  21. package/apps/desktop/code/public.ts +0 -1
  22. package/apps/desktop/code/runtime-identity.ts +50 -8
  23. package/apps/desktop/code/semantic-builder.ts +0 -43
  24. package/apps/desktop/code/source-bundle.ts +23 -6
  25. package/apps/desktop/code/source-typecheck.ts +131 -43
  26. package/apps/desktop/code/worker-client.ts +14 -2
  27. package/apps/desktop/core/music-analysis.ts +124 -1
  28. package/apps/desktop/dist/cli/main.js +244 -250
  29. package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
  30. package/apps/desktop/html-overlay/index.ts +9 -0
  31. package/apps/desktop/html-overlay/runtime.ts +2 -0
  32. package/apps/desktop/html-overlay/scene.ts +29 -0
  33. package/dist/cli.js +83 -81
  34. package/dist/{index-rvghzt7b.js → index-7131pg1c.js} +43 -236
  35. package/dist/{index-mcy8z0br.js → index-fava6pge.js} +1 -1
  36. package/dist/{index-z7239b4h.js → index-h1k0fnjq.js} +1 -1
  37. package/dist/{index-p63wavx0.js → index-zfnddgay.js} +1 -1
  38. package/dist/index.js +4 -18
  39. package/dist/operations.js +2 -2
  40. package/dist/vectorize/worker.js +1 -1
  41. package/dist/workflow.js +3 -3
  42. package/docs/README.md +1 -1
  43. package/docs/studio.md +1 -1
  44. package/package.json +6 -21
  45. package/skills/slopcamera/SKILL.md +8 -0
  46. package/skills/slopcamera/references/diagrams.md +5 -14
  47. package/skills/slopcamera/references/install.md +1 -1
  48. package/skills/slopcamera/references/music-video.md +23 -3
  49. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  50. package/skills/slopcamera/references/support.md +25 -0
  51. package/skills/slopcamera/references/web-media-excerpts.md +120 -0
  52. package/src/cli.ts +28 -98
  53. package/src/index.ts +0 -23
  54. package/src/support-completion.ts +16 -0
  55. package/src/support.ts +42 -0
  56. package/src/version.ts +1 -1
  57. package/apps/desktop/application/operations/recording/index.ts +0 -5
  58. package/apps/desktop/application/operations/recording/pause.ts +0 -34
  59. package/apps/desktop/application/operations/recording/resume.ts +0 -34
  60. package/apps/desktop/application/operations/recording/shared.ts +0 -199
  61. package/apps/desktop/application/operations/recording/start.ts +0 -169
  62. package/apps/desktop/application/operations/recording/stop.ts +0 -34
  63. package/apps/desktop/capture/protocol.ts +0 -689
  64. package/apps/desktop/cli/capture-bundle.ts +0 -1117
  65. package/apps/desktop/cli/recording-controller.ts +0 -1567
  66. package/apps/desktop/cli/recording-daemon.ts +0 -931
  67. package/src/desktop.ts +0 -253
@@ -89,7 +89,7 @@ function validatePcm(pcm: MonoPcm): void {
89
89
  }
90
90
  }
91
91
 
92
- function fftPowerSpectrum(samples: readonly number[]): Float64Array {
92
+ export function fftPowerSpectrum(samples: readonly number[]): Float64Array {
93
93
  let size = 1;
94
94
  while (size < samples.length) size *= 2;
95
95
  const real = new Float64Array(size);
@@ -145,6 +145,129 @@ function fftPowerSpectrum(samples: readonly number[]): Float64Array {
145
145
  return power;
146
146
  }
147
147
 
148
+ export const AUDIO_REACTIVITY_PROFILE = "bands-v1" as const;
149
+ export const AUDIO_REACTIVITY_SAMPLE_RATE_HZ = 24_000;
150
+ export const AUDIO_REACTIVITY_RATE_HZ = 60;
151
+ export const AUDIO_REACTIVITY_MAX_DURATION_US = 600_000_000;
152
+
153
+ const AUDIO_REACTIVITY_WINDOW_SAMPLES = 2_048;
154
+ const AUDIO_REACTIVITY_HOP_SAMPLES = AUDIO_REACTIVITY_SAMPLE_RATE_HZ / AUDIO_REACTIVITY_RATE_HZ;
155
+ const AUDIO_REACTIVITY_MAXIMUM_SAMPLES = AUDIO_REACTIVITY_MAX_DURATION_US * AUDIO_REACTIVITY_SAMPLE_RATE_HZ / 1_000_000;
156
+ const AUDIO_REACTIVITY_MAXIMUM_PCM_AMPLITUDE = 16;
157
+ // Suppress quiet input below -80 dBFS RMS, and band leakage below 2% of the
158
+ // simultaneous broadband RMS. Subtracting the floor avoids a hard onset step.
159
+ const AUDIO_REACTIVITY_ABSOLUTE_RMS_FLOOR = 0.0001;
160
+ const AUDIO_REACTIVITY_LEAKAGE_RMS_RATIO = 0.02;
161
+ const AUDIO_REACTIVITY_FLAT_RANGE_RATIO = 0.001;
162
+ const AUDIO_REACTIVITY_ATTACK_SECONDS = 0.03;
163
+ const AUDIO_REACTIVITY_RELEASE_SECONDS = 0.18;
164
+ // The FFT already applies a symmetric Hann window and divides power by N².
165
+ // Correct its mean squared gain when recovering one-sided RMS energy.
166
+ const AUDIO_REACTIVITY_HANN_POWER_GAIN = 3 * (AUDIO_REACTIVITY_WINDOW_SAMPLES - 1) / (8 * AUDIO_REACTIVITY_WINDOW_SAMPLES);
167
+
168
+ export interface AudioReactivityBands {
169
+ readonly bass: readonly number[];
170
+ readonly mid: readonly number[];
171
+ readonly treble: readonly number[];
172
+ readonly energy: readonly number[];
173
+ }
174
+
175
+ type MutableAudioReactivityBands = { [Band in keyof AudioReactivityBands]: number[] };
176
+ const AUDIO_REACTIVITY_BAND_NAMES = ["bass", "mid", "treble", "energy"] as const;
177
+
178
+ function audioReactivityPercentile(ordered: readonly number[], fraction: number): number {
179
+ const position = (ordered.length - 1) * fraction;
180
+ const left = Math.floor(position);
181
+ const a = ordered[left]!;
182
+ return a + ((ordered[Math.min(left + 1, ordered.length - 1)] ?? a) - a) * (position - left);
183
+ }
184
+
185
+ function normalizeAndSmoothAudioReactivity(values: readonly number[], energy: readonly number[], band: boolean): readonly number[] {
186
+ const gated = values.map((value, index) => Math.max(0, value - Math.max(
187
+ AUDIO_REACTIVITY_ABSOLUTE_RMS_FLOOR,
188
+ band ? energy[index]! * AUDIO_REACTIVITY_LEAKAGE_RMS_RATIO : 0,
189
+ )));
190
+ const ordered = [...gated].sort((left, right) => left - right);
191
+ let low = audioReactivityPercentile(ordered, 0.05);
192
+ let high = audioReactivityPercentile(ordered, 0.95);
193
+ // Events occupying less than 5% of the track still have a useful range.
194
+ if (high <= EPSILON) high = ordered.at(-1)!;
195
+ // A sustained tone has energy even when its useful percentile span is flat.
196
+ if (high - low <= Math.max(EPSILON, high * AUDIO_REACTIVITY_FLAT_RANGE_RATIO)) low = 0;
197
+ const span = high - low;
198
+ const attack = 1 - Math.exp(-1 / (AUDIO_REACTIVITY_RATE_HZ * AUDIO_REACTIVITY_ATTACK_SECONDS));
199
+ const release = 1 - Math.exp(-1 / (AUDIO_REACTIVITY_RATE_HZ * AUDIO_REACTIVITY_RELEASE_SECONDS));
200
+ let previous = 0;
201
+ const result = gated.map(value => {
202
+ const target = span <= EPSILON ? 0 : Math.min(1, Math.max(0, (value - low) / span));
203
+ previous += (target - previous) * (target > previous ? attack : release);
204
+ return Math.min(1, Math.max(0, previous));
205
+ });
206
+ // This endpoint is at the exact PCM duration, not the next regular grid tick.
207
+ result.push(0);
208
+ return Object.freeze(result);
209
+ }
210
+
211
+ /**
212
+ * Analyze already-decoded mono PCM on a fixed absolute timeline. Each band has
213
+ * ceil(sampleCount / 400) samples at i / 60 seconds, then a zero endpoint at
214
+ * durationUs. Windows are centered with zero padding; their 42.7 ms lookahead
215
+ * and offline normalization are intentional. This detects energy, not beats.
216
+ */
217
+ export function analyzeAudioReactivity(pcm: {
218
+ readonly sampleRateHz: number;
219
+ readonly samples: readonly number[] | Float32Array;
220
+ }): { readonly durationUs: number; readonly bands: AudioReactivityBands } {
221
+ if (pcm.sampleRateHz !== AUDIO_REACTIVITY_SAMPLE_RATE_HZ) {
222
+ throw new TypeError(`Audio reactivity PCM must use exactly ${AUDIO_REACTIVITY_SAMPLE_RATE_HZ} Hz.`);
223
+ }
224
+ if ((!Array.isArray(pcm.samples) && !(pcm.samples instanceof Float32Array))
225
+ || pcm.samples.length === 0 || pcm.samples.length > AUDIO_REACTIVITY_MAXIMUM_SAMPLES) {
226
+ throw new TypeError(`Audio reactivity PCM must contain 1–${AUDIO_REACTIVITY_MAXIMUM_SAMPLES} samples.`);
227
+ }
228
+ let nonzero = false;
229
+ for (let index = 0; index < pcm.samples.length; index++) {
230
+ const sample = pcm.samples[index]!;
231
+ if (!Number.isFinite(sample) || Math.abs(sample) > AUDIO_REACTIVITY_MAXIMUM_PCM_AMPLITUDE) {
232
+ throw new TypeError(`Audio reactivity PCM samples must be finite numbers within ±${AUDIO_REACTIVITY_MAXIMUM_PCM_AMPLITUDE}.`);
233
+ }
234
+ nonzero ||= sample !== 0;
235
+ }
236
+ const durationUs = Math.round(pcm.samples.length * 1_000_000 / AUDIO_REACTIVITY_SAMPLE_RATE_HZ);
237
+ const count = Math.ceil(pcm.samples.length / AUDIO_REACTIVITY_HOP_SAMPLES);
238
+ if (!nonzero) {
239
+ const silence = Object.freeze(Array<number>(count + 1).fill(0));
240
+ return Object.freeze({ durationUs, bands: Object.freeze({ bass: silence, mid: silence, treble: silence, energy: silence }) });
241
+ }
242
+ const raw: MutableAudioReactivityBands = { bass: [], mid: [], treble: [], energy: [] };
243
+ const window = Array<number>(AUDIO_REACTIVITY_WINDOW_SAMPLES).fill(0);
244
+ for (let frame = 0; frame < count; frame++) {
245
+ const start = frame * AUDIO_REACTIVITY_HOP_SAMPLES - AUDIO_REACTIVITY_WINDOW_SAMPLES / 2;
246
+ for (let index = 0; index < AUDIO_REACTIVITY_WINDOW_SAMPLES; index++) {
247
+ const source = start + index;
248
+ window[index] = source < 0 || source >= pcm.samples.length ? 0 : pcm.samples[source]!;
249
+ }
250
+ const spectrum = fftPowerSpectrum(window);
251
+ const power = { bass: 0, mid: 0, treble: 0, energy: 0 };
252
+ for (let bin = 0; bin < spectrum.length; bin++) {
253
+ const frequencyHz = bin * AUDIO_REACTIVITY_SAMPLE_RATE_HZ / AUDIO_REACTIVITY_WINDOW_SAMPLES;
254
+ const value = spectrum[bin]! * (bin === 0 || bin === spectrum.length - 1 ? 1 : 2) / AUDIO_REACTIVITY_HANN_POWER_GAIN;
255
+ power.energy += value;
256
+ if (frequencyHz >= 35 && frequencyHz < 180) power.bass += value;
257
+ else if (frequencyHz >= 180 && frequencyHz < 2_000) power.mid += value;
258
+ else if (frequencyHz >= 2_000 && frequencyHz <= 12_000) power.treble += value;
259
+ }
260
+ for (const name of AUDIO_REACTIVITY_BAND_NAMES) raw[name].push(Math.sqrt(power[name]));
261
+ }
262
+ const bands = Object.freeze({
263
+ bass: normalizeAndSmoothAudioReactivity(raw.bass, raw.energy, true),
264
+ mid: normalizeAndSmoothAudioReactivity(raw.mid, raw.energy, true),
265
+ treble: normalizeAndSmoothAudioReactivity(raw.treble, raw.energy, true),
266
+ energy: normalizeAndSmoothAudioReactivity(raw.energy, raw.energy, false),
267
+ });
268
+ return Object.freeze({ durationUs, bands });
269
+ }
270
+
148
271
  /** Fold pitched energy from C2 through B6 into twelve pitch classes. */
149
272
  export function computeChroma(frame: readonly number[], sampleRateHz: number): readonly number[] {
150
273
  if (!Number.isSafeInteger(sampleRateHz) || sampleRateHz < 8_000 || sampleRateHz > 192_000) {