|
|
@@ -1,156 +0,0 @@
|
|
|
-import { Player } from "cli-sound"
|
|
|
-import { mkdirSync } from "node:fs"
|
|
|
-import { tmpdir } from "node:os"
|
|
|
-import { basename, join } from "node:path"
|
|
|
-import { Process } from "@/util/process"
|
|
|
-import { which } from "@/util/which"
|
|
|
-import pulseA from "../asset/pulse-a.wav" with { type: "file" }
|
|
|
-import pulseB from "../asset/pulse-b.wav" with { type: "file" }
|
|
|
-import pulseC from "../asset/pulse-c.wav" with { type: "file" }
|
|
|
-import charge from "../asset/charge.wav" with { type: "file" }
|
|
|
-
|
|
|
-const FILE = [pulseA, pulseB, pulseC]
|
|
|
-
|
|
|
-const HUM = charge
|
|
|
-const DIR = join(tmpdir(), "opencode-sfx")
|
|
|
-
|
|
|
-const LIST = [
|
|
|
- "ffplay",
|
|
|
- "mpv",
|
|
|
- "mpg123",
|
|
|
- "mpg321",
|
|
|
- "mplayer",
|
|
|
- "afplay",
|
|
|
- "play",
|
|
|
- "omxplayer",
|
|
|
- "aplay",
|
|
|
- "cmdmp3",
|
|
|
- "cvlc",
|
|
|
- "powershell.exe",
|
|
|
-] as const
|
|
|
-
|
|
|
-type Kind = (typeof LIST)[number]
|
|
|
-
|
|
|
-function args(kind: Kind, file: string, volume: number) {
|
|
|
- if (kind === "ffplay") return [kind, "-autoexit", "-nodisp", "-af", `volume=${volume}`, file]
|
|
|
- if (kind === "mpv")
|
|
|
- return [kind, "--no-video", "--audio-display=no", "--volume", String(Math.round(volume * 100)), file]
|
|
|
- if (kind === "mpg123" || kind === "mpg321") return [kind, "-g", String(Math.round(volume * 100)), file]
|
|
|
- if (kind === "mplayer") return [kind, "-vo", "null", "-volume", String(Math.round(volume * 100)), file]
|
|
|
- if (kind === "afplay" || kind === "omxplayer" || kind === "aplay" || kind === "cmdmp3") return [kind, file]
|
|
|
- if (kind === "play") return [kind, "-v", String(volume), file]
|
|
|
- if (kind === "cvlc") return [kind, `--gain=${volume}`, "--play-and-exit", file]
|
|
|
- return [kind, "-c", `(New-Object Media.SoundPlayer '${file.replace(/'/g, "''")}').PlaySync()`]
|
|
|
-}
|
|
|
-
|
|
|
-let item: Player | null | undefined
|
|
|
-let kind: Kind | null | undefined
|
|
|
-let proc: Process.Child | undefined
|
|
|
-let tail: ReturnType<typeof setTimeout> | undefined
|
|
|
-let cache: Promise<{ hum: string; pulse: string[] }> | undefined
|
|
|
-let seq = 0
|
|
|
-let shot = 0
|
|
|
-
|
|
|
-function load() {
|
|
|
- if (item !== undefined) return item
|
|
|
- try {
|
|
|
- item = new Player({ volume: 0.35 })
|
|
|
- } catch {
|
|
|
- item = null
|
|
|
- }
|
|
|
- return item
|
|
|
-}
|
|
|
-
|
|
|
-async function file(path: string) {
|
|
|
- mkdirSync(DIR, { recursive: true })
|
|
|
- const next = join(DIR, basename(path))
|
|
|
- const out = Bun.file(next)
|
|
|
- if (await out.exists()) return next
|
|
|
- await Bun.write(out, Bun.file(path))
|
|
|
- return next
|
|
|
-}
|
|
|
-
|
|
|
-function asset() {
|
|
|
- cache ??= Promise.all([file(HUM), Promise.all(FILE.map(file))]).then(([hum, pulse]) => ({ hum, pulse }))
|
|
|
- return cache
|
|
|
-}
|
|
|
-
|
|
|
-function pick() {
|
|
|
- if (kind !== undefined) return kind
|
|
|
- kind = LIST.find((item) => which(item)) ?? null
|
|
|
- return kind
|
|
|
-}
|
|
|
-
|
|
|
-function run(file: string, volume: number) {
|
|
|
- const kind = pick()
|
|
|
- if (!kind) return
|
|
|
- return Process.spawn(args(kind, file, volume), {
|
|
|
- stdin: "ignore",
|
|
|
- stdout: "ignore",
|
|
|
- stderr: "ignore",
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-function clear() {
|
|
|
- if (!tail) return
|
|
|
- clearTimeout(tail)
|
|
|
- tail = undefined
|
|
|
-}
|
|
|
-
|
|
|
-function play(file: string, volume: number) {
|
|
|
- const item = load()
|
|
|
- if (!item) return run(file, volume)?.exited
|
|
|
- return item.play(file, { volume }).catch(() => run(file, volume)?.exited)
|
|
|
-}
|
|
|
-
|
|
|
-export function start() {
|
|
|
- stop()
|
|
|
- const id = ++seq
|
|
|
- void asset().then(({ hum }) => {
|
|
|
- if (id !== seq) return
|
|
|
- const next = run(hum, 0.24)
|
|
|
- if (!next) return
|
|
|
- proc = next
|
|
|
- void next.exited.then(
|
|
|
- () => {
|
|
|
- if (id !== seq) return
|
|
|
- if (proc === next) proc = undefined
|
|
|
- },
|
|
|
- () => {
|
|
|
- if (id !== seq) return
|
|
|
- if (proc === next) proc = undefined
|
|
|
- },
|
|
|
- )
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-export function stop(delay = 0) {
|
|
|
- seq++
|
|
|
- clear()
|
|
|
- if (!proc) return
|
|
|
- const next = proc
|
|
|
- if (delay <= 0) {
|
|
|
- proc = undefined
|
|
|
- void Process.stop(next).catch(() => undefined)
|
|
|
- return
|
|
|
- }
|
|
|
- tail = setTimeout(() => {
|
|
|
- tail = undefined
|
|
|
- if (proc === next) proc = undefined
|
|
|
- void Process.stop(next).catch(() => undefined)
|
|
|
- }, delay)
|
|
|
-}
|
|
|
-
|
|
|
-export function pulse(scale = 1) {
|
|
|
- stop(140)
|
|
|
- const index = shot++ % FILE.length
|
|
|
- void asset()
|
|
|
- .then(({ pulse }) => play(pulse[index], 0.26 + 0.14 * scale))
|
|
|
- .catch(() => undefined)
|
|
|
-}
|
|
|
-
|
|
|
-export function dispose() {
|
|
|
- stop()
|
|
|
-}
|
|
|
-
|
|
|
-export * as Sound from "./sound"
|