@mekuru/audio 0.0.0-stage → 0.1.4

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/README.jp.md ADDED
@@ -0,0 +1,3 @@
1
+ # @mekuru/audio
2
+
3
+ `AudioSource`・`Audio`・`readAudio` を提供します。WAVの長さ取得・結合・PCM変換は `@mekuru/audio/wav` から利用します。coreだけに依存し、音声合成や立ち絵に関する処理は持ちません。
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Temporary Holding Version
1
+ # @mekuru/audio
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Audio resources and WAV processing.
4
+
5
+ `AudioSource`, `Audio`, and `readAudio` are exported from `@mekuru/audio`. WAV duration, joining and PCM decoding are available from `@mekuru/audio/wav`. This package depends on core, never on speech engines or narrator.
package/package.json CHANGED
@@ -1,6 +1,25 @@
1
1
  {
2
2
  "name": "@mekuru/audio",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.4",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/kanade-k-1228/mekuru.git",
7
+ "directory": "pkgs/audio"
8
+ },
9
+ "homepage": "https://github.com/kanade-k-1228/mekuru/blob/main/pkgs/audio/README.md",
10
+ "type": "module",
11
+ "exports": {
12
+ ".": "./src/index.tsx",
13
+ "./wav": "./src/wav.ts"
14
+ },
15
+ "files": [
16
+ "src"
17
+ ],
18
+ "sideEffects": false,
19
+ "dependencies": {
20
+ "@mekuru/types": "^0.1.4"
21
+ },
22
+ "engines": {
23
+ "bun": ">=1.4"
24
+ }
25
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,36 @@
1
+ /** @jsxImportSource @mekuru/types */
2
+ import { relative, resolve, sep } from "node:path";
3
+ import { fps, projectRoot, type At, type Time } from "@mekuru/types";
4
+ import { wavSeconds } from "./wav.ts";
5
+ /** A finite audio resource in the current video's frames, including trailing silence. */
6
+ export type AudioSource = Readonly<{
7
+ len: number;
8
+ label?: string;
9
+ volume?: number;
10
+ }> &
11
+ (Readonly<{ status: "draft" }> | Readonly<{ status: "ready"; src: string }>);
12
+
13
+ const frames = (seconds: number) => Math.ceil(seconds * fps() - 1e-6);
14
+
15
+ const frameCount = (name: string, value: number): number => {
16
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${name} must be a non-negative whole number of frames`);
17
+ return value;
18
+ };
19
+
20
+ export const readAudio = async (src: string, options: { len?: Time; gap?: Time; label?: string; volume?: number } = {}): Promise<AudioSource> => {
21
+ const file = resolve(projectRoot(), src);
22
+ const gap = frameCount("gap", options.gap ?? 0);
23
+ const len = options.len ?? frames(wavSeconds(Buffer.from(await Bun.file(file).arrayBuffer()), file)) + gap;
24
+ return Object.freeze({ status: "ready", src: file, len: frameCount("len", len), label: options.label, volume: options.volume });
25
+ };
26
+
27
+ export const Audio = ({ source, id, at }: { source: AudioSource; id?: string; at?: At }) => (
28
+ <dur id={id} at={at} len={source.len}>
29
+ <audio
30
+ src={source.status === "ready" ? relative(projectRoot(), source.src).split(sep).join("/") : undefined}
31
+ draft={source.status === "draft"}
32
+ volume={source.volume}
33
+ label={source.label}
34
+ />
35
+ </dur>
36
+ );
package/src/wav.ts ADDED
@@ -0,0 +1,80 @@
1
+ interface Wav {
2
+ fmt: Buffer;
3
+ data: Buffer;
4
+ }
5
+
6
+ const parseWav = (bytes: Buffer, what: string): Wav => {
7
+ if (bytes.length < 12 || bytes.toString("latin1", 0, 4) !== "RIFF" || bytes.toString("latin1", 8, 12) !== "WAVE") {
8
+ throw new Error(`${what} is not a WAV file`);
9
+ }
10
+ let fmt: Buffer | undefined;
11
+ let data: Buffer | undefined;
12
+ for (let at = 12; at + 8 <= bytes.length; ) {
13
+ const id = bytes.toString("latin1", at, at + 4);
14
+ const size = Math.min(bytes.readUInt32LE(at + 4), bytes.length - at - 8);
15
+ if (id === "fmt ") fmt = bytes.subarray(at + 8, at + 8 + size);
16
+ if (id === "data") data = bytes.subarray(at + 8, at + 8 + size);
17
+ at += 8 + size + (size % 2);
18
+ }
19
+ if (!fmt || fmt.length < 16 || !data) throw new Error(`${what} is not a WAV file (no samples)`);
20
+ return { fmt, data };
21
+ };
22
+
23
+ export const wavSeconds = (bytes: Buffer, what = "the audio"): number => {
24
+ const { fmt, data } = parseWav(bytes, what);
25
+ const bytesPerSecond = fmt.readUInt32LE(8);
26
+ if (bytesPerSecond === 0) throw new Error(`${what} is not a WAV file (no sample rate)`);
27
+ return data.length / bytesPerSecond;
28
+ };
29
+
30
+ export const joinWavs = (parts: Buffer[]): Buffer => {
31
+ const wavs = parts.map((p, i) => parseWav(p, `part ${i + 1}`));
32
+ const { fmt } = wavs[0];
33
+ if (wavs.some((w) => !w.fmt.equals(fmt))) throw new Error("the parts of the line have different audio formats");
34
+ const data = Buffer.concat(wavs.map((w) => w.data));
35
+ const head = Buffer.alloc(12 + 8 + fmt.length + (fmt.length % 2) + 8);
36
+ head.write("RIFF", 0, "latin1");
37
+ head.writeUInt32LE(head.length - 8 + data.length, 4);
38
+ head.write("WAVEfmt ", 8, "latin1");
39
+ head.writeUInt32LE(fmt.length, 16);
40
+ fmt.copy(head, 20);
41
+ head.write("data", head.length - 8, "latin1");
42
+ head.writeUInt32LE(data.length, head.length - 4);
43
+ return Buffer.concat([head, data]);
44
+ };
45
+
46
+ export const samples = (wav: Buffer): { rate: number; data: Float32Array } => {
47
+ const { fmt, data } = parseWav(wav, "the audio");
48
+ const body = data;
49
+ let format = fmt.readUInt16LE(0);
50
+ if (format === 0xfffe && fmt.length >= 26) format = fmt.readUInt16LE(24);
51
+ const channels = Math.max(1, fmt.readUInt16LE(2));
52
+ const rate = fmt.readUInt32LE(4);
53
+ const bits = fmt.readUInt16LE(14);
54
+ const bytes = bits / 8;
55
+ const read: ((at: number) => number) | undefined =
56
+ format === 3 && bits === 32
57
+ ? (at) => body.readFloatLE(at)
58
+ : format === 3 && bits === 64
59
+ ? (at) => body.readDoubleLE(at)
60
+ : format !== 1
61
+ ? undefined
62
+ : bits === 8
63
+ ? (at) => (body.readUInt8(at) - 128) / 128
64
+ : bits === 16
65
+ ? (at) => body.readInt16LE(at) / 32768
66
+ : bits === 24
67
+ ? (at) => body.readIntLE(at, 3) / 8388608
68
+ : bits === 32
69
+ ? (at) => body.readInt32LE(at) / 2147483648
70
+ : undefined;
71
+ if (!read || rate === 0) throw new Error(`the WAV format is not supported (format ${format}, ${bits} bit)`);
72
+ const n = Math.floor(data.length / (bytes * channels));
73
+ const out = new Float32Array(n);
74
+ for (let i = 0; i < n; i++) {
75
+ let sum = 0;
76
+ for (let c = 0; c < channels; c++) sum += read((i * channels + c) * bytes);
77
+ out[i] = sum / channels;
78
+ }
79
+ return { rate, data: out };
80
+ };