录一段音频,把它的音高改变50次并把每一个新的音频匹配到键盘的一个键位,你就能把电脑变成一架钢琴!
一段音频可以被编码为一组数值的数组(或者列表),像这样:
我们可以在数组中每隔一秒拿掉一秒的值来将这段音频的速度变成两倍。
如此我们不仅将音频的长度减半了,而且我们还将它的频率翻倍了,这样使得它拥有比原来更高的音高(pitch)。
相反地,假如我们将数组中每个值重复一次,我们将得到一段更慢,周期更长,即音高更低的音频:
这里提供一个可以按任意系数改变音频速度的任意简单的Python函数:
import numpy as np
def speedx(sound_array, factor):
""" 将音频速度乘以任意系数`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) ]
这个问题更困难的地方在于改变音频长度的同时保持它的音高(变速,音频拉伸(sound stretching)),或者在改变音频的音高的同时保持它的长度(变调(pitch shifting))。
变速
变速可以通过传统的相位声码器(phase vocoder,感兴趣的朋友可以读一下维基百科的页面)来实现。首先将音频分解成重叠的比特,然后将这些比特重新排列使得他们重叠得更多(将缩短声音的长度)或者更少(将拉伸音频的长度),如下图所示:
困难之处在于重新排列的比特可能很严重的互相影响,那么这里就需要用到相位变换来确保它们之间没有影响。这里有一段Python代码,取自这个网页(打不开的话,您懂的。——译者注):
def stretch(sound_array, f, window_size, h):
""" 将音频按系数`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):
# 两个可能互相重叠的子数列
a1 = sound_array[i: i + window_size]
a2 = sound_array[i + h: i + window_size + h]
# 按第一个数列重新同步第二个数列
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))
# 加入到结果中
i2 = int(i/f)
result[i2 : i2 + window_size] += hanning_window*a2_rephased
result = ((2**(16-4)) * result/result.max()) # 归一化 (16bit)
return result.astype('int16')
变调
一旦你实现了变速以后,变调就不难了。如果需要一个更高的音高,可以先将这段音频拉伸并保持音高不变,然后再加快它的速度,如此最后得到的音频将具有原始音频同样的长度,更高的频率,即更高的音高。
把一段音频的频率翻倍将把音高提高一个八度,也就是12个半音。因此,要将音高提高n个半音的话,我们需要将频率乘上系数2^(n/12):
def pitchshift(snd_array, n, window_size=2**13, h=2**11):
""" 将一段音频的音高提高``n``个半音 """
factor = 2**(1.0 * n / 12.0)
stretched = stretch(snd_array, 1.0/factor, window_size, h)
return speedx(stretched[window_size:], factor)
小程序:电脑钢琴
让我们来玩一下我们的变调器。我们先敲碗来确定一个“标准音高”:
[youku id="XNzM1NDM2NTky"]
接下来我们基于之前的音频创造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) # 太灵活了
screen = pygame.display.set_mode((640,480)) # 设置焦点
# 得到键盘的键位的正确顺序的列表
# ``keys`` 如 ['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) # 停止播放并50ms淡出
is_playing[key] = False
就这样我们把计算机变成了一台钢琴!至此,让我为您表演一段土耳其进行曲来表达对您耐心阅读此文的谢意吧:
[youku id="XNzM1NDQ1MDA4"]
如果想自己试试的话,在这里可以下载你需要的所有文件。因为不是所有的人都用Python,我也用Javascript/HTML5(在这儿)实现了一台电脑钢琴,但是不是特别理想。如果有经验丰富的HTML5/JS/elm程序员来改进改进,或者从头重写就太好了。
接下来做什么?
更通常的情况下,我发现计算机很少被用来进行表演性质的演奏。我明白使用钢琴键盘或者直接从乐器录音会容易很多,但是请看看仅仅用一个碗和60行的Python代码就能做到什么!
即便是很便宜的计算机也有如此多的控制来实现一个马马虎虎的音乐台:你可以对着麦克风唱歌,对着摄像头做手势,用鼠标来调制,然后用键盘来完成剩下来的玩意儿。有如此多方式来表现自我,而每种方式又有那么一个Python包……有没有具有艺术天赋的大神加入呀?