baudy/src/server/audio-setup.ts

140 lines
3.7 KiB
TypeScript

const LINUX_AUDIO_TYPE = 'alsa'
const LOW_MIXER_PERCENT = 70
export interface AudioMixerLevel {
command: string
control: string
db?: number
percent: number
}
export interface AudioSetup {
captureArgs: string[]
captureLabel: string
mixerLevel?: AudioMixerLevel
playbackArgs: string[]
playbackLabel: string
}
interface LinuxDevice {
device: string
label: string
}
let audioSetup: AudioSetup | undefined
export function getAudioSetup() {
if (audioSetup) return audioSetup
audioSetup = process.platform === 'linux'
? getLinuxAudioSetup()
: getDefaultAudioSetup()
return audioSetup
}
export function isLowMixerLevel(level: AudioMixerLevel | undefined): level is AudioMixerLevel {
return !!level && level.percent < LOW_MIXER_PERCENT
}
export function refreshAudioSetup() {
audioSetup = undefined
return getAudioSetup()
}
export function setMixerLevel(control: string, percent: number) {
const proc = Bun.spawnSync(['amixer', '-q', 'sset', control, `${percent}%`], {
stdout: 'ignore',
stderr: 'ignore',
})
if (proc.exitCode !== 0) return false
audioSetup = undefined
return true
}
function commandExists(command: string) {
const proc = Bun.spawnSync(['which', command], { stdout: 'ignore', stderr: 'ignore' })
return proc.exitCode === 0
}
function getCommandOutput(args: string[]) {
try {
const proc = Bun.spawnSync(args, { stdout: 'pipe', stderr: 'ignore' })
if (proc.exitCode === 0) return proc.stdout.toString().trim()
} catch {
// ignored
}
}
function getDefaultAudioSetup(): AudioSetup {
return {
captureArgs: ['-d'],
captureLabel: getDefaultLabel('input'),
playbackArgs: ['-d'],
playbackLabel: getDefaultLabel('output'),
}
}
function getDefaultLabel(type: 'input' | 'output') {
if (process.platform === 'darwin' && commandExists('SwitchAudioSource')) {
const args = type === 'input' ? ['SwitchAudioSource', '-c', '-t', 'input'] : ['SwitchAudioSource', '-c']
return getCommandOutput(args) || `system default ${type}`
}
return `system default ${type}`
}
function getLinuxAudioSetup(): AudioSetup {
const capture = getLinuxDevice('capture')
const playback = getLinuxDevice('playback')
const mixerLevel = getLinuxMixerLevel()
return {
captureArgs: ['-t', LINUX_AUDIO_TYPE, capture.device],
captureLabel: capture.label,
mixerLevel,
playbackArgs: ['-t', LINUX_AUDIO_TYPE, playback.device],
playbackLabel: playback.label,
}
}
function getLinuxDevice(type: 'capture' | 'playback'): LinuxDevice {
const envName = type === 'capture' ? 'BAUDY_CAPTURE_DEVICE' : 'BAUDY_PLAYBACK_DEVICE'
const envDevice = process.env[envName]?.trim()
if (envDevice) return { device: envDevice, label: `${envDevice} (${envName})` }
const args = type === 'capture' ? ['arecord', '-l'] : ['aplay', '-l']
const output = getCommandOutput(args)
const match = output?.match(/card\s+(\d+):.*?device\s+(\d+):/s)
if (match) {
const [, card, device] = match
return { device: `plughw:${card},${device}`, label: `plughw:${card},${device}` }
}
return { device: 'default', label: 'default' }
}
function getLinuxMixerLevel(): AudioMixerLevel | undefined {
if (!commandExists('amixer')) return
for (const control of ['Speaker Analog', 'Speaker', 'Master', 'PCM']) {
const output = getCommandOutput(['amixer', 'get', control])
if (!output) continue
const percentMatch = output.match(/\[(\d+)%\]/)
if (!percentMatch) continue
const dbMatch = output.match(/\[(-?\d+(?:\.\d+)?)dB\]/)
return {
command: `amixer -q sset '${control}' 100%`,
control,
db: dbMatch ? Number(dbMatch[1]) : undefined,
percent: Number(percentMatch[1]),
}
}
}