Hatred's Log Place

DON'T PANIC!

Apr 2, 2021 - 6 minute read - programming linux

You can't just take and...

Сгенерировать нули.

В этом месяце (точнее в прошлом) я не прошёл бы тест на продуктивность (недавно узнал про такие: productivity test): 5 тасок и не все в завершённом состоянии.

С одной из них провозился 1.5 недели. Хотя сложного, на первый, взгляд ничего и нет.

А суть её заключается вот в чём: генерировать нули в звуковом устройстве, то есть - тишину.

Поехали!

Точнее генерировать ничего и не нужно: буфера в ALSA кольцевые, нужно его заполнить нулями один раз и просто двигать указатель по нему.

Двигать указатель.

Казалось бы, а тут-то какие сложности?

В предыдущей заметке я конспективно указал основные термины и понятия связанные с PCM. Нам нужен период, его размер и sample rate, он же - частота дискретизации.

Берём таймер. Пусть будет High Resolution Timer. Когда срабатывает ALSA колбек struct snd_pcm_ops::prepare у нас уже есть все нужные параметры для стрима, типа количества каналов, битности семплов, частоты. Есть и информация о размере выделенного кольцевого буфера: substream->runtime->period_size. Только нужно быть внимательным, смотрим на тип: snd_pcm_uframes_t и понимаем, что это размер периода не в герцах, не в единицах времени, а во фреймах.

Так вот, в этом колбеке, мы настраиваем таймер, что бы он тикал один раз в Period Size. Максимально точно в целых числах получается вот так:

period = runtime->period_size;  // во фреймах, тут как договоритесь в hw_params, может быть нифига не кратен rate
rate = runtime->rate;           // в HZ, например 48000, это частата **фреймов**
sec = period / rate;            // вычленим секунды, в более-менее low-latency здесь всегда будет 0
// ну и посчитаем максимально точно ту часть, что в наносекундах
period %= rate;             
nsecs = div_u64((u64)period * 1000000000UL + rate - 1, rate);

// чисто хелпер, по сути sec * 10^9 + nsecs
period_time = ktime_set(sec, nsecs);

После чего, в обработчике struct snd_pcm_ops::trigger, по START активируем таймер и он будет тикать раз в period_time наносекунд. А дальше, в обработчике прерывания таймера, мы вызываем специально предназначенную для этого функцию:

void snd_pcm_period_elapsed(struct snd_pcm_substream *substream)

Как сказано в документации, вольный перевод:

обновляет статус PCM для следующего периода

Вызывается из обработчика прерывания, когда источник семплов запроцессил (сиречь - положил в память) period_size фреймов. Заобновится указатель на данные, пробудятся те, кто данные ждёт и т.п.

Если вдруг между вызовами случилось несколько period_size - нужно сделать один вызов.

По сути, эта функция, вызывает другой наш колбек: struct snd_pcm_ops::pointer, который и говорит текущую позицию в кольцевом буфере.

То есть, на основании показаний таймера мы можем сказать виртуальную позицию в буфере. И тут никаких проблем: нам нужно спросить сколько таймер накрутил времени относительно какой-то точки отсчёта (логично сохранить некий аналог now() при старте таймера). И пересчитать эту дельту на основании rate и buffer_size в позицию. В коде это выглядит так:

now = hrtimer_cb_get_time(timer);        // текущее время таймера, обновляется пока он работает
delta = ktime_us_delta(now, base_time);  // дельта между текущим временем и базовым
delta = div_u64(delta * runtime->rate + 999999, 1000000); // время множим на частоту, получаем дельту во фреймах
                                                          // относительно старта таймера
                                                          // применяем трюки для минимизации ошибки округления
                                                          // делим на 10^6, потому как выше дельту в мкс (us) получили
div_u64_rem(delta, runtime->buffer_size, &pos); // Ну и берём остаток от деления на размер буфера, получаем искомую
                                                // позцию

Действительно, текста много, а сложностей - ноль. Оно работает, данные шлются. Вот только процесс в пользовательском пространстве жрёт 100% CPU. А при работе с реальным железом - нет.

Начинаем разбираться.

Для начала открываем для себя (ещё кто-то будет спрашивать, зачем я ВСЁ ядро в IDE гружу? :), что struct snd_pcm_ops::pointer вызывается далеко не из одного места:

Красными кружками обведены наиболее активные потребители, выявленные при помощи логов.

__snd_pcm_lib_xfer() используется для любого трансфера, осуществляющегося через read()/write() на устройстве. И частота его следований, в целом, соответствовало таковому для железа. А вот snd_pcm_delay(), который является обработчиком для IOCTL: SNDRV_IOCTL_DELAY зовётся очень активно. При этом, было замечено, что при работе с реальным железом, задержка или 0, что означает - данных ещё нет, или 8 и больше, что означает - приложению есть что прочитать. А вот у меня с тишиной (помимо 0) и 1, и 2, и, редко, больше.

