От переводчика:
Статья, которую я предлагаю вам почитать, не нова — она опубликована аж 29 марта. Но на Реддите ее запостили всего несколько дней назад, да и актуальности своей она точно не потеряла. Интересность ее в том, что автор на простом и коротком примере демонстрирует практическое применение трех больших и популярных библиотек: numpy, scipy и pygame. Про первые две многие слышали, но все больше в контексте научных работ, так что интересно посмотреть на их применение в «обычной» жизни. В конце статьи прекрасная видео-демонстрация результата, хотя бы ее точно стоит посмотреть.
Авторский код сохранен без изменений, несмотря на то, что он оформлен не по PEP-8 и за его валидность я не ручаюсь. Настоящий рабочий код так или иначе есть на ГитХабе, ссылку вы найдете в конце статьи.
Запишите звук, измените тон 50 раз и сопоставьте каждому новому звуку клавишу на клавиатуре компьютера. Получится Пианопьютер!
Звук можно закодировать как массив (или список, list) значений, примерно вот так:
Чтобы проиграть этот звук вдвое быстрее, удалим каждое второе значение в массиве:
Сделав это, мы не только сократили вдвое длительность звука, но и удвоили его частоту, поэтому и его тон стал выше.
Напротив, если повторить каждое значение, то получится более медленный звук, с более длинным периодом, а значит, и ниже тоном:
Вот простая функция на Питоне, которая меняет скорость звука соответственно переданному коэффициенту:
import numpy as np
def speedx(sound_array, factor):
""" Multiplies the sound's speed by some `factor` """
indices = np.round( np.arange(0, len(snd_array), factor) )
indices = indices[indices < len(snd_array)].astype(int)
return sound_array[ indices.astype(int) ]
Cложнее изменить длительность, сохранив при этом тон (растягивание звука), или изменить тон, сохранив длительность (смещение тона).
Растягивание звука
Растянуть звук можно, используя классический метод фазового вокодера (phase vocoder). Сначала разбиваем звук на пересекающиеся куски, а затем перемещаем их так, чтобы они пересекались больше (чтобы сократить звук) или меньше (чтобы его растянуть), как на картинке:
Сложность здесь заключается в том, что передвинутые куски могут плохо взаимодействовать, и необходима определенная фазовая трансформация, чтобы этого не произошло. Вот код на Питоне, вольно переписанный отсюда:
def stretch(sound_array, f, window_size, h):
""" Stretches the sound by a factor `f` """
phase = np.zeros(window_size)
hanning_window = np.hanning(window_size)
result = np.zeros( len(sound_array) /f + window_size)
for i in np.arange(0, len(sound_array)-(window_size+h), h*f):
# two potentially overlapping subarrays
a1 = sound_array[i: i + window_size]
a2 = sound_array[i + h: i + window_size + h]
# resynchronize the second array on the first
s1 = np.fft.fft(hanning_window * a1)
s2 = np.fft.fft(hanning_window * a2)
phase = (phase + np.angle(s2/s1)) % 2*np.pi
a2_rephased = np.fft.ifft(np.abs(s2)*np.exp(1j*phase))
# add to result
i2 = int(i/f)
result[i2 : i2 + window_size] += hanning_window*a2_rephased
result = ((2**(16-4)) * result/result.max()) # normalize (16bit)
return result.astype('int16')
Смещение тона
После растягивания звука смещение тона делается просто. Чтобы получить более высокий тон, растягиваем звук, сохраняя тон, а затем ускоряем результат, чтобы финальный звук имел ту же длину, что и изначальный, но более высокий тон из-за изменения скорости.
Удвоение частоты звука повысит тон на одну октаву, или 12 полутонов. Таким образом, чтобы повысить тона на n полутонов, надо умножить высоту на 2^(n/12):
def pitchshift(snd_array, n, window_size=2**13, h=2**11):
""" Changes the pitch of a sound by ``n`` semitones. """
factor = 2**(1.0 * n / 12.0)
stretched = stretch(snd_array, 1.0/factor, window_size, h)
return speedx(stretched[window_size:], factor)
Приложение: Пианопьютер
Давайте испытаем наш новый тоносместитель. Для начала стукнем по чаше:
Затем создадим 50 производных звуков от очень низкого до очень высокого:
from scipy.io import wavfile
fps, bowl_sound = wavfile.read("bowl.wav")
tones = range(-25,25)
transposed = [pitchshift(bowl_sound, n) for n in tones]
Каждой клавише на клавиатуре назначим звук, следуя порялку, заданному в этом файле, вот так:
А вот код на Питоне, который превращает ваш компьютер в пианино (пианопьютер):
import pygame
pygame.mixer.init(fps, -16, 1, 512) # so flexible ;)
screen = pygame.display.set_mode((640,480)) # for the focus
# Get a list of the order of the keys of the keyboard in right order.
# ``keys`` is like ['Q','W','E','R' ...]
keys = open('typewriter.kb').read().split('n')
sounds = map(pygame.sndarray.make_sound, transposed)
key_sound = dict( zip(keys, sounds) )
is_playing = {k: False for k in keys}
while True:
event = pygame.event.wait()
if event.type in (pygame.KEYDOWN, pygame.KEYUP):
key = pygame.key.name(event.key)
if event.type == pygame.KEYDOWN:
if (key in key_sound.keys()) and (not is_playing[key]):
key_sound[key].play(fade_ms=50)
is_playing[key] = True
elif event.key == pygame.K_ESCAPE:
pygame.quit()
raise KeyboardInterrupt
elif event.type == pygame.KEYUP and key in key_sound.keys():
key_sound[key].fadeout(50) # stops with 50ms fadeout
is_playing[key] = False
Вот и все! А теперь я сыграю вам традиционную турецкую песенку (на самом деле нет. Прим. перев.)!
Если хотите попробовать то же самое дома, вот все файлы, которые вам понадобятся. Думаю, было бы здорово, если бы кто-то среди читателей из HTML5/JS/elm-разработчиков создал браузерную версию Пианопьютера, так он стал бы доступен более широкой аудитории.
Если говорить вообще, мне кажется, что компьютеры недостаточно используют именно для исполнения музыки. Я понимаю, что легче взять настоящую фортепианную клавиатуру или записать настоящий инструмент, но вы только посмотрите, чего можно добиться с помощью обычной чаши и 60 строк на Питоне!
Даже дешевый компьютер имеет достаточно элементов управления, чтобы стать полноценной музыкальной станцией: можно петь в микрофон, показывать жесты через веб-камеру, модулировать всякие штуки мышкой и управлять остальным с клавиатуры. Столько средств самовыражения, и для каждого есть библиотека на Питоне… Артистичные хакеры, никто не желает шагнуть в эту сторону?
Автор: moigagoo