@omnimedia/omnitool 1.1.0-106 → 1.1.0-108

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 (34) hide show
  1. package/README.md +9 -0
  2. package/package.json +1 -1
  3. package/s/timeline/renderers/parts/audio-level.ts +16 -0
  4. package/s/timeline/renderers/parts/samplers/audio/parts/init.ts +5 -1
  5. package/s/timeline/renderers/parts/samplers/audio/parts/types.ts +2 -0
  6. package/s/timeline/renderers/player/parts/audio-levels.ts +120 -0
  7. package/s/timeline/renderers/player/parts/playback.ts +22 -5
  8. package/s/timeline/renderers/player/parts/types.ts +26 -0
  9. package/s/timeline/renderers/player/player.ts +3 -0
  10. package/x/demo/demo.bundle.min.js +100 -100
  11. package/x/demo/demo.bundle.min.js.map +4 -4
  12. package/x/index.html +2 -2
  13. package/x/tests.bundle.min.js +103 -103
  14. package/x/tests.bundle.min.js.map +4 -4
  15. package/x/tests.html +1 -1
  16. package/x/timeline/renderers/parts/audio-level.d.ts +4 -0
  17. package/x/timeline/renderers/parts/audio-level.js +13 -0
  18. package/x/timeline/renderers/parts/audio-level.js.map +1 -0
  19. package/x/timeline/renderers/parts/samplers/audio/parts/init.js +5 -1
  20. package/x/timeline/renderers/parts/samplers/audio/parts/init.js.map +1 -1
  21. package/x/timeline/renderers/parts/samplers/audio/parts/types.d.ts +2 -0
  22. package/x/timeline/renderers/parts/samplers/audio/sampler.d.ts +1 -0
  23. package/x/timeline/renderers/player/parts/audio-levels.d.ts +15 -0
  24. package/x/timeline/renderers/player/parts/audio-levels.js +93 -0
  25. package/x/timeline/renderers/player/parts/audio-levels.js.map +1 -0
  26. package/x/timeline/renderers/player/parts/playback.d.ts +4 -1
  27. package/x/timeline/renderers/player/parts/playback.js +16 -5
  28. package/x/timeline/renderers/player/parts/playback.js.map +1 -1
  29. package/x/timeline/renderers/player/parts/types.d.ts +20 -0
  30. package/x/timeline/renderers/player/parts/types.js +2 -0
  31. package/x/timeline/renderers/player/parts/types.js.map +1 -0
  32. package/x/timeline/renderers/player/player.d.ts +4 -0
  33. package/x/timeline/renderers/player/player.js +2 -0
  34. package/x/timeline/renderers/player/player.js.map +1 -1
package/README.md CHANGED
@@ -533,6 +533,15 @@ Notes:
533
533
  - Call `player.resize(width, height)` to resize the preview canvas.
534
534
  - `playbackRate` supports slower, faster, and reverse visual playback. Audio currently plays only at `1`.
535
535
 
536
+ Subscribe to the mixed, post-gain level of selected audio items - for clip, group, or master meters:
537
+
538
+ ```ts
539
+ const off = player.audio.levels.on(
540
+ () => itemIds,
541
+ ({time, peak, rms}) => {}
542
+ )
543
+ ```
544
+
536
545
  ## 📤 Export
537
546
 
538
547
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnimedia/omnitool",
3
- "version": "1.1.0-106",
3
+ "version": "1.1.0-108",
4
4
  "description": "open source video processing tools",
5
5
  "license": "MIT",
6
6
  "author": "Przemysław Gałęzki",
