p5.js 與 Tone.js 實作教學—— 不含任何真實貓咪的喵喵琴

大家好,我是鴨編,一個冷知識愛好者。

我喜歡去查各種奇怪的名詞組合,看能不能查到奇怪的新知識。

有一天,突然想要找看看有沒有全是貓咪叫聲的 MIDI 樂器,心想現代人如此愛貓,應該有很多人渴望使用貓貓的叫聲來寫曲子,那應該有為這樣子需求開發的喵喵叫聲合成器。

但還沒找到的時候,小編的注意力就被一個奇怪詞彙吸引住了。

 

貓琴。

▲ 取自 La Nature, 1883年,圖源:https://zh.wikipedia.org/wiki/%E7%8C%AB%E7%90%B4#/media/File:Cat_piano_1883.jpg

 

英文稱為Cat organ ,德語名為 Katzenorgel,是一種記載於少量文獻中的樂器。最早的記載是於阿塔納奇歐斯·基爾學的著作《Musurgia Universalis》(1650)中,沒有圖片記錄,只有文字敘述該樂器讓人聽了會非常不適的使用方式與長相。

把各種體型的貓咪放進特製的盒子裡,而他們的尾巴處都有尖銳物。藉由敲打不同琴鍵觸發尖銳物扎到貓咪的尾巴,使貓咪發出時而痛苦時而憤怒的叫聲。

後有其它記載此樂器的文獻,多有再提到將貓咪按照叫聲音色排列放置至木盒中,但所有的共通點都是使貓咪痛苦而發出喵喵聲,是貴族引以取樂的樂器。

不過幸好沒有任何正式文獻證明此樂器有被實踐出來過,小編也覺得這樣子的樂器還是就停留在想像之中吧。

 

不過,是否有借助科技的力量,以「沒有任何真實貓咪」為原則,做出一個類似貓琴效果的電子樂器?小編目前找到的貓貓叫聲樂器,多半有取樣自真實貓咪叫聲,再經由調變成不同音高與混響供人使用在DAW中。

 

於是小編就開始思考,來運用p5.js 自己寫出一個在瀏覽器中就可以供人彈奏的喵喵鍵盤。而小編也算是嘗試成功了,就把寫程式的過程放上來和大家分享。

 

1. 引入Tone.js到文件中

 

除了p5自己的sound library 外,還有一個叫做 Tone.js Web Audio 框架。它和p5.sound一樣都支援電子音樂創作時常見的邏輯和功能,如濾波器、ADSR、振盪器等,常用DAW的人,會覺得Tone.js提供的功能十分親切,而且Tone.js p5.sound不一樣的是,他有自己的取樣器,使用者可以輸入一個既有的mp3檔案,取樣器就可以自動幫助使用者生成不一樣的音高。

因為小編這裡是使用p5.js的桌面版編輯器,所以不需載套件直接將來源貼到文件的HTML頁面(預設檔名index.html)就可以引入了。

<script src=“https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.77/Tone.js"></script>

註:若是使用p5 editor 的話,請在index.html中貼入以下網址:

<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.9/Tone.js" integrity="sha512-nUjml8mN4CNYqBAy0ndDrd8pJV/eTtBNDsnvNtPqozx9/BccUeWSoKW14qWkQUnhuh8E3m+yra3qdzM68lqPEQ==" crossorigin=“anonymous"></script>

截至小編寫作的2022年九月為止,這兩個寫法運行起來沒有問題。

然後,在function setup() 之中打上 console.log(Tone):

function setup(){
console.log(Tone);
}

運行後若有出現Tone.js 的字樣和版本名,代表引入成功,可以使用其功能了。

 

2. 先解決聲音

 

這就馬上跳入Tone.js好玩的部分了。Tone.js有提供很多種振盪器(oscillator),有支援FMAM、白噪音等都收錄在「Instrument」的頁面,或是沒有概念從何開始的話,可以到這個 Preset 的頁面看振盪器的用法,改到喜歡的聲音為止。不過要注意的是,這些振盪器都是預設 monophonic,即一次只能演奏一個音,不能演奏和弦。如果想要一次發出很多個音的話,一定要和小編一樣使用Tone.PolySynth() 才能大合唱。

首先先宣告:

let synth =  new Tone.PolySynth().toDestination();

PolySynth()依據文件所述本身並不是一個樂器,它是用來使其他振盪器一次可以使用多個音的功能。

