155 lines
3.9 KiB
TypeScript
155 lines
3.9 KiB
TypeScript
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"
|
|
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()
|
|
}
|