@@ -0,0 +1,16 @@
1
+
2
+ export function measureAudioLevel(samples: Float32Array<ArrayBufferLike>) {
3
+ let peak = 0
4
+ let sumSquares = 0
5
+
6
+ for (const value of samples) {
7
+ peak = Math.max(peak, Math.abs(value))
8
+ sumSquares += value ** 2
9
+ }
10
+
11
+ return {
12
+ peak,
13
+ rms: samples.length > 0 ? Math.sqrt(sumSquares / samples.length) : 0
14
+ }
15
+ }
16
+
@@ -17,14 +17,17 @@ export async function initStreams(
17
17
  return
18
18
  if (item.enabled === false)
19
19
  return
20
+ if (localTime >= item.duration)
21
+ return
20
22
 
21
23
  const sink = await pool.getSink(item.mediaHash)
22
24
  if (!sink)
23
25
  return
24
26
 
25
27
  const mediaTime = item.start + localTime
28
+ const mediaEnd = item.start + item.duration
26
29
  const offset = seconds((from - mediaTime) / 1000)
27
- const iter = sink.samples(mediaTime / 1000)
30
+ const iter = sink.samples(mediaTime / 1000, mediaEnd / 1000)
28
31
 
29
32
  const first = await iter.next()
30
33
  if (first.done)
@@ -39,6 +42,7 @@ export async function initStreams(
39
42
  get currentSample() {return currentSample},
40
43
  timelineTime: () => seconds(offset + currentSample.timestamp),
41
44
  output: () => ({
45
+ itemId: item.id,
42
46
  sample: currentSample,
43
47
  timestamp: offset + currentSample.timestamp,
44
48
  gain: item.gain ?? 1
@@ -1,5 +1,6 @@
1
1
 
2
2
  import {AudioSample} from "mediabunny"
3
+ import {Id} from "../../../../../parts/basics.js"
3
4
  import {Seconds} from "../../../../../../units/seconds.js"
4
5
 
5
6
  export type ActiveStream = {
@@ -8,6 +9,7 @@ export type ActiveStream = {
8
9
  currentSample: AudioSample
9
10
  timelineTime: () => Seconds
10
11
  output: () => {
12
+ itemId: Id
11
13
  sample: AudioSample
12
14
  timestamp: number
13
15
  gain: number
@@ -0,0 +1,120 @@
1
+
2
+ import {Ms} from '../../../../units/ms.js'
3
+ import {Id} from '../../../parts/basics.js'
4
+ import {measureAudioLevel} from '../../parts/audio-level.js'
5
+ import {AudioItemSelector, AudioLevelListener, AudioLevelObserver, Meter} from './types.js'
6
+
7
+ export class AudioLevels implements AudioLevelObserver {
8
+ #meters = new Set<Meter>()
9
+ #items = new Map<GainNode, Id>()
10
+ #frame: number | null = null
11
+ #running = false
12
+
13
+ constructor(
14
+ private context: AudioContext,
15
+ private time: () => Ms,
16
+ private isPlaying: () => boolean
17
+ ) {}
18
+
19
+ on(items: AudioItemSelector, listener: AudioLevelListener) {
20
+ const silent = this.context.createGain()
21
+ const analyser = this.context.createAnalyser()
22
+
23
+ silent.gain.value = 0
24
+ analyser.smoothingTimeConstant = 0
25
+
26
+ analyser.connect(silent)
27
+ silent.connect(this.context.destination)
28
+
29
+ const meter: Meter = {
30
+ items,
31
+ silent,
32
+ listener,
33
+ analyser,
34
+ nodes: new Set(),
35
+ samples: new Float32Array(analyser.fftSize) as Float32Array<ArrayBuffer>
36
+ }
37
+
38
+ this.#meters.add(meter)
39
+
40
+ if (this.isPlaying())
41
+ this.start()
42
+
43
+ return () => {
44
+ if (!this.#meters.delete(meter))
45
+ return
46
+
47
+ for (const node of meter.nodes)
48
+ node.disconnect(analyser)
49
+
50
+ analyser.disconnect()
51
+ silent.disconnect()
52
+
53
+ if (!this.#meters.size)
54
+ this.stop()
55
+ }
56
+ }
57
+
58
+ attach(itemId: Id, node: GainNode) {
59
+ this.#items.set(node, itemId)
60
+ }
61
+
62
+ detach(node: GainNode) {
63
+ this.#items.delete(node)
64
+
65
+ for (const meter of this.#meters) {
66
+ if (meter.nodes.delete(node))
67
+ node.disconnect(meter.analyser)
68
+ }
69
+ }
70
+
71
+ start() {
72
+ if (this.#running || !this.#meters.size)
73
+ return
74
+
75
+ this.#running = true
76
+ this.#frame = requestAnimationFrame(() => this.#tick())
77
+ }
78
+
79
+ stop() {
80
+ this.#running = false
81
+
82
+ if (this.#frame !== null)
83
+ cancelAnimationFrame(this.#frame)
84
+
85
+ this.#frame = null
86
+ }
87
+
88
+ #tick() {
89
+ if (!this.#running)
90
+ return
91
+
92
+ for (const meter of this.#meters) {
93
+ this.#sync(meter)
94
+ meter.analyser.getFloatTimeDomainData(meter.samples)
95
+ meter.listener({time: this.time(), ...measureAudioLevel(meter.samples)})
96
+ }
97
+
98
+ if (this.#running)
99
+ this.#frame = requestAnimationFrame(() => this.#tick())
100
+ }
101
+
102
+ #sync(meter: Meter) {
103
+ const selected = new Set(meter.items())
104
+
105
+ for (const [node, itemId] of this.#items) {
106
+ const connected = meter.nodes.has(node)
107
+ const shouldConnect = selected.has(itemId)
108
+
109
+ if (shouldConnect && !connected) {
110
+ node.connect(meter.analyser)
111
+ meter.nodes.add(node)
112
+ }
113
+ else if (!shouldConnect && connected) {
114
+ node.disconnect(meter.analyser)
115
+ meter.nodes.delete(node)
116
+ }
117
+ }
118
+ }
119
+ }
120
+
@@ -1,4 +1,5 @@
1
1
 
2
+ import {AudioLevels} from "./audio-levels.js"
2
3
  import {Fps} from '../../../../units/fps.js'
3
4
  import {ms, Ms} from '../../../../units/ms.js'
4
5
  import {Driver} from '../../../../driver/driver.js'
@@ -12,6 +13,7 @@ import {createAudioSampler} from '../../parts/samplers/audio/sampler.js'
12
13
  import {createVisualSampler} from '../../parts/samplers/visual/sampler.js'
13
14
 
14
15
  export class Playback {
16
+ audioLevels
15
17
  audioSampler
16
18
  seekVisualSampler
17
19
  playVisualSampler: CursorVisualSampler | null = null
@@ -26,7 +28,7 @@ export class Playback {
26
28
 
27
29
  audioContext = new AudioContext({sampleRate: 48000})
28
30
  audioGain = this.audioContext.createGain()
29
- audioNodes = new Set<AudioBufferSourceNode>()
31
+ audioNodes = new Map<AudioBufferSourceNode, GainNode>()
30
32
  #audioAbort: AbortController | null = null
31
33
 
32
34
  constructor(
@@ -36,6 +38,11 @@ export class Playback {
36
38
  ) {
37
39
  this.audioGain.connect(this.audioContext.destination)
38
40
  this.audioGain.gain.value = 0.7 ** 2
41
+ this.audioLevels = new AudioLevels(
42
+ this.audioContext,
43
+ () => this.currentTime,
44
+ () => this.#controller.isPlaying()
45
+ )
39
46
  this.seekVisualSampler = createVisualSampler(this.resolveMedia)
40
47
  this.audioSampler = createAudioSampler(this.resolveMedia)
41
48
  this.#samples()
@@ -88,12 +95,14 @@ export class Playback {
88
95
  this.reversePlayVisualSampler = new ReverseCursorVisualSampler(this.driver, this.resolveMedia, this.timeline)
89
96
 
90
97
  this.#controller.play()
98
+ this.audioLevels.start()
91
99
  this.#startAudio()
92
100
  }
93
101
 
94
102
  pause() {
95
103
  this.#playbackStart = this.currentTime
96
104
  this.#controller.pause()
105
+ this.audioLevels.stop()
97
106
  this.#stopAudio()
98
107
 
99
108
  if (this.playVisualSampler) {
@@ -165,8 +174,10 @@ export class Playback {
165
174
  this.#audioAbort?.abort()
166
175
  this.#audioAbort = null
167
176
 
168
- for (const node of this.audioNodes)
177
+ for (const [node, gain] of this.audioNodes) {
169
178
  node.stop()
179
+ this.audioLevels.detach(gain)
180
+ }
170
181
 
171
182
  this.audioNodes.clear()
172
183
  }
@@ -184,7 +195,7 @@ export class Playback {
184
195
  if (this.#audioStartSec === null)
185
196
  return
186
197
 
187
- for await (const {sample, timestamp, gain} of this.audioSampler.sampleAudio(
198
+ for await (const {itemId, sample, timestamp, gain} of this.audioSampler.sampleAudio(
188
199
  this.timeline, ms(from * 1000)
189
200
  )) {
190
201
 
@@ -200,8 +211,14 @@ export class Playback {
200
211
  itemGain.gain.value = gain
201
212
  node.connect(itemGain)
202
213
  itemGain.connect(this.audioGain)
203
- node.onended = () => this.audioNodes.delete(node)
204
- this.audioNodes.add(node)
214
+
215
+ node.onended = () => {
216
+ this.audioNodes.delete(node)
217
+ this.audioLevels.detach(itemGain)
218
+ }
219
+
220
+ this.audioNodes.set(node, itemGain)
221
+ this.audioLevels.attach(itemId, itemGain)
205
222
 
206
223
  const startAt = this.#audioStartSec + timestamp - from
207
224
 
@@ -0,0 +1,26 @@
1
+
2
+ import {Ms} from "../../../../units/ms.js"
3
+ import {Id} from "../../../parts/basics.js"
4
+
5
+ export type AudioLevel = {
6
+ time: Ms
7
+ peak: number
8
+ rms: number
9
+ }
10
+
11
+ export type AudioItemSelector = () => Iterable<Id>
12
+ export type AudioLevelListener = (level: AudioLevel) => void
13
+
14
+ export type AudioLevelObserver = {
15
+ on(items: AudioItemSelector, listener: AudioLevelListener): () => void
16
+ }
17
+
18
+ export type Meter = {
19
+ items: AudioItemSelector
20
+ listener: AudioLevelListener
21
+ analyser: AnalyserNode
22
+ silent: GainNode
23
+ samples: Float32Array<ArrayBuffer>
24
+ nodes: Set<GainNode>
25
+ }
26
+
@@ -4,6 +4,7 @@ import {fps} from "../../../units/fps.js"
4
4
  import {Playback} from "./parts/playback.js"
5
5
  import {Driver} from "../../../driver/driver.js"
6
6
  import {TimelineFile} from "../../parts/basics.js"
7
+ import {AudioLevelObserver} from "./parts/types.js"
7
8
  import {DecoderSource} from "../../../driver/fns/schematic.js"
8
9
 
9
10
  type ResolveMedia = (hash: string) => DecoderSource
@@ -11,6 +12,7 @@ type ResolveMedia = (hash: string) => DecoderSource
11
12
  export class VideoPlayer {
12
13
  canvas: HTMLCanvasElement
13
14
  playback: Playback
15
+ readonly audio: {levels: AudioLevelObserver}
14
16
 
15
17
  #pendingSeek: number | null = null
16
18
  #flushTask: Promise<void> | null = null
@@ -21,6 +23,7 @@ export class VideoPlayer {
21
23
  timeline: TimelineFile
22
24
  ) {
23
25
  this.playback = new Playback(driver, timeline, resolveMedia)
26
+ this.audio = {levels: this.playback.audioLevels}
24
27
  this.canvas = driver.compositor.pixi.renderer.canvas
25
28
  }
26
29