@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.
@@ -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
- this.audioSource = this.audioCtx.createMediaElementSource(this.audio);
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
- this.audioSource = this.audioCtx.createMediaElementSource(this.audio);
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>
@@ -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
- this.audioSource = this.audioCtx.createMediaElementSource(this.audio)
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.17",
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.17",
18
- "@94ai/nf-theme-chalk": "^8.3.17",
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": "73c9cbd0da85443cbd495c812a282676a1fa08cb"
34
+ "gitHead": "ec206eadc7ca1a279e552ac22b51c29a5e71b326"
35
35
  }
File without changes