python

Превращаем клавиатуру в пианино с помощью Python

  • вторник, 20 апреля 2021 г. в 00:36:06
https://habr.com/ru/post/553032/
  • Python
  • Программирование


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


Немножко о MIDI

MIDI - стандарт цифровой звукозаписи на формат обмена данными между электронными музыкальными инструментами. Это отдельный большой мир, который заслуживает отдельного разговора. Но нам необходимо знать лишь некоторые правила:

  1. В каждом файле midi есть неограниченное количество треков, которые запускаются одновременно.

  2. В каждом треке хранятся определенные команды для синтезатора. Например, noteon – включить определенную ноту; noteoff – выключить определенную ноту; change_program – изменить инструмент, control_change – изменение настроек, влияющих на воспроизведение нот, их смену, и тп. Все команды можно посмотреть здесь.

  3. Каждая команда характеризуется несколькими параметрами: значение – это, например, номер ноты, номер инструмента и т.п; время от прошлой команды, через которое необходимо выполнить эту команду; номер канала (всего их 16), в котором играет данная нота или применяется соответствующая настройка, или изменяется инструмент. Если не включена полифония, то в канале не может звучать две одинаковые ноты одновременно.

Запись MIDI-файлов с помощью Mido

Mido – это библиотека на python, созданная для работы с MIDI-сообщениями и портами. Установка.

Классический пример прочтения файла:

from mido import MidiFile
 
mid = MidiFile('song.mid')
for i, track in enumerate(mid.tracks):
    print('Track {}: {}'.format(i, track.name))
    for msg in track:
        print(msg)

Классический пример создания файла:

from mido import Message, MidiFile, MidiTrack, second2tick
 
 
mid = MidiFile()
 
track = MidiTrack()
mid.tracks.append(track)
time = int(second2tick(0.1, 480, 500000))
for i in range(100):
    track.append(Message('program_change', program=12, time=0))
    track.append(Message('note_on', note=64, velocity=64, time=time))
    track.append(Message('note_off', note=64, velocity=64, time=time))
 
mid.save('new_song.mid')

Обратите внимание на параметр «time». Поподробней можно прочитать здесь.

Обработка событий клавиатуры c keyboard.

Классический пример:

import keyboard
 
 
def hook(key):
    if key.event_type == "down":
        print("{} press".format(key.name))
    if key.event_type == "up":
        print("{} release".format(key.name))
 
keyboard.hook(hook)
keyboard.wait("esc")

Словарь {key: note} можно сделать так (начинается с малой октавы):

import keyboard
 
 
keys = {}
note = 48
def hook(key):
    global note

    if key.event_type == "down":
        if key.name != "esc":
            keys.update({key.name: note})
            note += 1

    if key.event_type == "up":
        if key.name == "esc":
            print(keys)
 
keyboard.hook(hook)
keyboard.wait()

Воспроизведениe нот в реальном времени с помощью mido

Сперва надо установить python-rtmidi.

Получаем список портов (у меня всего один):

>>> mido.get_output_names()
['Microsoft GS Wavetable Synth 0']

При нажатии на клавишу передаем порту сообщение о включении или выключении ноты:

import keyboard
import mido
 
 
port = mido.open_output('Microsoft GS Wavetable Synth 0')
keys = keys = {'1': 48, '2': 49, '3': 50, '4': 51, '5': 52, '6': 53, '7': 54, '8': 55, '9': 56, '0': 57, '-': 58, '=': 59, 'q': 60, 'w': 61, 'e': 62, 'r': 63, 't': 64, 'y': 65, 'u': 66, 'i': 67, 'o': 68, 'p': 69, '[': 70, ']': 71, 'a': 72, 's': 73, 'd': 74, 'f': 75, 'g': 76, 'h': 77, 'j': 78, 'k': 79, 'l': 80, ';': 81, "'": 82, 'enter': 83}
pressed_keys = {key: False for key in keys.keys()}
 
 
def hook(key):
    if key.event_type == "down":
        if key.name in keys:
            if not pressed_keys[key.name]:
                port.send(mido.Message('note_on', note=keys[key.name]))
                pressed_keys[key.name] = True
 
    if key.event_type == "up":
        if key.name in keys:
            port.send(mido.Message('note_off', note=keys[key.name]))
            pressed_keys[key.name] = False
 