因此,在PolySynth() 的括號中必須打入想要的用的振盪器種類,就可以了。

小編試過了之後,認為支援ADSR 濾波器的 MonoSynth 最符合需求:

synth = new Tone.PolySynth(Tone.MonoSynth).toDestination(); //.toDestination() 指連接到聲音輸出端

然後就可以開始設定數值了。

 

synth.set({
//設定數值請放這
})

各種振盪器都有其支援與不支援的變數,細節請參照Tone.js的文件。

仔細聽,貓咪的「喵」聲粗糙,因此不適合正弦波,但方波又太尖銳,因此使用了折衷的鋸齒波。「喵」聲是有明顯的ADSR幅度的,並且在特定頻率有較明顯的聲音,因此小編使用了bandpass帶通濾波器,並賦予了帶通濾波器自身ADSR,來調整聲音質感。

大家可以自行試試看數值,這裡是小編調整後最相近於喵喵聲的數據:


synth.set({
"oscillator": {
 "type": "sawtooth",},

 "filter": {
   "Q": 6,
   "type": "bandpass",
   "rolloff": -12,
},
"envelope": {
   "attack": 0.1,
   "decay": 0.1,
   "sustain": 0.3,
   "release": 2,
},
 "filterEnvelope": {
  "attack": 0.04, 
  "attackCurve":"exponential", 
  "decay": 0.5, 
  "decayCurve": "exponential",
  "sustain": 0.3,
  "release": 0.6,
  "baseFrequency": 800,
  "octaves": 2,},
});

      

3.  把鍵盤變成琴鍵

 

首先在function setup() 中設定好畫布大小

準備好琴鍵對應的陣列

