python

Звук в DIY проектах

  • пятница, 15 апреля 2022 г. в 00:39:47
https://habr.com/ru/post/661037/
  • Open source
  • Python
  • Программирование
  • Разработка под Linux


Если ваше хобби/DIY, как и моё, связано с компьютером, то на каком то этапе вам захочется использовать звук. Предлагаю поговорить о звуке и обменяться опытом. Конкретно говорить будем, про запись и воспроизведение звука на компьютере. Возьмем компьютер под управлением Linux, но и под Windows должно работать. Язык для программирования предпочитаю Python. Как известно в Linux есть ALSA и для нее есть python библиотека pyalsaaudio. В каком то проекте голосового помощника я видел ее использовали для регулировки громкости. Про громкость поговорим потом. Т.к. библиотека не является системонезависимой поэтому возьмем другую хорошо известную - sounddevice. Библиотека основано на кросс платформенной C/C++ библиотеке portaudio.

Необходимый минимум для использования sounddevice есть в описании и он весьма прост:

import sounddevice as sd
sd.play(myarray, fs)

здесь myarray — массив со звуком, надо сказать, что sounddevice поддерживает массивы numpy. fs — битрейт записи или частота дискретизации. Откуда это все брать? Нужна еще библиотека, например soundfile.

import sounddevice as sd
import soundfile as sf

myarray, fs = sf.read('my-file.wav')
sd.play(myarray, fs)
sd.wait() #  ждем конца воспроизведения

Можно создать свой сигнал, например синус, таким образом для каких то нужд можно получить генератор любой звуковой частоты:

duration=1 длительность сигнала сек 
frequency = 1000 # частота Гц 
samplerate = 24000 # битрейт 
amp = 10000 # амплитуда 
############# 
#берем numpy 
import numpy as np 
t = np.arange(duration * samplerate) / samplerate 
signal = amp*np.sin(2 * np.pi * frequency * t) 
sd.play(signal) sd.wait()

Естественно выбор не ограничен синусом. Не намного сложнее запись:

import sounddevice as sd 
import soundfile as sf

fs = 48000 # битрейт записи 
duration=3 # длительность записи 
rec = sd.rec(int(duration * fs), samplerate=fs, channels=1, blocking=True) 
sd.wait() 
sf.write('my-file.wav', rec, fs)

Есть еще функция для одновременной записи и воспроизведя. Какие то подробности можно посмотреть в описании. Но это на мой взгляд мало интересно, т.к. недостаточно гибкости… Переходим к потокам. Про потоки в описании тоже ест и даже с примерами, поэтому отмечу, на мой взгляд, интересные моменты. Создаем поток:

stream = sd.Stream(device=(dev_in, dev_out),
            samplerate=41000,
            blocksize=blocksize,
            dtype="int16",
            channels=(1,2),
            callback=callback_fun)

Данный тип потока поддерживает и запись и воспроизведение. В библиотеке есть и раздельные, но этот мне показался наиболее интересным. В параметре device, поэтому содержится указание на на эти устройства. Где же их брать? Воспользуемся функцией sd.query_devices():

device_info = sd.query_devices()

в результате получим список с доступными звуковыми устройствами. Например такой:

0 sof-hda-dsp: - (hw:0,0), ALSA (2 in, 0 out) 
1 sof-hda-dsp: - (hw:0,3), ALSA (0 in, 2 out)
2 sof-hda-dsp: - (hw:0,4), ALSA (0 in, 2 out)
3 sof-hda-dsp: - (hw:0,5), ALSA (0 in, 2 out)
4 sof-hda-dsp: - (hw:0,6), ALSA (4 in, 0 out)
5 sof-hda-dsp: - (hw:0,7), ALSA (4 in, 0 out)
6 sysdefault, ALSA (128 in, 0 out)
7 samplerate, ALSA (128 in, 0 out)
8 speexrate, ALSA (128 in, 0 out)
9 pulse, ALSA (32 in, 32 out)
10 upmix, ALSA (8 in, 0 out) 
11 vdownmix, ALSA (6 in, 0 out)
12 default, ALSA (32 in, 32 out)

в параметр device можно записать или номер или имя, например device = (12,12). Я пробовал в параметр вписать номер от микрофона USB камеры, программа работает через раз — вылетает ошибка с указанием на portaudio, как я понял такая ошибка не только у меня. C default все работает. Так можно получить имя устройства:

dev_in = sd.query_devices(kind="input")["name"]
dev_out = sd.query_devices(kind="output")["name"]

Для работы библиотека предлагает, и это удобно, использовать callback функцию.

def callback_fun(indata, outdata, frames, time, status):
  if status: 
    print(status) 
  outdata[:] = indata

Она вызывается функцией потока. Здесь, в функции видно, что входной сигнал подается прямо на выход. Например сигнал с микрофона можно сразу услышать. Или если кто то делает голосового ассистента, то сигнал с микрофона надо направить на распознавание голоса, а сигнал с синтезатора голоса на выход. В этом случае удобно использовать очереди — queue. Для примера, как проиграть WAV фай с диска? Для этого можно использовать такие функции:

import queue

fifo = queue.Queue() 
inp_fifo = queue.Queue() 
#############
def play(file):
    with sf.SoundFile(file,mode="r") as f:
        data = f.read(blocksize,dtype='int16')
        while len(data):
            if not fifo.full():
                data = f.read(blocksize,dtype='int16')
                fifo.put(data)
#################
def callback_fun(indata, outdata, frames, time, status):
    inp_fifo.put(bytes(indata.copy())) # для обработки сигнала с микрофона
    if status:
        print(status)
    inp = np.zeros(len(outdata), dtype=np.int16)
    if not fifo.empty() :
        bf = np.frombuffer(fifo.get(), dtype=np.int16)
        inp[:bf.shape[0]] = bf #
    outdata[:] = inp.reshape(-1,1)


Отмечу, если помните, при создании потока был параметр channels=(1,2), который значит, что на вход у нас 1 — моно сигнал, а на выход 2 — стерео. Если вы подаете на выход моно сигнал в поток, то он автоматически разведется на два канала.

Пойдем далее. Возможно, что кто то уже догадался у нас массивы — вот он шанс сделать регулировку громкости, т. к. в sounddevice таких специальных функций нет.
Заводим переменную, например vol = 1, а далее магия циф в строке:

 inp[:bf.shape[0]] = bf // vol

нам остается только правильно менять vol. Если взять vol = 1000, то можно практически
занулить звук. Можно пойти дальше… Если у нас есть, например, две входные очереди мы можем смешать звук от двух источников:

# см. В callback_fun
if not fifo1.empty() :
        bf1 = np.frombuffer(fifo1.get(), dtype=np.int16)
        inp1[:bf1.shape[0]] = bf1 #
if not fifo2.empty() :
        bf2= np.frombuffer(fifo2.get(), dtype=np.int16)
        inp2[:bf2.shape[0]] = bf2 #
inp = (inp1 + inp2)//2
outdata[:] = inp.reshape(-1,1)

Или один сигнал в левое уха, а другой в правое:

# см. В callback_fun
inp = np.column_stack((inp1,inp2))
    outdata[:] = inp

На мой взгляд тут широкое поле для творчества. Если кто то захочет повторить мои эксперименты смотрите внимательно, я не профессиональный программист, могут быть ошибки. Ссылка на документацию sounddevice.

Надеюсь эта информация была полезна.