А всё почему? А потому, что между вызовами таймер тикает и время движется. И в любой момент времени (ну практически) выходит так, что данные как бы есть и софт старательно их вычитывает.

Вылечилось, как обычно, при помощи sleep()… Шутка. Почти.

Для железного модуля позиция высчитывалась на основании переданных байт в регистре счётчика. Согласно документации, данные эти обновляются каждый period_size, но на самом деле - каждые 8 фреймов. Точнее так: каждые 32 байта: модуль, скорее всего, завязан именно на байты, просто в моём режиме: PCM_S16LE/Stereo получается 8 фреймов.

Выходит, что у железа есть задержка в 8 фреймов. На частоте 48000Hz это примерно 166мкс. И этого хватает, что бы не вводить процессор в busy loop.

В общем, я добавил такую “виртуальную” задержку и в программный генератор: считается позиция, если она сдвинулась на 8 или более фреймов, то сохраняем и возвращаем новое значение. В противном случае возвращаем старое значение.

Вот теперь - работает!

Для чего это нужно или чуть больше конкретики

Есть IP Core от Xilinx: Audio PCM Formatter. Он нужен для того, что бы переложить данные из других PL (FPGA) модулей, подключенных в цепочку по AXI шине в память, к которой можно достучаться из CPU в прочитать устройством. Это режим Capture. Есть и обратный режим: переложить данные из памяти и передать по AXI шине.

Как и всё на шине этот модуль тактируется, когда данные есть. Если источником данных является ADC, то данные есть всегда, пока работает ADC, а он работает, обычно, всегда когда устройство открыто и в работе. ADC без разницы что оцифровывать: реальный звук или наводки на проводах.

Другое дело, когда источником сигнала является SDI или HDMI вход. У этих чудиков может быть и физически кабель воткнут, а аудио не быть. Или просто кабель не воткнут. С одной стороны это удобно: семплы не идут, значит сигнала нет. Пробуем потом (ага, тут или поллинг или платформоспецифичные проверки со стороны user-space).

Но теряется унификация: для аналогового аудио у нас данные всегда идут: или реальные или условные нули (наводки).

В общем возникла идея: а что если в драйвере PCM Formatter’а детектировать отсутствие данных и переключаться на программный генератор тишины. И переключаться обратно, когда данные появились. Если это сделать быстрее, чем period size, то мало кто и заметит. Точнее так, мы не озаботились о плавном переходе между позицией указателя между генератором и железом, поэтому в момент переключения могут быть некие переходные процессы, но, в целом, всё работает нормально.

Поэтому 1.5 недели включали в изучение вопроса - как это вообще скрестить между собой.

Feb 6, 2021 - 2 minute read - Туризм

Коэффициент ложки, ч.2

Недавно приехали покупки с магазина, среди которых была титановая полноразмерная ложка от Урал ВСМПО, купленная взамен утерянной. Но по прибытии, оказалось, что она ощутимо тяжелее и прочнее старого образка. В результате, возникла идея перемерить коэффициент ложки, сделанный несколько лет назад камрадом Live: Вёсла разные нужны, вёсла разные важны

Участники:

20210206_104900_2

Слева на право:

  1. Ложка пластиковая, обыкновенная. Далее просто Пластик
  2. Ложка стальная, десертная, любезно предоставлена сыном :). Далее: Сталь (мал)
  3. Ложка стальная столовая, просто взята из посудницы. Далее: Сталь (больш)
  4. Ложка титановая Урал ВСМПО, старого образца. Далее: Ti (old)
  5. Ложка титановая Урал ВСМПО, нового образца. Далее: Ti (new)
  6. Ложка алюминиевая, далее Al. Любезно предоставлена коллегой с работы.

Методика измерений точно повторяет Колину:

  1. взвешиваем ложку (в таблице строка ВЕС);
  2. набираем ложкой ПРОДУКТ, столько сколько влезет максимально, с горкой. Взвешиваем набранный продукт (строка ВЕСЛО).

Продукт набираем 3 раза, показания усредняем. Если весы прыгают между значениями, то брал максимальное.

Коэффициент ложки (или просто Коэффициент) считается как ВЕСЛО/ВЕС.

Jan 30, 2021 - 4 minute read - Linux

Alacritty: первое знакомство

Как сказано на GitHub разработчика:

Alacritty - A fast, cross-platform, OpenGL terminal emulator

Со слов автора он создал его, так как неудобно было конфигурировать st. (–htrd: add cite)

