140 lines
3.7 KiB
TypeScript
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]),
|
|
}
|
|
}
|
|
}
|