@aelionsdk/audio 1.2.0-rc.1 → 1.2.0-rc.3

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/README.md CHANGED
@@ -8,7 +8,7 @@ Audio scheduling and deterministic PCM processing primitives for AelionSDK.
8
8
  npm install @aelionsdk/audio@next
9
9
  ```
10
10
 
11
- `next` currently resolves to `1.2.0-rc.1`. Product applications should prefer
11
+ `next` currently resolves to `1.2.0-rc.3`. Product applications should prefer
12
12
  `@aelionsdk/sdk`; use this package directly when building a custom audio host,
13
13
  analysis pipeline or renderer integration.
14
14
 
@@ -18,6 +18,8 @@ analysis pipeline or renderer integration.
18
18
  - streaming resampling and pitch-preserving time stretch;
19
19
  - SharedArrayBuffer and transferable PCM queues;
20
20
  - AudioWorklet clocks, device state and video scheduling.
21
+ - bounded beat and audio-energy change analysis. Audio analysis does not claim
22
+ pixel-based video scene detection.
21
23
 
22
24
  Queue and clock instances own browser and buffer resources. Stop producers,
23
25
  cancel pending work and dispose the owning session or primitive when playback
@@ -22,18 +22,26 @@ export interface BeatDetectionResult {
22
22
  readonly totalFrames: number;
23
23
  readonly beats: readonly BeatMarker[];
24
24
  }
25
- /** A single detected scene boundary. */
26
- export interface SceneBoundary {
27
- /** Frame where the scene change is detected. */
25
+ /** A candidate boundary inferred from a discontinuity in audio energy. */
26
+ export interface AudioEnergyBoundary {
27
+ /** Audio frame where the energy change is detected. */
28
28
  readonly frame: number;
29
29
  /** Normalized change magnitude, 0..1. */
30
30
  readonly magnitude: number;
31
31
  }
32
- /** Result of scene-boundary detection over a PCM source. */
32
+ /** Result of audio-energy change detection over a PCM source. */
33
+ export interface AudioEnergyChangeDetectionResult {
34
+ readonly sampleRate: number;
35
+ readonly totalFrames: number;
36
+ readonly changes: readonly AudioEnergyBoundary[];
37
+ }
38
+ /** @deprecated Use AudioEnergyBoundary; audio alone cannot detect video scenes. */
39
+ export type SceneBoundary = AudioEnergyBoundary;
40
+ /** @deprecated Use AudioEnergyChangeDetectionResult. */
33
41
  export interface SceneDetectionResult {
34
42
  readonly sampleRate: number;
35
43
  readonly totalFrames: number;
36
- readonly scenes: readonly SceneBoundary[];
44
+ readonly scenes: readonly AudioEnergyBoundary[];
37
45
  }
38
46
  /**
39
47
  * Detect beats from an interleaved PCM source using a bounded energy-envelope
@@ -46,13 +54,22 @@ export declare function detectBeats(options: AnalysisSource & {
46
54
  readonly minimumOnsetDb?: number;
47
55
  }): Promise<BeatDetectionResult>;
48
56
  /**
49
- * Detect scene boundaries as audio-energy discontinuities. A scene change in
50
- * video is usually accompanied by an abrupt change in the audio envelope, so a
51
- * large window-to-window energy jump is reported as a candidate boundary.
52
- * This is a deterministic, media-agnostic approximation driven by the same
53
- * readFrames source; it does not inspect video pixels.
57
+ * Detect audio-energy discontinuities as editing candidates. This function
58
+ * does not inspect video pixels and therefore deliberately makes no claim to
59
+ * detect scene boundaries.
60
+ */
61
+ export declare function detectAudioEnergyChanges(options: AnalysisSource & {
62
+ readonly minimumJumpDb?: number;
63
+ }): Promise<AudioEnergyChangeDetectionResult>;
64
+ /**
65
+ * @deprecated Audio-only input cannot detect video scenes. Use
66
+ * `detectAudioEnergyChanges` and treat the result as editing candidates.
54
67
  */
55
68
  export declare function detectScenes(options: AnalysisSource & {
56
69
  readonly minimumJumpDb?: number;
57
- }): Promise<SceneDetectionResult>;
70
+ }): Promise<{
71
+ sampleRate: number;
72
+ totalFrames: number;
73
+ scenes: readonly AudioEnergyBoundary[];
74
+ }>;
58
75
  //# sourceMappingURL=analysis.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../src/analysis.ts"],"names":[],"mappings":"AAEA,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,CACnB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD;AAED,8BAA8B;AAC9B,MAAM,WAAW,UAAU;IACzB,wBAAwB;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;CACvC;AAED,wCAAwC;AACxC,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,4DAA4D;AAC5D,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;CAC3C;AAID;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,cAAc,GAAG;IAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAAC,mBAAmB,CAAC,CAgC9B;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,cAAc,GAAG;IAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5D,OAAO,CAAC,oBAAoB,CAAC,CA6B/B"}
1
+ {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../src/analysis.ts"],"names":[],"mappings":"AAEA,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,CACnB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD;AAED,8BAA8B;AAC9B,MAAM,WAAW,UAAU;IACzB,wBAAwB;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;CACvC;AAED,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,iEAAiE;AACjE,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAC;CAClD;AAED,mFAAmF;AACnF,MAAM,MAAM,aAAa,GAAG,mBAAmB,CAAC;AAEhD,wDAAwD;AACxD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAC;CACjD;AAmBD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,cAAc,GAAG;IAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAAC,mBAAmB,CAAC,CAkC9B;AAED;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,cAAc,GAAG;IAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5D,OAAO,CAAC,gCAAgC,CAAC,CA+B3C;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG;IAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE;;;;GAO/F"}
package/dist/analysis.js CHANGED
@@ -1,5 +1,19 @@
1
1
  import { throwIfAborted } from '@aelionsdk/core';
2
2
  const MIN_CHANGE_DB = 6;
3
+ function validateSource(options, threshold, label) {
4
+ if (!Number.isSafeInteger(options.sampleRate) || options.sampleRate <= 0) {
5
+ throw new RangeError(`${label} sampleRate must be a positive safe integer`);
6
+ }
7
+ if (!Number.isSafeInteger(options.channelCount) || options.channelCount <= 0) {
8
+ throw new RangeError(`${label} channelCount must be a positive safe integer`);
9
+ }
10
+ if (!Number.isSafeInteger(options.totalFrames) || options.totalFrames < 0) {
11
+ throw new RangeError(`${label} totalFrames must be a non-negative safe integer`);
12
+ }
13
+ if (!Number.isFinite(threshold) || threshold < 0) {
14
+ throw new RangeError(`${label} threshold must be a non-negative finite number`);
15
+ }
16
+ }
3
17
  /**
4
18
  * Detect beats from an interleaved PCM source using a bounded energy-envelope
5
19
  * onset detector. The signal is processed in fixed windows, computing a local
@@ -12,6 +26,7 @@ export async function detectBeats(options) {
12
26
  const channelCount = options.channelCount;
13
27
  const totalFrames = options.totalFrames;
14
28
  const minimumOnsetDb = options.minimumOnsetDb ?? MIN_CHANGE_DB;
29
+ validateSource(options, minimumOnsetDb, 'Beat detection');
15
30
  const windowFrames = Math.max(1, Math.round(sampleRate / 20));
16
31
  const beats = [];
17
32
  let previousDb = 0;
@@ -33,28 +48,29 @@ export async function detectBeats(options) {
33
48
  const onset = db - floor > minimumOnsetDb && db > previousDb;
34
49
  if (onset && startFrame - lastBeatFrame >= windowFrames) {
35
50
  const strength = Math.min(1, Math.max(0, (db - floor) / 24));
36
- beats.push({ frame: startFrame, frameCount: windowFrames, strength });
51
+ beats.push({ frame: startFrame, frameCount, strength });
37
52
  lastBeatFrame = startFrame;
38
53
  }
39
54
  previousDb = db;
40
55
  options.onProgress?.((startFrame + frameCount) / totalFrames);
41
56
  }
57
+ if (totalFrames === 0)
58
+ options.onProgress?.(1);
42
59
  return { sampleRate, totalFrames, beats };
43
60
  }
44
61
  /**
45
- * Detect scene boundaries as audio-energy discontinuities. A scene change in
46
- * video is usually accompanied by an abrupt change in the audio envelope, so a
47
- * large window-to-window energy jump is reported as a candidate boundary.
48
- * This is a deterministic, media-agnostic approximation driven by the same
49
- * readFrames source; it does not inspect video pixels.
62
+ * Detect audio-energy discontinuities as editing candidates. This function
63
+ * does not inspect video pixels and therefore deliberately makes no claim to
64
+ * detect scene boundaries.
50
65
  */
51
- export async function detectScenes(options) {
66
+ export async function detectAudioEnergyChanges(options) {
52
67
  const sampleRate = options.sampleRate;
53
68
  const channelCount = options.channelCount;
54
69
  const totalFrames = options.totalFrames;
55
70
  const minimumJumpDb = options.minimumJumpDb ?? MIN_CHANGE_DB;
71
+ validateSource(options, minimumJumpDb, 'Audio energy analysis');
56
72
  const windowFrames = Math.max(1, Math.round(sampleRate / 10));
57
- const scenes = [];
73
+ const changes = [];
58
74
  let previousDb = 0;
59
75
  for (let startFrame = 0; startFrame < totalFrames; startFrame += windowFrames) {
60
76
  throwIfAborted(options.signal, 'Scene detection');
@@ -71,11 +87,25 @@ export async function detectScenes(options) {
71
87
  if (startFrame > 0) {
72
88
  const jump = Math.abs(db - previousDb);
73
89
  if (jump >= minimumJumpDb) {
74
- scenes.push({ frame: startFrame, magnitude: Math.min(1, jump / 24) });
90
+ changes.push({ frame: startFrame, magnitude: Math.min(1, jump / 24) });
75
91
  }
76
92
  }
77
93
  previousDb = db;
78
94
  options.onProgress?.((startFrame + frameCount) / totalFrames);
79
95
  }
80
- return { sampleRate, totalFrames, scenes };
96
+ if (totalFrames === 0)
97
+ options.onProgress?.(1);
98
+ return { sampleRate, totalFrames, changes };
99
+ }
100
+ /**
101
+ * @deprecated Audio-only input cannot detect video scenes. Use
102
+ * `detectAudioEnergyChanges` and treat the result as editing candidates.
103
+ */
104
+ export async function detectScenes(options) {
105
+ const result = await detectAudioEnergyChanges(options);
106
+ return {
107
+ sampleRate: result.sampleRate,
108
+ totalFrames: result.totalFrames,
109
+ scenes: result.changes,
110
+ };
81
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aelionsdk/audio",
3
- "version": "1.2.0-rc.1",
3
+ "version": "1.2.0-rc.3",
4
4
  "description": "AudioWorklet clock, PCM buffering and audio mixing for AelionSDK",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,8 +34,8 @@
34
34
  "provenance": true
35
35
  },
36
36
  "dependencies": {
37
- "@aelionsdk/core": "1.2.0-rc.1",
38
- "@aelionsdk/render-ir": "1.2.0-rc.1"
37
+ "@aelionsdk/core": "1.2.0-rc.3",
38
+ "@aelionsdk/render-ir": "1.2.0-rc.3"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -b",