Если коротко, то это эмулятор терминала с быстрым выводом текста на экран, используя GPU (OpenGL). Без поддержки табов, без конфигурировать через меню, но с достаточно гибкими возможностями конфигурации через файлы, включая подхват конфигурации на лету.

Jan 30, 2021 - 3 minute read - Linux

Структура XDG menu

Нельзя вот так просто взять и создать MyProgram.desktop файл в главном меню какого-нить KDE ручками из консоли. Особенно если путь вроде Menu → Wine → Programs → Some Application

Казалось бы, просто идём в ~/.local/share/applications/wine/Programs/Some Application и создаём MyProgram.desktop с нужным содержимым. Ан нет. Во-первых, программа появится в меню только после рестарта или после выполнения команды:

kbuildsycoca5

Во-вторых, она появится совсем в другом месте, а именно в Menu → Прочее. Вот это поворот…

Dec 24, 2020 - 4 minute read - Linux Programming

HOSTALIASES for IP

Время от времени возникает необходимость организовать аналог /etc/hosts, но индивидуально для пользователя. Такая возможность есть, и называется HOSTALIASES.

Apr 19, 2020 - 6 minute read - Туризм

Двойной траверс в окрестностях Владивостока

В субботу был запланирован пешкодрапинг в окрестностях Владивостока, в целях самоизоляции и ограничения социальных контактов посредством транспортировки своей тушки в лес, подальше от этого самого социума и неблагополучной эпидемиологической обстановки в городе. Кроме того, в планах стояла задача государственной важности в части борьбы с треклятым вирусом, путём проведения опытов над людьми (Женевская конвенция? Не, не слышал!) в части стимуляции иммунной системы нечеловеческими (даже живодёрскими) методами, а именно:

  • насильственным вдыханием свежего воздуха;
  • непомерными нагрузками на двигательный аппарат, сердечно-сосудистую и дыхательные системы;
  • стимуляции и манипуляции психикой при помощи созерцания жутких картин живописных видов, буйства дикой природы.

Программа экспериментов открыта и допускается к расширению в полевых условиях.

Число. Подпись.

Apr 10, 2020 - 1 minute read - linux programming

platform_device subdevice

Изучал https://github.com/Xilinx/linux-xlnx/blob/xlnx_rebase_v4.19/sound/soc/xilinx/xlnx_pl_snd_card.c

Конкретно функцию xlnx_snd_probe() и не мог понять… Как в pdev->dev.platform_data оказываются нужные ноды, в нужной последовательности (сначала PLAYBACK, потом CAPTURE), причём, если PLAYBACK нет, то там будет NULL.

Так как у драйвера нет of_match_table, то он:

  1. сам не загрузится (это так на практике) и
  2. что бы xlnx_snd_probe() что-то нашла, через него нужно пропустить ВСЕ platform_device… но не всех же платформенных устройств в platform_data вообще именно этот тип данных будет!

Второе оказывается не так. Это “виртуальный” драйвер, а другой, реальный для этого устройства, вызовом функции platform_device_register_resndata() собственно создаёт platform_device, и в качестве второго аргумента оно принимает имя, по которому будет отфильтровываться, какому драйверу оно может быть передано. В данном случае “xlnx_snd_card”. И этот же вызов регистрирует pdev->dev.platform_data, в нужном для целевого драйвера виде.

А уже матчинг подходящего платформенного драйвера для вновь созданного платформенного устройства будет происходить здесь: platform_match(). В данном случае - по имени устройства и по имени драйвера.

Тяжело идёт.

PS разные верии ядра роли не играют.

Mar 8, 2020 - 2 minute read - Linux

KDE: WA для возврата окон на главный экран при подключении внешнего монитора

Суть проблемы, если коротко, в том, что подключая внешний монитор к ноутбуку в том случае, если он логически расширяет экран влево (находится левее основного экрана), то все открытые окна с экрана ноутбука перемещается на вновь подключенный экран.

Вообще, проблема актуальна не только для ноутбука, но частое подключение и отключение мониторов более присуще именно ноутбукам.

Решения проблемы не существует на данный момент. Ну… или я его не нашёл. Но буквально сегодня разыскал занимательный хак, как вернуть окна, если они прыгнули на левый (в прямом и переносном смысле) монитор:

Под катом сохраню копию, на всякий случай. Вообще, как грубое решение в лоб работает. Повесил на горячую клавишу Super+P.

Feb 29, 2020 - 2 minute read - Life Туризм

Длинные выходные на Камчатке

Так получилось, что поймали акционные билеты до Петропавловска, на троих человек - около 22 тыр. Глядя на цену сразу мысль - надо брать!

Ниже небольшой фото-видео-отчёт с выходных на Камчатке. Для тех кого волнует трафик: осторожно, много тяжёлых фото! (я пока не нашёл адекватного варианта хостинга фотографий).

