castad-pre-v0.3/castad-data/services/audioUtils.ts

144 lines
6.4 KiB
TypeScript

/// <reference lib="dom" />
/**
* Base64로 인코딩된 오디오 데이터를 Uint8Array로 디코딩하는 유틸리티 함수입니다.
* @param {string} base64 - Base64 인코딩된 문자열 (data URI 접두사 없이 순수 데이터)
* @returns {Uint8Array} - 디코딩된 바이트 배열
*/
export function decodeBase64(base64: string): Uint8Array {
// `atob` 함수를 사용하여 Base64 문자열을 이진 문자열로 디코딩합니다.
const binaryString = atob(base64);
const len = binaryString.length;
// 디코딩된 바이트를 저장할 Uint8Array를 생성합니다.
const bytes = new Uint8Array(len);
// 각 문자의 ASCII 코드를 바이트 배열에 저장합니다.
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
/**
* PCM 오디오 데이터를 AudioBuffer 객체로 디코딩하는 비동기 함수입니다.
* 웹 오디오 API를 사용하여 브라우저에서 오디오를 재생하거나 처리하기 위해 사용됩니다.
*
* @param {Uint8Array} data - 디코딩할 PCM 오디오 데이터 (Uint8Array 형식)
* @param {AudioContext} ctx - 현재 사용 중인 AudioContext 인스턴스
* @param {number} sampleRate - 오디오 샘플 레이트 (기본값: 24000 Hz)
* @param {number} numChannels - 오디오 채널 수 (기본값: 1, 모노)
* @returns {Promise<AudioBuffer>} - 디코딩된 AudioBuffer 객체
*/
export async function decodeAudioData(
data: Uint8Array,
ctx: AudioContext,
sampleRate: number = 24000,
numChannels: number = 1,
): Promise<AudioBuffer> {
// 16비트 정렬을 확인하고 필요한 경우 버퍼 크기를 조정합니다.
// (createBuffer는 짝수 길이의 버퍼를 선호할 수 있습니다.)
let buffer = data.buffer;
if (buffer.byteLength % 2 !== 0) {
const newBuffer = new ArrayBuffer(buffer.byteLength + 1);
new Uint8Array(newBuffer).set(data);
buffer = newBuffer;
}
// Int16Array로 데이터를 해석하여 16비트 PCM 데이터를 처리합니다.
const dataInt16 = new Int16Array(buffer);
// 총 프레임 수를 계산합니다 (샘플 수 / 채널 수).
const frameCount = dataInt16.length / numChannels;
// AudioBuffer를 생성합니다. (채널 수, 프레임 수, 샘플 레이트)
const audioBuffer = ctx.createBuffer(numChannels, frameCount, sampleRate);
// 각 오디오 채널에 대해 데이터를 처리합니다.
for (let channel = 0; channel < numChannels; channel++) {
// 현재 채널의 데이터를 가져옵니다 (Float32Array).
const channelData = audioBuffer.getChannelData(channel);
for (let i = 0; i < frameCount; i++) {
// Int16 값을 Float32 범위 [-1.0, 1.0]으로 변환합니다.
// 16비트 부호 있는 정수(short)의 최대값은 32767이므로 이 값으로 나눕니다.
channelData[i] = dataInt16[i * numChannels + channel] / 32768.0;
}
}
return audioBuffer;
}
/**
* AudioBuffer 객체를 WAV 형식의 Blob으로 변환하는 함수입니다.
* 이렇게 생성된 Blob은 HTML <audio> 태그에서 직접 재생할 수 있습니다.
*
* @param {AudioBuffer} abuffer - WAV로 변환할 AudioBuffer 객체
* @param {number} len - AudioBuffer의 총 샘플 길이 (프레임 수)
* @returns {Blob} - WAV 형식의 오디오 데이터를 담은 Blob 객체
*/
export function bufferToWaveBlob(abuffer: AudioBuffer, len: number): Blob {
const numOfChan = abuffer.numberOfChannels; // 채널 수 (예: 1=모노, 2=스테레오)
// WAV 파일의 총 길이를 계산합니다. (데이터 길이 + 헤더 길이)
// (샘플 수 * 채널 수 * 2 바이트/샘플 (16비트) + 44 바이트 (WAV 헤더))
const length = len * numOfChan * 2 + 44;
const buffer = new ArrayBuffer(length); // 전체 WAV 파일 크기의 ArrayBuffer
const view = new DataView(buffer); // 데이터를 쓰기 위한 DataView
const channels = []; // 각 채널의 데이터를 저장할 배열
let i;
let sample;
let offset = 0; // 현재 읽고 있는 샘플 오프셋
let pos = 0; // DataView에 쓰는 현재 위치
// 16비트 정수를 DataView에 쓰는 헬퍼 함수
function setUint16(data: number) {
view.setUint16(pos, data, true); // little-endian
pos += 2;
}
// 32비트 정수를 DataView에 쓰는 헬퍼 함수
function setUint32(data: number) {
view.setUint32(pos, data, true); // little-endian
pos += 4;
}
// --- WAV 헤더 작성 ---
setUint32(0x46464952); // "RIFF" chunk ID
setUint32(length - 8); // ChunkSize (파일 길이 - 8 바이트)
setUint32(0x45564157); // "WAVE" format
setUint32(0x20746d66); // "fmt " sub-chunk ID
setUint32(16); // Subchunk1Size (fmt 서브 청크 길이 = 16 바이트)
setUint16(1); // AudioFormat (PCM = 1)
setUint16(numOfChan); // NumChannels
setUint32(abuffer.sampleRate); // SampleRate
setUint32(abuffer.sampleRate * 2 * numOfChan); // ByteRate (SampleRate * NumChannels * BitsPerSample/8)
setUint16(numOfChan * 2); // BlockAlign (NumChannels * BitsPerSample/8)
setUint16(16); // BitsPerSample (현재 예제에서는 16비트로 고정)
setUint32(0x61746164); // "data" sub-chunk ID
setUint32(length - pos - 4); // Subchunk2Size (데이터 길이)
// --- 인터리브된 오디오 데이터 작성 ---
// 각 채널의 오디오 데이터를 배열에 저장
for (i = 0; i < abuffer.numberOfChannels; i++)
channels.push(abuffer.getChannelData(i));
// 각 샘플을 순회하며 채널 데이터를 인터리브하여 DataView에 작성
while (offset < len) {
for (i = 0; i < numOfChan; i++) {
// DataView에 쓰기 전 안전한 경계 검사
if (pos + 2 > length) break;
// 채널 인터리브 (예: 좌, 우, 좌, 우 ...)
const channel = channels[i];
// 채널 데이터에서 현재 오프셋의 샘플을 가져옵니다. (안전하게 접근)
const s = channel && offset < channel.length ? channel[offset] : 0;
// 샘플 값을 -1에서 1 사이로 클램프(clamp)합니다.
sample = Math.max(-1, Math.min(1, s));
// 16비트 부호 있는 정수(-32768 ~ 32767) 스케일로 변환합니다.
sample = (sample < 0 ? sample * 0x8000 : sample * 0x7FFF) | 0;
view.setInt16(pos, sample, true); // 16비트 샘플 작성 (little-endian)
pos += 2;
}
offset++; // 다음 원본 샘플로 이동
}
// 최종적으로 WAV Blob을 생성하여 반환합니다.
return new Blob([buffer], { type: 'audio/wav' });
}