let notes = ['C', ‘C#','D','Eb','E','F','F#','G','G#','A','Bb','B','C',];
let keys=[‘a’,'w','s','e','d','f','t','g','y','h','u','j','k',];
let octave; //設定一個八度,這裡在setup宣告預設數值為3

小編這裡示範的是一個八度的鍵盤。鍵盤上的「A」對應至中央「C」,「W」對應至「C#」(升C),「S」對應至「D以此類推,直到「K」鍵對應至下一個八度的「C」為止。

設定好鍵盤後,在function draw() function setup() 以外建立一新的 function keyPressed(){}

然後在裡面建立以下迴圈:

 function keyPressed(){
for(let i=0;i<12;i++){ //一個八度C B
if(key==keys[i]){
synth.triggerAttackRelease(notes[i]+octave.toString(), "8n");
}
if(key==keys[12]){ // 此為下一個八度的C,即K鍵,因為八度比別人多1所以另外算
let high_c=octave+1;
synth.triggerAttackRelease(notes[12]+(octave+1).toString(), "8n");}// 從第一個鍵至第12個鍵(一個八度,cb),將note與鍵盤琴鍵對應
}

發出聲音是靠 synth.triggerAttackRelease( [音高] , [音長時間單位]);

音高可以填入”C3” 等這種string格式的音高,所以小編是將note陣列中的音符代號再加上先前宣告的八度(octave)數值,至於8n是指八分音符的長度。

小編在keyPress(){} 中寫了使用上下鍵換升降八度的功能:

if(keyCode== UP_ARROW){octave+=1; }
if(keyCode== DOWN_ARROW){octave-=1;}

這樣一來,運行程式後按下鍵盤上對應字母就可以發出聲音了!

4. 增加效果器

 

除此之外,我們也可以利用串接Tone.js的效果器來調變聲音。

小編以Freeverb功能和Distortion做個示範。

let freeverb = new Tone.Freeverb(0.5,3000).toDestination(); // new Tone.Freeverb([空間大小的數值],[阻尼值])
let distort = new Tone.Distortion(0.8).toDestination(); // new Tone.Distortion([破音數值])

設定好了之後要如何將效果器套用到聲音上呢?

如果只要使用單一的特效,可以使用connect(),如 synth.connect(freeverb);

但如果有一個以上的效果器的話,則可以使用 chain()

synth.chain(distort,freeverb);

注意,chain套用效果器有順序性,如以上寫法是先套用扭曲,再套用freeverb

5. 效果器數值調變

 

假設若是想要以滑桿控制Freeverb效果器數值的話,先到 index.html文件中引入p5gui程式庫。

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/addons/p5.dom.min.js"></script>

以上是給p5的桌面編輯器,若是使用p5 editor則不用增加這一行。

然後宣告一個滑桿slider:

let Fslider = createSlider(0.001,1,0.5,0.1);//  createSlider(最小值,對大值,開始的預設數值,每一階增加多少);

draw(){} 中宣告:

freeverb.roomSize.value= Fslider.value(); //freeverb中的空間大小數值交由滑桿值決定

然後運行程式,操控滑桿,就可以了。

 

6. 琴鍵的運動效果

 

先到外部宣告一個新的陣列,用途為偵測按下與否。

let pressed= [ ]; //預設沒被按下

到我們剛剛寫好的function keyPressed(){} 改成:

for(let i=0;i<12;i++){
if(key==keys[i]){
synth.triggerAttackRelease(notes[i]+octave.toString(), "8n"); pressed[i]=1; //pressed[]=1為按下
}
}
if(key==keys[12]){
let high_c=octave+1;
synth.triggerAttackRelease(notes[12]+(octave+1).toString(), “8n");pressed[12]=1;//pressed[]=1為按下
}

draw setup 外再寫一個

function keyReleased(){ //keyPressed相對,為偵測按鍵放開
  for(let i=0;i<13;i++){
if(key==keys[i]){pressed[i]=0;
// 13個鍵分別沒有按壓時pressed[]=0,即沒被按下
}
}
}

然後重點來了,宣告一個專門控制及畫出琴鍵的funciton:

function keyboard_model( _pressed,positionX,positionY,positionZ,keycolor,keylength,keywid){
if(_pressed==1){ //若該鍵被按下
translate( positionX, positionY,positionZ-20); // 則模型視覺被往下按
}
else{translate( positionX, positionY,positionZ);} // 除此以外皆保持正常維持原位置
fill(keycolor); //琴鍵填上顏色,0 為黑,255為白
box(keywid,keylength,30); //琴鍵長方體
pop();
}

然後在draw(){} 添加以下這一堆,在裡面將琴鍵排列好,賦予三維座標,琴鍵顏色,琴鍵大小寬度等數值

因為鋼琴的黑白鍵排列是不規則的,所以小編只好慢慢寫:

//keyboard_model(對應判斷按下的是哪一個press[]負責判斷,琴鍵x軸座標,琴鍵y軸座標,琴鍵顏色,琴鍵高,琴鍵寬); 不用照小編寫的長寬高和坐標
//keywhite
keyboard_model(pressed[0],-width/13.0*12,5,-10,255,150,40);
keyboard_model(pressed[2],-width/13.0*11,5,-10,255,150,40);
keyboard_model(pressed[4],-width/13.0*10,5,-10,255,150,40);
keyboard_model(pressed[5],-width/13.0*9,5,-10,255,150,40);
.
.
.
keyboard_model(pressed[12],-width/13.0*5,5,-10,255,150,40);

///keyblack

keyboard_model(pressed[1],-width/13.0*11-15,-20,15,0,100,25);
keyboard_model(pressed[3],-width/13.0*10-10,-20,15,0,100,25);
.
.
.
keyboard_model(pressed[10],-width/13.0*6-15,-20,15,0,100,25);
7. 大功告成

 

恭喜,「不用任何真實貓咪」的喵喵琴做好了!

目前還只是簡單的版本,大家可以再繼續幫貓貓琴增加新效果、新功能!

如果有覺得小編寫的不清楚,大喊「我想看完整版」的話,這裡有完整的code和新加的東西可以參考。

 

同場加映:取樣器

 

先前有提到過,Tone.js有一個強大的取樣功能,可以將既有的音檔轉成midi音高,雖然有違「不使用任何真實貓咪」的原則,這裡還是給大家參考一下。

let sampler= new Tone.Sampler({
urls: {
  "E4": "/data/cat-meow01.wav", //引入音檔路徑,然後宣告該音檔的基準音高,音檔越多取樣器的參照資訊就會越多、越好
},
release:1,
}).toDestination();
//若要觸發聲音則像前面synth一樣:
sampler.triggerAttackRelease([音高], [音長時間]);

Tone.js 還有很多功能,小編就介紹到這,留給大家慢慢探索囉!

 

參考資料:

貓琴:http://imaginaryinstruments.org/the-cat-piano-katzenklavier-piano-de-chats/

https://en.wikipedia.org/wiki/Cat_organ#cite_note-8

Tone.js 官網:https://tonejs.github.io/

 

 

Leave a Comment

發佈留言必須填寫的電子郵件地址不會公開。

相關文章

回到頂端