dimhold.by
← Статьи

В Linux записать динамики можно бесплатно, пока не тронешь громкость

earshot, мой локальный транскрибатор, стал публичным в августе. В его README есть таблица про запись звука, её писал я. Микрофон работает на всех 3 системах. Про системный звук, то есть про то, что играют динамики, в таблице написано “works out of the box” для Linux. Windows нужен Stereo Mix или VB-CABLE, а для “macOS 12 and later” нужен BlackHole. Под таблицей я написал, что macOS вообще не даёт обычному процессу API для записи системного звука.

Эту строку я написал по памяти. Память из тех лет, когда все ставили Soundflower, чтобы записать звук на Маке. Правда ли это до сих пор, я не проверял. Строку про Linux я тоже не ставил под сомнение, потому что на моей машине всё просто работало. На этой неделе я прошёлся по обеим строкам.

Что на самом деле дают операционные системы

Машины с Windows или с Маком для этого у меня нет, так что эта часть по документации и исходникам.

В Windows запись с выхода есть в WASAPI со времён Vista. Открываешь поток записи на устройстве воспроизведения с флагом AUDCLNT_STREAMFLAGS_LOOPBACK. Ещё есть process loopback, он записывает звук одного приложения и его дочерних процессов. Минимальной версией Microsoft указывает Windows 10 сборки 20348. Ни один клиентский выпуск Windows 10 до этой сборки не дошёл, это сборка Windows Server 2022, так что на деле речь о Windows 11 или этом сервере. В macOS звук появился в ScreenCaptureKit с macOS 13. С macOS 14.2 в Core Audio есть ещё AudioHardwareCreateProcessTap, отвод с выхода одного процесса или всех сразу. Так что моя фраза про macOS неверна для всех версий начиная с 13.

Виртуальное устройство earshot всё равно нужно из-за ffmpeg. Сам earshot никакое аудиоустройство не открывает, он запускает ffmpeg, читает из его stdout PCM 16 кГц моно и оставляет платформу на ffmpeg. Сегодня, 11 октября, я посмотрел список libavdevice в ветке master у ffmpeg. Для звука в Windows там есть dshow, а входа WASAPI нет. На macOS во входе avfoundation нет кода ScreenCaptureKit, поэтому из источников звука он видит только устройства записи.

запись того, что играют динамики, октябрь 2026 что даёт ОС, с какой версии что читает ffmpeg что нужно earshot Linux источник monitor PulseAudio, PipeWire -f pulse читает monitor ничего Windows WASAPI loopback, Vista process loopback, 20348 -f dshow входа WASAPI нет Stereo Mix или VB-CABLE macOS ScreenCaptureKit, 13 process tap, 14.2 -f avfoundation только устройства записи BlackHole
Столбцы про ОС по документации Microsoft и Apple, столбец ffmpeg по libavdevice в ветке master на 11 октября 2026. Прогнана только строка Linux.

Linux, с замером

Строку про Linux я мог померить. Нода, на которой я работаю, это сервер без звуковой карты. Поэтому я распаковал PulseAudio 16.1 и PipeWire 1.0.5 с WirePlumber 0.4.17 из пакетов Ubuntu 24.04 в локальную папку и создал виртуальные выходы. Оба сервера я гонял одними и теми же pacat и pactl, так что клиентская сторона одинаковая. Тестовый звук это 3 секунды шума с фиксированным зерном плюс тон 1 кГц, 48 кГц стерео. Каждую запись я выравниваю по оригиналу и сравниваю отсчёт за отсчётом.

На громкости 100% monitor отдаёт звук бит в бит. Это 144 000 кадров из 144 000, на обоих серверах и в каждом прогоне окончательного стенда.

Потом я подвинул ползунок на PulseAudio. На выходе с громкостью 50% запись равна оригиналу, умноженному на 0,125, с точностью до округления последнего бита. Это 0,5 в кубе, кривая громкости PulseAudio. Выключенный звук на выходе даёт чистые нули. Громкость одного приложения на 50% во время воспроизведения тоже даёт 0,125. Значит, на PulseAudio monitor читает звук после всех этих регуляторов.

И сервер это запоминает. После того как я убавил одно приложение, его следующий поток снова начался с 0,125. Сервер хранит эту громкость на диске по роли потока, а если роли нет, то по идентификатору или имени приложения. Для earshot это значит, что старая настройка приложения действует и на его следующий созвон.

Я чуть не написал, что PipeWire ведёт себя так же. На моих первых выходах так и было. Потом проверка по исходникам показала причину. Эти выходы я создавал через слой совместимости PipeWire с PulseAudio. Этот слой включает им monitor.channel-volumes. У самого PipeWire по умолчанию эта настройка выключена. WirePlumber 0.4.17 оставляет её выключенной для настоящих звуковых карт. Тогда я сделал ещё один выход прямо в PipeWire, с настройкой по умолчанию. На нём выход на 50% дал запись бит в бит, выключенный звук тоже. Громкость приложения по-прежнему давала 0,125 и по-прежнему запоминалась.