Все фото можно посмотреть в галерее на Google Photo.

Feb 28, 2020 - 3 minute read - linux

mDNS stopped working

В один прекрасный момент, на ровном месте перестал работать mDNS и перестали резолвится хосты в зоне .local.

При этом демон Avahi был запущен, сервис systemd-resolved остановлен и в /etc/nsswitch.conf всё в порядке (присутствуют mdns4_minimal и mdns4, в общем, всё как в ArchWiki).

Более того, вызов:

avahi-resolve -n 35002201.local

завершается успешно и возвращает адрес хоста.

Исследование привело к команде getent. Вызов:

getent hosts 35002201.local

выдал пустой результат. Хмм… как говорит документация, эта утилита точно работает через NSS.

Что делает пытливый ум линуксоида в таком случае? Правильно! Запускает команду через strace (я убрал лишний вывод, оставил только суть):

$ LANG=C strace -f getent hosts 35002201.local
execve("/usr/bin/getent", ["getent", "hosts", "35002201.local"], 0x7ffef387b638 /* 98 vars */) = 0
brk(NULL)                               = 0x5618ad268000
...
openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=372, ...}) = 0
read(3, "# Name Service Switch configurat"..., 4096) = 372
read(3, "", 4096)                       = 0
close(3)                                = 0
...
openat(AT_FDCWD, "/usr/lib/libnss_mdns4_minimal.so.2", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 \20\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=18104, ...}) = 0
mmap(NULL, 20496, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f2bef77a000
mmap(0x7f2bef77b000, 8192, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1000) = 0x7f2bef77b000
mmap(0x7f2bef77d000, 4096, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f2bef77d000
mmap(0x7f2bef77e000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f2bef77e000
close(3)                                = 0
...
openat(AT_FDCWD, "/etc/mdns.allow", O_RDONLY) = -1 ENOENT (No such file or directory)
...
exit_group(2)                           = ?
+++ exited with 2 +++

Хмм… А что это за файл /etc/mdns.allow, который во моей системе отсутствует? Читаем и видим, что рекомендованная конфигурация - это отсутствие этого файла. Всё как у меня. Тогда, выполняется некоторая эвристика:

  • Если запрос не заканчивается на .local. или .local, он отклоняется
  • Если в запросе есть точка (появяляется домент третьего уровня, например foo.bar.local), то он тоже отклоняется
  • Если системный unicast DNS (если проще: обычный DNS, работающий через 53 порт), указанный в /etc/resolv.conf отдаёт SOA запись для local, запрос тоже отклоняется. Иными словами, запрос отклоняется, если запрос host -t SOA local верунл что-то отличное от Host local not found: 3(NXDOMAIN).

Хм, а вот третий пункт уже интересней. Вводим команду и видим:

$ host -t SOA local
local has SOA record ns1.inetvl.ru. support.inetvl.ru. 2017062801 28800 3600 1209600 86400

Оппа… Создаём файл /etc/mdns.allow со следующим содержимым:

.local.
.local

И пробуем:

$ getent hosts 35002201.local
192.168.101.2   35002201.local

$ ping 35002201.local
PING 35002201.local (192.168.101.2) 56(84) bytes of data.
64 bytes from 35002201.local (192.168.101.2): icmp_seq=1 ttl=64 time=0.216 ms
64 bytes from 35002201.local (192.168.101.2): icmp_seq=2 ttl=64 time=0.271 ms
64 bytes from 35002201.local (192.168.101.2): icmp_seq=3 ttl=64 time=0.247 ms
^C
--- 35002201.local ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2029ms
rtt min/avg/max/mdev = 0.216/0.244/0.271/0.022 ms

Ура!

Выводы

Только два:

  1. При обновлении системы убрали дефолтный /etc/mdns.allow. Но пакеты, где бы он мог находиться в последнее время не обновлялись.
  2. Изменились настройки сети провайдера

Собственно, скорее всего пункт 2.

Feb 16, 2020 - 1 minute read -

Google Photo: как получить прямую ссылку на фото в открытом альбоме

Небольшой хинт:

Если в двух словах:

  1. делаем альбом и генерируем ссылку на него
  2. открываем её из другого браузера, где вы не залогинены. Можно в режиме инкогнито - тоже пойдёт.
  3. копируем ссылку и пользуемся

В конце ссылки есть “=w1236-h825” - это размер. Можно скорректировать под свои нужды. Если заменить на “=d” - скачаем оригинальную картинку (оригинальный размер).

Посмотрим, как работать будет :)

UPD: проблема в том, что при вставке большого числа изображений на страницу, не все будут открываться. Гугл будет ругаться на слишком частые обращения.

UPD: ну и небольшие дополнительные утилиты: