Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103// AM の音(16kHz モノラルの10秒)を Ogg Opus にする。24kbps で約30KB だ(仕様「本番の作り」)// ブラウザの WebCodecs(AudioEncoder)で符号にし、Ogg の入れ物はここで組む
import { RATE, SLOT } from "./am.js";
const BITRATE = 24000;const SERIAL = 0x7a9e5;
export async function toOpus(buffer) { if (typeof AudioEncoder === "undefined") throw new Error("このブラウザでは録った音を Opus にできない"); const config = { codec: "opus", sampleRate: RATE, numberOfChannels: 1, bitrate: BITRATE }; if (!(await AudioEncoder.isConfigSupported(config)).supported) throw new Error("このブラウザでは録った音を Opus にできない");
const packets = []; let preSkip = 312; // 48kHz の数で。libopus の先読み6.5ms だ let failed = null; const enc = new AudioEncoder({ output: (chunk, meta) => { const d = new Uint8Array(chunk.byteLength); chunk.copyTo(d); packets.push({ data: d, samples: Math.round((chunk.duration ?? 20_000) * 48 / 1000) }); const desc = meta?.decoderConfig?.description; if (desc) { const h = new Uint8Array(desc instanceof ArrayBuffer ? desc : desc.buffer); if (String.fromCharCode(...h.slice(0, 8)) === "OpusHead") preSkip = h[10] | (h[11] << 8); } }, error: (e) => { failed = e; }, }); enc.configure(config); const pcm = buffer.getChannelData(0).slice(0, RATE * SLOT); enc.encode(new AudioData({ format: "f32-planar", sampleRate: RATE, numberOfChannels: 1, numberOfFrames: pcm.length, timestamp: 0, data: pcm })); await enc.flush(); enc.close(); if (failed) throw failed; if (!packets.length) throw new Error("Opus にできなかった"); return new Blob(ogg(packets, preSkip, pcm.length * 48000 / RATE), { type: "audio/ogg; codecs=opus" });}
// Ogg の入れ物。頭の2ページ(OpusHead・OpusTags)のあと、1秒(50パケット)ずつページにするfunction ogg(packets, preSkip, total48) { const head = new Uint8Array(19); head.set(ascii("OpusHead")); const hv = new DataView(head.buffer); hv.setUint8(8, 1); hv.setUint8(9, 1); hv.setUint16(10, preSkip, true); hv.setUint32(12, RATE, true); const vendor = ascii("tapes"); const tags = new Uint8Array(8 + 4 + vendor.length + 4); tags.set(ascii("OpusTags")); new DataView(tags.buffer).setUint32(8, vendor.length, true); tags.set(vendor, 12);
const pages = [page([head], 0, 0x02, 0), page([tags], 0, 0, 1)]; let seq = 2, granule = preSkip; const end = preSkip + total48; // 終わりの埋め草はここで切る for (let k = 0; k < packets.length; k += 50) { const group = packets.slice(k, k + 50); granule += group.reduce((s, p) => s + p.samples, 0); const last = k + 50 >= packets.length; pages.push(page(group.map((p) => p.data), last ? Math.min(granule, end) : granule, last ? 0x04 : 0, seq++)); } return pages;}
function page(datas, granule, flags, seq) { const lacing = []; for (const d of datas) { let n = d.length; while (n >= 255) { lacing.push(255); n -= 255; } lacing.push(n); } const size = datas.reduce((s, d) => s + d.length, 0); const out = new Uint8Array(27 + lacing.length + size); const v = new DataView(out.buffer); out.set(ascii("OggS")); v.setUint8(5, flags); v.setBigUint64(6, BigInt(granule), true); v.setUint32(14, SERIAL, true); v.setUint32(18, seq, true); v.setUint8(26, lacing.length); out.set(lacing, 27); let o = 27 + lacing.length; for (const d of datas) { out.set(d, o); o += d.length; } v.setUint32(22, crc(out), true); return out;}
const ascii = (s) => Uint8Array.from(s, (c) => c.charCodeAt(0));
let table;function crc(bytes) { if (!table) { table = new Uint32Array(256); for (let i = 0; i < 256; i++) { let r = i << 24; for (let j = 0; j < 8; j++) r = r & 0x80000000 ? (r << 1) ^ 0x04c11db7 : r << 1; table[i] = r >>> 0; } } let c = 0; for (const b of bytes) c = ((c << 8) ^ table[((c >>> 24) ^ b) & 255]) >>> 0; return c;}