Термодатчик из звуковой карты

в 14:32, , рубрики: C#, DIY, diy или сделай сам, звук, преобразование фурье, термистор

Всем привет!

Как-то раз зимой у меня сгорел греющий кабель в водопроводе (он не даёт замёрзнуть воде в трубах, проложенных близко к поверхности). Кабель конечно пришлось заменить, водопровод отогрелся и снова заработал, однако возникло жгучее желание "что-то с этим сделать". Хотя бы узнавать о его неисправности заранее, а в лучшем случае - ещё и автоматически отогревать. Идея в общем-то несложная: надо мерять температуру трубы и включать обогрев (при помощи любого электрического обогревателя), если она мёрзнет. Всё просто, но датчика температуры под рукой нет. Конечно, можно его заказать на всем известном китайском сайте, или на не менее известном российском, но это совершенно неспортивно. Потому попробуем изготовить датчик из имеющихся под рукой компонентов. Для этого нам понадобится: звуковая карта (наверняка найдётся в компьютере), два jack-разъёма (от наушников или микрофонов), один терморезистор и пара резисторов.

Disclaimer: всё нижеизложенное просьба воспринимать как забавный способ размять мозги и развлечься. Само собой, "по-хорошему" надо обзавестись нормальным датчиком, а не придумывать велосипед. Однако мне было интересно собрать что-то не очень типичное, а заодно и разобраться в генерации и анализе звука в коде.

Основная идея

Итак, у нас есть термистор. В теории он является источником аналогового сигнала, меняя своё сопротивление в зависимости от температуры. Чтобы оцифровать его нам потребуется аналогово-цифровой преобразователь (АЦП, ADC). В компьютере такой есть, и находится он в звуковой карте, оцифровывая сигнал, поступающий с микрофона. Беглое гугление по фразе thermistor sound card приводит нас к старой статье, которая описывает практически то, что нам нужно [1]. Вкратце, суть в том, что звуковая карта для формирования звука создаёт напряжение на своём выходе, а для получения - замеряет соответственно напряжение на микрофонном входе и мы можем генерировать напряжение на выходе звуковой карты и замерять его на входе. Однако нам надо измерять сопротивление. Чтобы это сделать, можно собрать схему, известную как делитель напряжения [3], примерно таким образом.

Термодатчик из звуковой карты - 1

Самым простым способом было бы сгенерировать постоянный сигнал на выходе звуковой карты, после чего замерять уровень громкости микрофона. Однако на входе и выходе звуковой карты расположены фильтрующие конденсаторы [2], потому произвольно выбранный сигнал при прохождении через них будет искажён. Не вдаваясь в подробности (о них мельком упомянем ниже), сигнал не будет искажаться, в частности, если он будет иметь вид некоторой синусоиды с частотой в рамках слышимого диапазона (примерно 20-20000 Гц). Более формально, уровень сигнала в момент времени t должен определяться формулой следующего вида:

Термодатчик из звуковой карты - 2

Где t - время от начала генерации сигнала, в секундах, ω - частота, A - амплитуда (она же громкость). При этом на входе микрофона появится аналогичный синусоидальный сигнал, с такой же частотой, но с другой амплитудой A1. 

Легко заметить, что генерируя подобный сигнал мы получим на цифро-аналоговом преобразователе (ЦАП) звуковой карты источник переменного напряжения V1, которое прямо пропорционально амплитуде генерируемого сигнала A (с некоторым коэффициентом k). На входе звуковой карты получим сигнал с амплитудой A2, которая прямо пропорциональна напряжению на входе V2 (с коэффициентом k2). Исходя из формулы делителя напряжения:

Термодатчик из звуковой карты - 3

Откуда получаем искомое сопротивление термистора:

Термодатчик из звуковой карты - 4

Здесь возникает проблема с определением коэффициентов k и k2, которые зависят от внутреннего устройства звуковой карты, а соответственно могут меняться. Заметим, что оба коэффициента нам не нужны, важно лишь их отношение (в некотором смысле это соотношение громкости звука на выходе карты и чувствительности микрофона). Чтобы его найти, можно откалибровать датчик: заменить термистор на резистор с известным сопротивлением, провести замеры, после чего получить из формулы соотношение k и k2. Однако это неудобно. Можно ли обойтись без калибровки? Наверное нет, но можно её автоматизировать. Вспомним, что в выходе звуковой карты есть отдельные контакты для левого и правого каналов. Мы можем на выход правого канала подсоединить резистор с известным сопротивлением, на выход левого - термистор, а потом просто сделать два замера: сначала калибровочный, а затем основной. Схема включения таким образом приобретает вот такой вид:

Термодатчик из звуковой карты - 5

Как распознать синусоиду во входном сигнале

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

Известно, что любую достаточно гладкую функцию можно представить в виде суммы некоторого количество гармонических колебаний (синусоид) с разными частотами. Делается это при помощи преобразования Фурье, про которое была статья на Хабре [4]. Его результатом мы получим набор амплитуд и частот таких, что если складывать синусоиды с соответствующими параметрами, получится исходная функция.