keyboard.hook(hook)
keyboard.wait()

Но у этого способа есть одна проблема – качество звучания. Да и превратить midi в wav просто так нельзя.

Воспроизведениe нот в реальном времени с помощью fluidsynth

Fluidsynth – это бесплатный программный синтезатор.

Установка fluidsynth (в Windows):

  1. Скачайте fluidsynth для Windows и распакуйте в любой папке.

  2. Добавьте подкаталог «fluidsynth\bin» в свой path. Для этого в поисковой строке напишете «Изменение системных переменных среды», запустите; далее по порядку «Переменные среды», «Path», «Изменить», «Создать» и введите путь к подкаталогу «fluidsynth\bin».

  3. Скачайте музыкальный шрифт.

  4. Теперь нужно проверить работоспособность fluidsynth. Скачайте любой midi файл и выполните в консоли «fluidsynth FluidR3_GM.sf2 file_name.mid». Не забудьте перейти в необходимый каталог.

Теперь нужно установить pyfluidsynth.

  1. Скачайте pyfluidsynth (разработка ведется на github) и распакуйте.

  2. Чтобы додуматься до этого шага мне пришлось потратить 1.5 дня (еще один намек на то, чтобы нормально выучить язык, а не с помощью статей в интернете). Перейдите в каталог «fluidsynth\bin» и найдите там файл «libfluidsynth-3.dll» (Быть может, у вас другая цифра). Теперь откройте файл «fluidsynth.py» в каталоге «pyfluidsynth», найдите строчку «lib = find_library('fluidsynth') or…» (она должна быть в начале) и поменяйте «fluidsynth» или любой другой аргумент на «libfluidsynth-3.dll» (У вас может быть другая цифра).

  3. В каталоге «pyfluidsynth» выполните команду «py setup.py install». После чего данный каталог можно удалить.

Также может потребоваться установить numpy.

Классический пример:

import time
import fluidsynth
 
fs = fluidsynth.Synth()
fs.start()
 
sfid = fs.sfload("FluidR3_GM.sf2")
fs.program_select(0, sfid, 0, 0)
 
 
for i in range(10):
    fs.noteon(0, 60, 30)
    fs.noteon(0, 67, 30)
    fs.noteon(0, 76, 30)
 
    time.sleep(1.0)
 
    fs.noteoff(0, 60)
    fs.noteoff(0, 67)
    fs.noteoff(0, 76)
 
time.sleep(1.0)
 
fs.delete()

Соединяем с keyboard:

import keyboard
import mido
import fluidsynth
 
 
fs = fluidsynth.Synth()
fs.start()
sfid = fs.sfload("FluidR3_GM.sf2")
fs.program_select(0, sfid, 0, 41)
 
 
keys = {'1': 48, '2': 49, '3': 50, '4': 51, '5': 52, '6': 53, '7': 54, '8': 55, '9': 56, '0': 57, '-': 58, '=': 59, 'q': 60, 'w': 61, 'e': 62, 'r': 63, 't': 64, 'y': 65, 'u': 66, 'i': 67, 'o': 68, 'p': 69, '[': 70, ']': 71, 'a': 72, 's': 73, 'd': 74, 'f': 75, 'g': 76, 'h': 77, 'j': 78, 'k': 79, 'l': 80, ';': 81, "'": 82, 'enter': 83}
pressed_keys = {key: False for key in keys.keys()}
 
 
def hook(key):
    if key.event_type == "down":
        if key.name in keys:
            if not pressed_keys[key.name]:
                fs.noteon(0, keys[key.name], 127)
                pressed_keys[key.name] = True
 
    if key.event_type == "up":
        if key.name in keys:
            fs.noteoff(0, keys[key.name])
            pressed_keys[key.name] = False
 
keyboard.hook(hook)
keyboard.wait()

Из midi в wav

Выполните в консоли:

fluidsynth -F melody.wav FluidR3_GM.sf2 melody.mid

Спасибо за прочтение статьи. Удачи!