откуда читает monitor, PulseAudio 16.1 и PipeWire 1.0.5 приложение громкость прил. микшер громкость, mute динамики приложение на 50%: ×0,125 на обоих запоминается для нового потока monitor PipeWire, по умолч. выход и mute: не влияют monitor PulseAudio выход 50%: ×0,125, mute: нули
По 3 прогона на каждом выходе. На 100% любая запись совпадает бит в бит. У выхода PipeWire настройка monitor по умолчанию, та, что WirePlumber 0.4.17 оставляет настоящим звуковым картам.

Переключение выхода на обоих работает нормально. Я начал запись с @DEFAULT_MONITOR@, переключил выход по умолчанию на другой и играл туда. Оба сервера перевели запись на новый monitor. Запись снова совпала бит в бит.

Где начинает болеть

earshot отправляет в модель не любой звук. Он режет поток на кадры по 20 мс. Кадр считается речью, если его RMS не меньше 600 по шкале 16 бит. Фрагмент остаётся, только если в нём набралось хотя бы 300 мс таких кадров. Эти значения по умолчанию выбирал я сам и ни разу не думал о них вместе с ползунком громкости.

Поэтому я проиграл запись речи на разной громкости. Записывал ffmpeg 6.1.1 с теми же аргументами, которые earshot собирает на Linux, только в файл, а не в stdout. Потом отдал результат сегментеру earshot на коммите 15da817 с настройками по умолчанию. Запись это 10 гарвардских фраз из Open Speech Repository, для воспроизведения я перевёл её в стерео. На 100% пик около -12 dBFS, а у самого громкого кадра в 20 мс RMS от 3500 до 3650, в зависимости от прогона.

сколько фрагментов earshot нарезал из 10 фраз 100% 90% 80% 70% 60% 50% 40% 30% PulseAudio, ползунок выхода 10 10 10 8 0 0 0 0 PipeWire, ползунок выхода 10 10 10 10 10 10 10 10 PipeWire, ползунок прил. 10 10 10 9 0 0 0 0
10 гарвардских фраз из Open Speech Repository, записанных с monitor с аргументами ffmpeg из earshot и разрезанных его сегментером с настройками по умолчанию: речь это RMS от 600, фрагменту нужно 300 мс такой речи.

На PulseAudio с ползунком выхода earshot нарезает из 10 фраз 10 фрагментов до 80% включительно. На 70% нарезает 8. С 60% и ниже ни одного. На 60% самые громкие кадры всё ещё проходят порог 600, ведь 3650 на 0,216 это около 790, но таких кадров во всей записи всего 18. Ни один фрагмент не набирает из них 300 мс. На PipeWire ползунок выхода ничего не меняет. earshot нарезает все 10 фрагментов даже на 30%. Ползунок приложения возвращает там тот же обрыв: 9 фрагментов на 70%, ни одного с 60% и ниже. Разницу 8 против 9 на 70% я списываю на округление и границы кадров и дальше не проверял. Ни в одном из этих прогонов ничего не падает, ffmpeg и сегментер продолжают работать, а расшифровка остаётся пустой.

Меня какое-то время обманывал и собственный стенд. В первых прогонах запись начиналась за 1 секунду до воспроизведения и останавливалась через 1 секунду после. Некоторые записи теряли до секунды в начале или кусок в конце. С запасом в 3 секунды с каждой стороны это больше не повторялось. Концы терялись потому, что pipewire-pulse отдаёт записи данные крупными кусками. Почему пропадало начало, я не знаю и не копал.

Что меняется для earshot

В README строка про Linux не должна говорить только “works out of the box”. Работает, но что именно пишется, зависит от громкости. На PulseAudio ползунок выхода и выключение звука решают, услышит ли earshot речь вообще. На PipeWire это решает только громкость приложения. В обоих случаях пустая расшифровка приходит без всякой ошибки. Строка про macOS просто неверна. Её надо убрать из README и из docs/audio-capture.md тоже.

Для Windows и macOS честное решение это собственный путь записи рядом с ffmpeg. То есть WASAPI loopback с одной стороны и process tap с другой. Это больше кода, чем есть в earshot сейчас. Я его не написал.

Что я не проверял

Ничего из этого не запускалось ни на Windows, ни на macOS. Я не знаю, видит ли WASAPI loopback или process tap звук до системной громкости или после.

Все выходы были виртуальными. PulseAudio может применять часть громкости в железе звуковой карты. По исходникам эта часть идёт после monitor, а выключение звука всегда делается программно. Значит, на ноутбуке с PulseAudio обрыв может оказаться ниже по ползунку, а выключенный звук всё равно должен давать тишину. Это я прочитал в коде, запустить не мог. Мой выход в PipeWire только повторяет настройку по умолчанию для настоящей карты. Настоящей картой он от этого не становится.

Сам обрыв относится только к этой записи. Более тихий созвон сдвигает его вверх по ползунку, более громкий вниз. Порог 600 к тому же меняется флагом --threshold.