Применительно к нашему входному сигналу, мы знаем, что он представляет собой одну единственную синусоиду. Потому, после применения преобразования Фурье, в его результате достаточно будет найти максимальную амплитуду - она и будет искомой. А далее её уже можно сравнивать с той амплитудой, с которой генерировался исходный сигнал.

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

Talk is cheap, show me the code

Для реализации был выбран C#. Во-первых, потому что хотелось его вспомнить. А во-вторых, была надежда на то, что можно полученный софт окажется кроссплатформенным (оправдалась лишь частично). Для работы со звуковыми устройствами после некоторого гугления была выбрана библиотека NAudio [5]. 

Генерация сигнала при помощи NAudio выглядит достаточно просто - создаём класс, унаследованный от ISampleProvider и реализуем в нём Read, который будет заполнять значениями сигнала предоставленный буфер.

Генерация сигнала на выходе звуковой карты
class ThermoSampleProvider : ISampleProvider
    {
        public const int SAMPLE_RATE = 44100;
        WaveFormat format;
        float freq;
        float amp;
        int alreadyGeneratedSamples;

        public ThermoSampleProvider(float freq, float amp)
        {
            format = WaveFormat.CreateIeeeFloatWaveFormat(SAMPLE_RATE, 1);
            this.freq = freq;
            this.amp = amp;
            alreadyGeneratedSamples = 0;
        }

        public WaveFormat WaveFormat
        {
            get
            {
                return format;
            }
        }

        public int Read(float[] buffer, int offset, int count)
        {
            for (int i = 0; i < count; i++)
            {
                int sample = alreadyGeneratedSamples++;
                buffer[offset + i] = amp * MathF.Sin(2 * MathF.PI * sample * freq / SAMPLE_RATE);
            }
            return count;
        }
    }

Получение данных с входного устройства особой сложности не представляет - достаточно подписаться на событие DataAvailable и включить запись. 

Чтение сигнала с микрофона
class Recorder
    {
        const int RECORDING_RATE = 44100;
        private WaveInEvent device;
        public List<float> samples;

        public Recorder(WaveInEvent device)
        {
            this.device = device;
        }

        public async Task Record(float duration)
        {
            device.WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(RECORDING_RATE, 1);
            samples = new List<float>();
            device.DataAvailable += OnDataAvailable;
            device.StartRecording();
            await Task.Delay((int)(duration * 1000));
            device.StopRecording();
        }

        private void OnDataAvailable(object sender, WaveInEventArgs e)
        {
            var buffer = new WaveBuffer(e.Buffer);
            for (int i = 0; i < e.BytesRecorded / 4; i++)
            {
                samples.Add(buffer.FloatBuffer[i]);
            }
        }
    }

Основная генерация и обработка сигнала оказывается очень простой. В силу асинхронности C# и самой NAudio можно не возиться с потоками и их синхронизацией, а просто запустить параллельно асинхронную генерацию звука и запись его же.

var recorder = new Recorder(inputDevice);
var provider = new ThermoSampleProvider(THERMO_FREQ, THERMO_AMP);
outputDevice.Init(provider.ToStereo(channel == MeasureChannel.Left ? 1 : 0, channel == MeasureChannel.Right ? 1 : 0));
outputDevice.Play();
await recorder.Record(duration);
outputDevice.Stop();

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

Остальная часть проекта особого интереса не представляет - он оформлен в виде консольного приложения, которое имеет несколько команд: выдать входные и выходные устройства (на случай если их несколько, чтобы можно было точно указать название) и собственно провести замер.

Выводы

Тестирование показало, что такая конструкция замеряет температуру с достаточно приемлемой точностью (расхождение со стандартным датчиком DHT22) составляет не более полутора градусов. Однако требуется подбирать длительность замера - если она будет слишком маленькой, на выходе преобразования Фурье будут либо совсем неадекватные данные, либо вообще не будет синусоиды с ожидаемой частотой. Очень порадовала NAudio и вообще экосистема C# - проект без проблем заработал как под Windows, так и под Linux (openSUSE). Огорчило лишь то, что под 32-битной версией openSUSE мне не удалось его запустить. 

Проект целиком доступен на Github

Бонус: поскольку запустить под x86 версию на C# мне не удалось, есть альтернативная версия, реализованная с использованием Qt на C++

Использованные источники

1. https://www.edn.com/measure-resistance-and-temperature-with-a-sound-card-2/

2. https://noiseengineering.us/blogs/loquelic-literitas-the-blog/ac-vs-dc-coupling-what-is-it

3. http://wiki.amperka.ru/%D1%81%D1%85%D0%B5%D0%BC%D0%BE%D1%82%D0%B5%D1%85%D0%BD%D0%B8%D0%BA%D0%B0:%D0%B4%D0%B5%D0%BB%D0%B8%D1%82%D0%B5%D0%BB%D1%8C-%D0%BD%D0%B0%D0%BF%D1%80%D1%8F%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F

4. https://habr.com/ru/post/253447/

5. https://github.com/naudio/NAudio

Автор: Денис

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js