@94ai/nf-audio 8.3.16 → 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();
@@ -296,33 +350,18 @@ const _sfc_main = {
296
350
  return window.location.protocol === "https:" || window.location.hostname === "localhost";
297
351
  },
298
352
  async startAudioCtx() {
299
- var _a, _b, _c, _d;
353
+ var _a, _b;
300
354
  if (this.isHighPrecision()) {
301
355
  try {
302
356
  if (!this.audioCtx) {
303
- const AudioCtx = window.AudioContext || window.webkitAudioContext;
304
- this.audioCtx = new AudioCtx();
357
+ this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
305
358
  if (this.audioCtx.state === "suspended") {
306
359
  await this.audioCtx.resume();
307
360
  }
361
+ await ((_a = this.audioCtx.audioWorklet) == null ? void 0 : _a.addModule("data:application/javascript," + encodeURIComponent(this.audioWorkletProcessor)));
362
+ this.processor = new AudioWorkletNode(this.audioCtx, "tick");
308
363
  this.audioSource = this.audioCtx.createMediaElementSource(this.audio);
309
- const blob = new Blob([this.audioWorkletProcessor], {
310
- type: "application/javascript"
311
- });
312
- const workletUrl = URL.createObjectURL(blob);
313
- try {
314
- await ((_a = this.audioCtx.audioWorklet) == null ? void 0 : _a.addModule(workletUrl));
315
- this.processor = new AudioWorkletNode(this.audioCtx, "tick");
316
- this.audioSource.connect(this.processor).connect(this.audioCtx.destination);
317
- } catch (workletErr) {
318
- if (this.debuglog) {
319
- console.warn("AudioWorklet unavailable, falling back to direct output:", workletErr);
320
- }
321
- this.audioSource.connect(this.audioCtx.destination);
322
- this.processor = null;
323
- } finally {
324
- URL.revokeObjectURL(workletUrl);
325
- }
364
+ this.audioSource.connect(this.processor).connect(this.audioCtx.destination);
326
365
  if ((_b = this.processor) == null ? void 0 : _b.port) {
327
366
  this.processor.port.onmessage = (event) => {
328
367
  if (event.data.command === "tick") {
@@ -340,12 +379,12 @@ const _sfc_main = {
340
379
  };
341
380
  }
342
381
  } else {
343
- await this.audioCtx.resume();
382
+ this.audioCtx.resume();
344
383
  }
345
384
  } catch (e) {
346
385
  console.log(e);
347
386
  }
348
- (_d = (_c = this.processor) == null ? void 0 : _c.port) == null ? void 0 : _d.postMessage({
387
+ this.processor.port.postMessage({
349
388
  command: "start"
350
389
  });
351
390
  }
@@ -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();
@@ -294,33 +348,18 @@ const _sfc_main = {
294
348
  return window.location.protocol === "https:" || window.location.hostname === "localhost";
295
349
  },
296
350
  async startAudioCtx() {
297
- var _a, _b, _c, _d;
351
+ var _a, _b;
298
352
  if (this.isHighPrecision()) {
299
353
  try {
300
354
  if (!this.audioCtx) {
301
- const AudioCtx = window.AudioContext || window.webkitAudioContext;
302
- this.audioCtx = new AudioCtx();
355
+ this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
303
356
  if (this.audioCtx.state === "suspended") {
304
357
  await this.audioCtx.resume();
305
358
  }
359
+ await ((_a = this.audioCtx.audioWorklet) == null ? void 0 : _a.addModule("data:application/javascript," + encodeURIComponent(this.audioWorkletProcessor)));
360
+ this.processor = new AudioWorkletNode(this.audioCtx, "tick");
306
361
  this.audioSource = this.audioCtx.createMediaElementSource(this.audio);
307
- const blob = new Blob([this.audioWorkletProcessor], {
308
- type: "application/javascript"
309
- });
310
- const workletUrl = URL.createObjectURL(blob);
311
- try {
312
- await ((_a = this.audioCtx.audioWorklet) == null ? void 0 : _a.addModule(workletUrl));
313
- this.processor = new AudioWorkletNode(this.audioCtx, "tick");
314
- this.audioSource.connect(this.processor).connect(this.audioCtx.destination);
315
- } catch (workletErr) {
316
- if (this.debuglog) {
317
- console.warn("AudioWorklet unavailable, falling back to direct output:", workletErr);
318
- }
319
- this.audioSource.connect(this.audioCtx.destination);
320
- this.processor = null;
321
- } finally {
322
- URL.revokeObjectURL(workletUrl);
323
- }
362
+ this.audioSource.connect(this.processor).connect(this.audioCtx.destination);
324
363
  if ((_b = this.processor) == null ? void 0 : _b.port) {
325
364
  this.processor.port.onmessage = (event) => {
326
365
  if (event.data.command === "tick") {
@@ -338,12 +377,12 @@ const _sfc_main = {
338
377
  };
339
378
  }
340
379
  } else {
341
- await this.audioCtx.resume();
380
+ this.audioCtx.resume();
342
381
  }
343
382
  } catch (e) {
344
383
  console.log(e);
345
384
  }
346
- (_d = (_c = this.processor) == null ? void 0 : _c.port) == null ? void 0 : _d.postMessage({
385
+ this.processor.port.postMessage({
347
386
  command: "start"
348
387
  });
349
388
  }
@@ -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();
@@ -484,29 +538,15 @@ export default {
484
538
  if (this.isHighPrecision()) {
485
539
  try {
486
540
  if (!this.audioCtx) {
487
- const AudioCtx = window.AudioContext || window.webkitAudioContext
488
- this.audioCtx = new AudioCtx()
541
+ this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
542
+ // 必须在用户交互栈中立即 resume
489
543
  if (this.audioCtx.state === 'suspended') {
490
- await this.audioCtx.resume()
544
+ await this.audioCtx.resume();
491
545
  }
546
+ await this.audioCtx.audioWorklet?.addModule('data:application/javascript,' + encodeURIComponent(this.audioWorkletProcessor))
547
+ this.processor = new AudioWorkletNode(this.audioCtx, 'tick')
492
548
  this.audioSource = this.audioCtx.createMediaElementSource(this.audio)
493
- // 使用 Blob URL 替代 data URI,提升 Safari 兼容性
494
- const blob = new Blob([this.audioWorkletProcessor], { type: 'application/javascript' })
495
- const workletUrl = URL.createObjectURL(blob)
496
- try {
497
- await this.audioCtx.audioWorklet?.addModule(workletUrl)
498
- this.processor = new AudioWorkletNode(this.audioCtx, 'tick')
499
- this.audioSource.connect(this.processor).connect(this.audioCtx.destination)
500
- } catch (workletErr) {
501
- if (this.debuglog) {
502
- console.warn('AudioWorklet unavailable, falling back to direct output:', workletErr)
503
- }
504
- // AudioWorklet 不可用时,直接连接到 destination,保证有声音
505
- this.audioSource.connect(this.audioCtx.destination)
506
- this.processor = null
507
- } finally {
508
- URL.revokeObjectURL(workletUrl)
509
- }
549
+ this.audioSource.connect(this.processor).connect(this.audioCtx.destination)
510
550
  if (this.processor?.port) {
511
551
  this.processor.port.onmessage = (event) => {
512
552
  if (event.data.command === 'tick') {
@@ -524,12 +564,12 @@ export default {
524
564
  }
525
565
  }
526
566
  } else {
527
- await this.audioCtx.resume()
567
+ this.audioCtx.resume()
528
568
  }
529
569
  } catch (e) {
530
570
  console.log(e)
531
571
  }
532
- this.processor?.port?.postMessage({ command: 'start' })
572
+ this.processor.port.postMessage({ command: 'start' })
533
573
  }
534
574
  },
535
575
  pause() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@94ai/nf-audio",
3
- "version": "8.3.16",
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.16",
18
- "@94ai/nf-theme-chalk": "^8.3.16",
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": "75e563768bd7105b7e6ac7c4ca438a318941469b"
34
+ "gitHead": "ec206eadc7ca1a279e552ac22b51c29a5e71b326"
35
35
  }
File without changes