visual-song 1.1.8 → 1.2.0

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/dist/index.js CHANGED
@@ -2,6 +2,64 @@ import { jsx } from 'react/jsx-runtime';
2
2
  import { forwardRef, useState, useRef, useEffect, createContext, useContext, useCallback, useImperativeHandle } from 'react';
3
3
  import VSModule from './wasm_src/visualsong-core.js';
4
4
 
5
+ // 엔진(dr_wav·dr_mp3·dr_flac)이 직접 읽는 형식. 그 밖(M4A/AAC, OGG Vorbis·Opus 등)은 브라우저
6
+ // 디코더로 PCM 을 얻어 16비트 WAV 로 감싸 엔진에 넘긴다. 코덱은 브라우저가 가진 것을 쓰므로
7
+ // 엔진에 디코더를 넣지 않아도 되고 라이선스 문제도 없다. 브라우저가 못 풀면 엔진이 거절하던 대로 실패한다.
8
+ const ENGINE_FORMAT = /\.(wav|mp3|flac)$/i;
9
+ const DECODE_SAMPLE_RATE = 44100;
10
+ function isEngineFormat(fileName) {
11
+ return ENGINE_FORMAT.test(fileName);
12
+ }
13
+ // 엔진은 모노·스테레오 WAV 를 받는다. 3채널 이상이면 앞의 두 채널만 쓴다.
14
+ function encodeWav(buffer) {
15
+ const channels = Math.min(2, buffer.numberOfChannels);
16
+ const dataBytes = buffer.length * channels * 2;
17
+ const bytes = new Uint8Array(44 + dataBytes);
18
+ const view = new DataView(bytes.buffer);
19
+ const writeText = (offset, text)=>{
20
+ for(let index = 0; index < text.length; index += 1){
21
+ view.setUint8(offset + index, text.charCodeAt(index));
22
+ }
23
+ };
24
+ writeText(0, "RIFF");
25
+ view.setUint32(4, 36 + dataBytes, true);
26
+ writeText(8, "WAVE");
27
+ writeText(12, "fmt ");
28
+ view.setUint32(16, 16, true);
29
+ view.setUint16(20, 1, true);
30
+ view.setUint16(22, channels, true);
31
+ view.setUint32(24, buffer.sampleRate, true);
32
+ view.setUint32(28, buffer.sampleRate * channels * 2, true);
33
+ view.setUint16(32, channels * 2, true);
34
+ view.setUint16(34, 16, true);
35
+ writeText(36, "data");
36
+ view.setUint32(40, dataBytes, true);
37
+ const data = Array.from({
38
+ length: channels
39
+ }, (_, index)=>buffer.getChannelData(index));
40
+ let offset = 44;
41
+ for(let frame = 0; frame < buffer.length; frame += 1){
42
+ for (const channel of data){
43
+ const sample = Math.round((channel[frame] ?? 0) * 32768);
44
+ view.setInt16(offset, Math.max(-32768, Math.min(32767, sample)), true);
45
+ offset += 2;
46
+ }
47
+ }
48
+ return bytes;
49
+ }
50
+ async function decodeToWav(encoded) {
51
+ const scope = globalThis;
52
+ const Context = scope.OfflineAudioContext ?? scope.webkitOfflineAudioContext;
53
+ if (!Context) throw new Error("This browser cannot decode this audio format.");
54
+ const context = new Context(2, 1, DECODE_SAMPLE_RATE);
55
+ // 예전 Safari 는 콜백 형식만 지원한다
56
+ const audio = await new Promise((resolve, reject)=>{
57
+ const pending = context.decodeAudioData(encoded, resolve, reject);
58
+ if (pending && typeof pending.then === "function") pending.then(resolve, reject);
59
+ });
60
+ return encodeWav(audio);
61
+ }
62
+
5
63
  const _generateUID = ()=>{
6
64
  return Date.now().toString(36) + Math.random().toString(36).substring(2);
7
65
  };
@@ -81,13 +139,26 @@ function useVisualSongAudio(file, useSinWave = false) {
81
139
  file,
82
140
  useSinWave
83
141
  ]);
142
+ // 마지막으로 고른 파일만 엔진에 넘긴다. 브라우저 디코딩이 늦게 끝나 새 파일을 덮지 않게 한다.
143
+ const loadSerialRef = useRef(0);
84
144
  const internalLoadAudioFile = (file)=>{
85
145
  if (!module) return;
146
+ const serial = ++loadSerialRef.current;
147
+ const isCurrent = ()=>loadSerialRef.current === serial;
86
148
  const reader = new FileReader();
87
149
  reader.onload = (e)=>{
88
- if (!e.target) return;
150
+ if (!e.target || !isCurrent()) return;
89
151
  const arrayBuffer = e.target.result;
90
- module.loadAudioFileFromMemory(file.name, new Uint8Array(arrayBuffer));
152
+ if (isEngineFormat(file.name)) {
153
+ module.loadAudioFileFromMemory(file.name, new Uint8Array(arrayBuffer));
154
+ return;
155
+ }
156
+ // M4A·OGG 등은 브라우저 디코더로 풀어 WAV 로 넘긴다(audioDecode.ts). decodeAudioData 는 버퍼를 떼어 가므로 사본을 준다.
157
+ void decodeToWav(arrayBuffer.slice(0)).then((wav)=>{
158
+ if (isCurrent()) module.loadAudioFileFromMemory(`${file.name}.wav`, wav);
159
+ }).catch(()=>{
160
+ if (isCurrent()) module.loadAudioFileFromMemory(file.name, new Uint8Array(arrayBuffer));
161
+ });
91
162
  };
92
163
  reader.readAsArrayBuffer(file);
93
164
  };
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visual-song",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "description": "visual-song core library",
5
5
  "author": "kunyoungparkk (rjsdud3263@gmail.com)",
6
6
  "license": "MIT",