@omnimedia/omnitool 1.1.0-107 → 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.
- package/README.md +9 -0
- package/package.json +1 -1
- package/s/timeline/renderers/parts/audio-level.ts +16 -0
- package/s/timeline/renderers/parts/samplers/audio/parts/init.ts +1 -0
- package/s/timeline/renderers/parts/samplers/audio/parts/types.ts +2 -0
- package/s/timeline/renderers/player/parts/audio-levels.ts +120 -0
- package/s/timeline/renderers/player/parts/playback.ts +22 -5
- package/s/timeline/renderers/player/parts/types.ts +26 -0
- package/s/timeline/renderers/player/player.ts +3 -0
- package/x/demo/demo.bundle.min.js +100 -100
- package/x/demo/demo.bundle.min.js.map +4 -4
- package/x/index.html +2 -2
- package/x/tests.bundle.min.js +103 -103
- package/x/tests.bundle.min.js.map +4 -4
- package/x/tests.html +1 -1
- package/x/timeline/renderers/parts/audio-level.d.ts +4 -0
- package/x/timeline/renderers/parts/audio-level.js +13 -0
- package/x/timeline/renderers/parts/audio-level.js.map +1 -0
- package/x/timeline/renderers/parts/samplers/audio/parts/init.js +1 -0
- package/x/timeline/renderers/parts/samplers/audio/parts/init.js.map +1 -1
- package/x/timeline/renderers/parts/samplers/audio/parts/types.d.ts +2 -0
- package/x/timeline/renderers/parts/samplers/audio/sampler.d.ts +1 -0
- package/x/timeline/renderers/player/parts/audio-levels.d.ts +15 -0
- package/x/timeline/renderers/player/parts/audio-levels.js +93 -0
- package/x/timeline/renderers/player/parts/audio-levels.js.map +1 -0
- package/x/timeline/renderers/player/parts/playback.d.ts +4 -1
- package/x/timeline/renderers/player/parts/playback.js +16 -5
- package/x/timeline/renderers/player/parts/playback.js.map +1 -1
- package/x/timeline/renderers/player/parts/types.d.ts +20 -0
- package/x/timeline/renderers/player/parts/types.js +2 -0
- package/x/timeline/renderers/player/parts/types.js.map +1 -0
- package/x/timeline/renderers/player/player.d.ts +4 -0
- package/x/timeline/renderers/player/player.js +2 -0
- 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
|
@@ -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
|
+
|
|
@@ -42,6 +42,7 @@ export async function initStreams(
|
|
|
42
42
|
get currentSample() {return currentSample},
|
|
43
43
|
timelineTime: () => seconds(offset + currentSample.timestamp),
|
|
44
44
|
output: () => ({
|
|
45
|
+
itemId: item.id,
|
|
45
46
|
sample: currentSample,
|
|
46
47
|
timestamp: offset + currentSample.timestamp,
|
|
47
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
|
|
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
|
-
|
|
204
|
-
|
|
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
|
|