錄一段音訊,把它的音高改變50次並把每一個新的音訊匹配到鍵盤的一個鍵位,你就能把電腦變成一架鋼琴!
一段音訊可以被編碼為一組數值的陣列(或者列表),像這樣:
我們可以在陣列中每隔一秒拿掉一秒的值來將這段音訊的速度變成兩倍:
如此我們不僅將音訊的長度減半了,而且我們還將它的頻率翻倍了,這樣使得它擁有比原來更高的音高(pitch)。
相反地,假如我們將陣列中每個值重複一次,我們將得到一段更慢,週期更長,即音高更低的音訊:
這裡提供一個可以按任意係數改變音訊速度的任意簡單的Python函式:
1 2 3 4 5 6 7 |
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程式碼,取自這個網頁(打不開的話,您懂的。——譯者注):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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):
1 2 3 4 5 |
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個變調的音高,從很低到很高:
1 2 3 4 5 |
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] |
接下來根據這個檔案中的順序,我們把每一個音訊匹配到鍵盤的一個鍵位,如下圖所示:
我們只需要在程式碼中告訴計算機當一個鍵按下來的時候播放其對應的聲音,然後當按鍵鬆開後停止播放就可以了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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包……有沒有具有藝術天賦的大神加入呀?