@94ai/nf-audio 8.3.17 → 8.3.19
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/lib/nf-audio.cjs.js
CHANGED
|
@@ -121,7 +121,61 @@ const _sfc_main = {
|
|
|
121
121
|
},
|
|
122
122
|
data() {
|
|
123
123
|
return {
|
|
124
|
-
audioWorkletProcessor: `
|
|
124
|
+
audioWorkletProcessor: /Safari/i.test(navigator.userAgent) ? `
|
|
125
|
+
class TickWorkletProcessor extends AudioWorkletProcessor {
|
|
126
|
+
constructor() {
|
|
127
|
+
super();
|
|
128
|
+
this.port.onmessage = this.handleMessage.bind(this);
|
|
129
|
+
this.end = true;
|
|
130
|
+
this.sampleCount = 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
handleMessage(event) {
|
|
134
|
+
const { command, start, end } = event.data;
|
|
135
|
+
if (command === 'start') {
|
|
136
|
+
this.end = false;
|
|
137
|
+
} else if (command === 'end') {
|
|
138
|
+
this.end = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
process(inputs, outputs) {
|
|
143
|
+
// 1. 发送心跳消息(让主线程知道我还活着)
|
|
144
|
+
if (!this.end) {
|
|
145
|
+
this.port.postMessage({ command: 'tick' });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const input = inputs[0];
|
|
149
|
+
const output = outputs[0];
|
|
150
|
+
|
|
151
|
+
for (let channel = 0; channel < output.length; ++channel) {
|
|
152
|
+
const outputChannel = output[channel];
|
|
153
|
+
|
|
154
|
+
// 2. 【Safari 修复核心】
|
|
155
|
+
// 即使没有输入,也要产生极其微弱的“静音噪声”
|
|
156
|
+
// 这会强制 Safari 保持 AudioWorklet 的渲染管线开启
|
|
157
|
+
// 如果不加这个,Safari 可能会在几帧后停止调用 process
|
|
158
|
+
for (let i = 0; i < outputChannel.length; ++i) {
|
|
159
|
+
// 生成 -100dB 到 -120dB 左右的随机噪声,人耳听不见
|
|
160
|
+
// 但足以让 Safari 认为音频流是“活跃”的
|
|
161
|
+
const noise = (Math.random() * 2 - 1) * 0.00001;
|
|
162
|
+
|
|
163
|
+
// 如果有输入信号,则混合输入;如果没有输入(静音),则只输出噪声
|
|
164
|
+
if (input.length > 0 && input[channel]) {
|
|
165
|
+
outputChannel[i] = input[channel][i] + noise;
|
|
166
|
+
} else {
|
|
167
|
+
outputChannel[i] = noise;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.sampleCount += output[0].length;
|
|
173
|
+
// 3. 始终返回 true,保持节点存活
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
registerProcessor('tick', TickWorkletProcessor);
|
|
178
|
+
` : `
|
|
125
179
|
class TickWorkletProcessor extends AudioWorkletProcessor {
|
|
126
180
|
constructor() {
|
|
127
181
|
super();
|
|
@@ -301,9 +355,12 @@ const _sfc_main = {
|
|
|
301
355
|
try {
|
|
302
356
|
if (!this.audioCtx) {
|
|
303
357
|
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
304
|
-
|
|
358
|
+
if (this.audioCtx.state === "suspended") {
|
|
359
|
+
await this.audioCtx.resume();
|
|
360
|
+
}
|
|
305
361
|
await ((_a = this.audioCtx.audioWorklet) == null ? void 0 : _a.addModule("data:application/javascript," + encodeURIComponent(this.audioWorkletProcessor)));
|
|
306
362
|
this.processor = new AudioWorkletNode(this.audioCtx, "tick");
|
|
363
|
+
this.audioSource = this.audioCtx.createMediaElementSource(this.audio);
|
|
307
364
|
this.audioSource.connect(this.processor).connect(this.audioCtx.destination);
|
|
308
365
|
if ((_b = this.processor) == null ? void 0 : _b.port) {
|
|
309
366
|
this.processor.port.onmessage = (event) => {
|
|
@@ -119,7 +119,61 @@ const _sfc_main = {
|
|
|
119
119
|
},
|
|
120
120
|
data() {
|
|
121
121
|
return {
|
|
122
|
-
audioWorkletProcessor: `
|
|
122
|
+
audioWorkletProcessor: /Safari/i.test(navigator.userAgent) ? `
|
|
123
|
+
class TickWorkletProcessor extends AudioWorkletProcessor {
|
|
124
|
+
constructor() {
|
|
125
|
+
super();
|
|
126
|
+
this.port.onmessage = this.handleMessage.bind(this);
|
|
127
|
+
this.end = true;
|
|
128
|
+
this.sampleCount = 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
handleMessage(event) {
|
|
132
|
+
const { command, start, end } = event.data;
|
|
133
|
+
if (command === 'start') {
|
|
134
|
+
this.end = false;
|
|
135
|
+
} else if (command === 'end') {
|
|
136
|
+
this.end = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
process(inputs, outputs) {
|
|
141
|
+
// 1. 发送心跳消息(让主线程知道我还活着)
|
|
142
|
+
if (!this.end) {
|
|
143
|
+
this.port.postMessage({ command: 'tick' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const input = inputs[0];
|
|
147
|
+
const output = outputs[0];
|
|
148
|
+
|
|
149
|
+
for (let channel = 0; channel < output.length; ++channel) {
|
|
150
|
+
const outputChannel = output[channel];
|
|
151
|
+
|
|
152
|
+
// 2. 【Safari 修复核心】
|
|
153
|
+
// 即使没有输入,也要产生极其微弱的“静音噪声”
|
|
154
|
+
// 这会强制 Safari 保持 AudioWorklet 的渲染管线开启
|
|
155
|
+
// 如果不加这个,Safari 可能会在几帧后停止调用 process
|
|
156
|
+
for (let i = 0; i < outputChannel.length; ++i) {
|
|
157
|
+
// 生成 -100dB 到 -120dB 左右的随机噪声,人耳听不见
|
|
158
|
+
// 但足以让 Safari 认为音频流是“活跃”的
|
|
159
|
+
const noise = (Math.random() * 2 - 1) * 0.00001;
|
|
160
|
+
|
|
161
|
+
// 如果有输入信号,则混合输入;如果没有输入(静音),则只输出噪声
|
|
162
|
+
if (input.length > 0 && input[channel]) {
|
|
163
|
+
outputChannel[i] = input[channel][i] + noise;
|
|
164
|
+
} else {
|
|
165
|
+
outputChannel[i] = noise;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.sampleCount += output[0].length;
|
|
171
|
+
// 3. 始终返回 true,保持节点存活
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
registerProcessor('tick', TickWorkletProcessor);
|
|
176
|
+
` : `
|
|
123
177
|
class TickWorkletProcessor extends AudioWorkletProcessor {
|
|
124
178
|
constructor() {
|
|
125
179
|
super();
|
|
@@ -299,9 +353,12 @@ const _sfc_main = {
|
|
|
299
353
|
try {
|
|
300
354
|
if (!this.audioCtx) {
|
|
301
355
|
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
302
|
-
|
|
356
|
+
if (this.audioCtx.state === "suspended") {
|
|
357
|
+
await this.audioCtx.resume();
|
|
358
|
+
}
|
|
303
359
|
await ((_a = this.audioCtx.audioWorklet) == null ? void 0 : _a.addModule("data:application/javascript," + encodeURIComponent(this.audioWorkletProcessor)));
|
|
304
360
|
this.processor = new AudioWorkletNode(this.audioCtx, "tick");
|
|
361
|
+
this.audioSource = this.audioCtx.createMediaElementSource(this.audio);
|
|
305
362
|
this.audioSource.connect(this.processor).connect(this.audioCtx.destination);
|
|
306
363
|
if ((_b = this.processor) == null ? void 0 : _b.port) {
|
|
307
364
|
this.processor.port.onmessage = (event) => {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Play Audio in Specified Range with AudioWorklet</title>
|
|
5
|
+
</head>
|
|
6
|
+
<body>
|
|
7
|
+
<div style="display: flex;align-items: center">
|
|
8
|
+
<audio id="myAudio" src="./20170206081849_A100187_34f7e6b8-thumb.mp3" controls></audio>
|
|
9
|
+
<button onclick="playAudioInSpecifiedRange(1, 12)">Play Audio in Range 1-20 seconds</button>
|
|
10
|
+
<button onclick="endAudioInSpecifiedRange()">End Audio</button>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
let playAudioInSpecifiedRange
|
|
15
|
+
let endAudioInSpecifiedRange
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
addEventListener("DOMContentLoaded", async (event) => {
|
|
19
|
+
const audioWorkletProcessor = `
|
|
20
|
+
class TickWorkletProcessor extends AudioWorkletProcessor {
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
this.port.onmessage = this.handleMessage.bind(this);
|
|
24
|
+
this.end = true;
|
|
25
|
+
this.sampleCount = 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
handleMessage(event) {
|
|
29
|
+
const { command, start, end } = event.data;
|
|
30
|
+
if (command === 'start') {
|
|
31
|
+
this.end = false;
|
|
32
|
+
} else if (command === 'end') {
|
|
33
|
+
this.end = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
process(inputs, outputs) {
|
|
38
|
+
// 1. 发送心跳消息(让主线程知道我还活着)
|
|
39
|
+
this.port.postMessage({ command: 'tick' });
|
|
40
|
+
|
|
41
|
+
const input = inputs[0];
|
|
42
|
+
const output = outputs[0];
|
|
43
|
+
|
|
44
|
+
for (let channel = 0; channel < output.length; ++channel) {
|
|
45
|
+
const outputChannel = output[channel];
|
|
46
|
+
|
|
47
|
+
// 2. 【Safari 修复核心】
|
|
48
|
+
// 即使没有输入,也要产生极其微弱的“静音噪声”
|
|
49
|
+
// 这会强制 Safari 保持 AudioWorklet 的渲染管线开启
|
|
50
|
+
// 如果不加这个,Safari 可能会在几帧后停止调用 process
|
|
51
|
+
for (let i = 0; i < outputChannel.length; ++i) {
|
|
52
|
+
// 生成 -100dB 到 -120dB 左右的随机噪声,人耳听不见
|
|
53
|
+
// 但足以让 Safari 认为音频流是“活跃”的
|
|
54
|
+
const noise = (Math.random() * 2 - 1) * 0.00001;
|
|
55
|
+
|
|
56
|
+
// 如果有输入信号,则混合输入;如果没有输入(静音),则只输出噪声
|
|
57
|
+
if (input.length > 0 && input[channel]) {
|
|
58
|
+
outputChannel[i] = input[channel][i] + noise;
|
|
59
|
+
} else {
|
|
60
|
+
outputChannel[i] = noise;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.sampleCount += output[0].length;
|
|
66
|
+
// 3. 始终返回 true,保持节点存活
|
|
67
|
+
return !this.end;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
registerProcessor('tick', TickWorkletProcessor);
|
|
71
|
+
`
|
|
72
|
+
|
|
73
|
+
let ctx
|
|
74
|
+
let processor
|
|
75
|
+
let source
|
|
76
|
+
|
|
77
|
+
endAudioInSpecifiedRange = async () => {
|
|
78
|
+
myAudio.pause()
|
|
79
|
+
if (processor) {
|
|
80
|
+
processor.port.postMessage({ command: 'end' });
|
|
81
|
+
}
|
|
82
|
+
if (ctx && ctx.state !== 'closed') {
|
|
83
|
+
// Safari 中 suspend 有时会导致状态异常,视情况使用
|
|
84
|
+
// ctx.suspend()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
playAudioInSpecifiedRange = async (start, end) => {
|
|
89
|
+
// 1. 初始化 AudioContext (包含之前的 Safari 唤醒修复)
|
|
90
|
+
if (!ctx) {
|
|
91
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
92
|
+
ctx = new AudioContext();
|
|
93
|
+
|
|
94
|
+
// 必须在用户交互栈中立即 resume
|
|
95
|
+
if (ctx.state === 'suspended') {
|
|
96
|
+
await ctx.resume();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await ctx.audioWorklet.addModule('data:application/javascript,' + encodeURIComponent(audioWorkletProcessor));
|
|
100
|
+
|
|
101
|
+
processor = new AudioWorkletNode(ctx, 'tick');
|
|
102
|
+
// 设置 bufferSize 为最小值,减少延迟,提高 process 调用频率
|
|
103
|
+
processor.port.onmessage = (event) => {
|
|
104
|
+
if (event.data.command === 'tick') {
|
|
105
|
+
|
|
106
|
+
// 检测是否超出范围
|
|
107
|
+
if (myAudio.currentTime > end) {
|
|
108
|
+
endAudioInSpecifiedRange();
|
|
109
|
+
}
|
|
110
|
+
// 调试用:每隔一段时间打印,避免刷屏
|
|
111
|
+
console.log(myAudio.currentTime);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
source = ctx.createMediaElementSource(myAudio);
|
|
116
|
+
source.connect(processor).connect(ctx.destination);
|
|
117
|
+
} else {
|
|
118
|
+
if (ctx.state === 'suspended') {
|
|
119
|
+
await ctx.resume();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// 发送 start 命令给 Worklet
|
|
123
|
+
processor.port.postMessage({ command: 'start' });
|
|
124
|
+
// 2. 重置状态并开始
|
|
125
|
+
myAudio.currentTime = start;
|
|
126
|
+
// 3. 播放音频
|
|
127
|
+
await myAudio.play();
|
|
128
|
+
|
|
129
|
+
// // 4. 【额外保险】使用 requestAnimationFrame 监控时间
|
|
130
|
+
// // 既然 process 在 Safari 可能不可靠,我们用 RAF 作为兜底来检测结束时间
|
|
131
|
+
// const monitorLoop = () => {
|
|
132
|
+
// if (myAudio.paused || myAudio.ended) return;
|
|
133
|
+
//
|
|
134
|
+
// if (myAudio.currentTime > end) {
|
|
135
|
+
// endAudioInSpecifiedRange();
|
|
136
|
+
// } else {
|
|
137
|
+
// requestAnimationFrame(monitorLoop);
|
|
138
|
+
// }
|
|
139
|
+
// };
|
|
140
|
+
// requestAnimationFrame(monitorLoop);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
</body>
|
|
146
|
+
</html>
|
package/package/nf-audio.vue
CHANGED
|
@@ -299,7 +299,61 @@ export default {
|
|
|
299
299
|
},
|
|
300
300
|
data() {
|
|
301
301
|
return {
|
|
302
|
-
audioWorkletProcessor: `
|
|
302
|
+
audioWorkletProcessor: /Safari/i.test(navigator.userAgent) ? `
|
|
303
|
+
class TickWorkletProcessor extends AudioWorkletProcessor {
|
|
304
|
+
constructor() {
|
|
305
|
+
super();
|
|
306
|
+
this.port.onmessage = this.handleMessage.bind(this);
|
|
307
|
+
this.end = true;
|
|
308
|
+
this.sampleCount = 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
handleMessage(event) {
|
|
312
|
+
const { command, start, end } = event.data;
|
|
313
|
+
if (command === 'start') {
|
|
314
|
+
this.end = false;
|
|
315
|
+
} else if (command === 'end') {
|
|
316
|
+
this.end = true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
process(inputs, outputs) {
|
|
321
|
+
// 1. 发送心跳消息(让主线程知道我还活着)
|
|
322
|
+
if (!this.end) {
|
|
323
|
+
this.port.postMessage({ command: 'tick' });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const input = inputs[0];
|
|
327
|
+
const output = outputs[0];
|
|
328
|
+
|
|
329
|
+
for (let channel = 0; channel < output.length; ++channel) {
|
|
330
|
+
const outputChannel = output[channel];
|
|
331
|
+
|
|
332
|
+
// 2. 【Safari 修复核心】
|
|
333
|
+
// 即使没有输入,也要产生极其微弱的“静音噪声”
|
|
334
|
+
// 这会强制 Safari 保持 AudioWorklet 的渲染管线开启
|
|
335
|
+
// 如果不加这个,Safari 可能会在几帧后停止调用 process
|
|
336
|
+
for (let i = 0; i < outputChannel.length; ++i) {
|
|
337
|
+
// 生成 -100dB 到 -120dB 左右的随机噪声,人耳听不见
|
|
338
|
+
// 但足以让 Safari 认为音频流是“活跃”的
|
|
339
|
+
const noise = (Math.random() * 2 - 1) * 0.00001;
|
|
340
|
+
|
|
341
|
+
// 如果有输入信号,则混合输入;如果没有输入(静音),则只输出噪声
|
|
342
|
+
if (input.length > 0 && input[channel]) {
|
|
343
|
+
outputChannel[i] = input[channel][i] + noise;
|
|
344
|
+
} else {
|
|
345
|
+
outputChannel[i] = noise;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.sampleCount += output[0].length;
|
|
351
|
+
// 3. 始终返回 true,保持节点存活
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
registerProcessor('tick', TickWorkletProcessor);
|
|
356
|
+
` : `
|
|
303
357
|
class TickWorkletProcessor extends AudioWorkletProcessor {
|
|
304
358
|
constructor() {
|
|
305
359
|
super();
|
|
@@ -485,9 +539,13 @@ export default {
|
|
|
485
539
|
try {
|
|
486
540
|
if (!this.audioCtx) {
|
|
487
541
|
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
|
|
488
|
-
|
|
542
|
+
// 必须在用户交互栈中立即 resume
|
|
543
|
+
if (this.audioCtx.state === 'suspended') {
|
|
544
|
+
await this.audioCtx.resume();
|
|
545
|
+
}
|
|
489
546
|
await this.audioCtx.audioWorklet?.addModule('data:application/javascript,' + encodeURIComponent(this.audioWorkletProcessor))
|
|
490
547
|
this.processor = new AudioWorkletNode(this.audioCtx, 'tick')
|
|
548
|
+
this.audioSource = this.audioCtx.createMediaElementSource(this.audio)
|
|
491
549
|
this.audioSource.connect(this.processor).connect(this.audioCtx.destination)
|
|
492
550
|
if (this.processor?.port) {
|
|
493
551
|
this.processor.port.onmessage = (event) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@94ai/nf-audio",
|
|
3
|
-
"version": "8.3.
|
|
3
|
+
"version": "8.3.19",
|
|
4
4
|
"description": "> TODO: description",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"author": "liuxiangxiang <liuxiangxiang@94ai.com>",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"url": "http://94ai.gitlab.com/zoujiahe/common-ui.git"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@94ai/nf-message": "^8.3.
|
|
18
|
-
"@94ai/nf-theme-chalk": "^8.3.
|
|
17
|
+
"@94ai/nf-message": "^8.3.19",
|
|
18
|
+
"@94ai/nf-theme-chalk": "^8.3.19",
|
|
19
19
|
"vue-demi": "^0.14.5"
|
|
20
20
|
},
|
|
21
21
|
"peerDependenciesMeta": {
|
|
@@ -31,5 +31,5 @@
|
|
|
31
31
|
"types": "lib/index.d.ts",
|
|
32
32
|
"main": "lib/nf-audio.cjs.js",
|
|
33
33
|
"module": "lib/nf-audio.esm-bundler.js",
|
|
34
|
-
"gitHead": "
|
|
34
|
+
"gitHead": "ec206eadc7ca1a279e552ac22b51c29a5e71b326"
|
|
35
35
|
}
|
|
File without changes
|