有接觸過P5.js的朋友們,想必一定有在Youtube上看過一位大神——Daniel Shiffman 的P5教學影片,他總是用清楚的講解與有趣的小專題帶領程式初學者走過新手期的陣痛,讓大家認識程式世界的無限可能,一直都是鴨編的偶像。
▲Daniel Shiffman 的P5教學影片,截自youtube
Daniel 的Coding Train 教學影片之中,在分享畫面的左下角,總是有他即時拍攝自己的影像,加上他生動的語氣,比起單單只有一行行冰冷、黑底彩字的程式咒語填滿全螢幕畫面更增添趣味性。 事實上,這樣子搭配教學者自拍webcam的形式,在程式教學的影片中很常見,鴨編我總覺得能看到「人」即時反應的樣子,讓在網路上心亂尋找只為了急救寫得亂七八糟程式的自己,在觀看艱澀的程式教學過程感到溫暖許多。
因此,當我也想要開始拍攝程式教學影片的時候,也會想要用這樣的方式。然而有一個問題,就是我太怕鏡頭了。
沒錯,這在長達兩年的遠距google meet 經驗中得到反覆的驗證,我也對webcam中的自己的臉感到非常尷尬,所以也不敢想像自己的臉出現在任何影音平台上。
怎麼辦呢?於是我決定自己用程式寫一張虛擬的「臉」,這張臉會根據我發出的聲音和滑鼠的運動這些僅需簡單接收方式的訊號有變化和回饋,就像擁有表情一樣。
首先,需要:
- 一個導入Tone.js 程式庫的 p5 editor上的空白程式。(如何導入Tone.js,請參考這一個教學)
- 一隻麥克風,總之需要使電腦有音訊接收的管道,因此電腦本身有的麥克風收音也行。
那我們開始實作吧。
1. 先畫一張臉
我們在function draw()與 setup() 以外創建一個新的function:
function drawface(){
noStroke(0);
fill(255,153,48);
ellipse(width/2 ,height/2,150,150); // 用圓簡單地畫一個臉,填個喜歡的顏色,放在畫面中間
}
然後在drawface中加上簡單的五官:
//用弧形畫嘴吧 push(); fill(0,0,0); arc(width/2,height/2, 40,10,QUARTER_PI*0.5,-QUARTER_PI*4.5, PIE); pop(); // 雙眼 push(); noStroke(); fill(255); ellipse(width/2-20,height/2-30,20,10); ellipse(width/2+20,height/2-30,20,10); pop(); // 眉毛 push(); fill(0); rect(width/2-20,height/2-90,20,5); rect(width/2+20,height/2-90,20,5); pop();
按下運作前,記得將 drawface() 加進 function draw()裡面:
function draw() {
background(10,200,200);//墊個喜歡的背景色
drawface();
}
好了,目前為止,我們會得到一張沒有感情的臉。
我們現在來用聲音輸入的數值,來為它加上一些變化。
2. 利用 Tone.js 的 Analyser() 與Meter() 功能,將麥克風接收的聲音做訊號轉換
先在function setup( ){} 外定義三個全域變數:
let fftAnalyser; //音頻轉換器 let MeterAnalyser; //音量偵測 let mic; //最重要的麥克風
再到function setup( ){} 中導入Tone.js資料庫內建的轉換器的功能:
fftAnalyser= new Tone.Analyser({ "type" : "fft", //fft 模式 "size" : 32, //分析訊號的解析度 “smoothingg”:0.9 });
Tone. Analyser() 的功能有兩種模式,「fft」 和 「waveform」。前者是將收到的聲音進行快速傅立葉轉換得到的頻譜,後者是顯現波形。我們這次使用fft 模式,觀察聲音頻率由低到高的變化。
「size」中我們可以決定分析的聲音訊號的解析度(即,你想把聲音的頻率低到高切成多細分析),可以多細呢?從32 到32768只要是2次方的數字都可以,但是因為我們只是想大致上分析就好,也不想吃太多系統資源,所以用32就好了。(註:其實鴨編有發現16也可以運作,但再低的話就會出現錯誤訊息。)
有了Analyser()的功能,我們便可以分析自己講話聲音的高低,然後再依據聲音特定頻率的變化之後去做調變之後,我們再導入另一個要用到的分析功能, Tone.Meter():
MeterAnalyser= new Tone.Meter(0.8);
Tone.Meter() 可截取當下聲音的大小起伏,可以充當分貝儀使用,這樣一來,當人在麥克風前因為情緒起伏而大聲或小聲說話的音量數據,都可以拿來做視覺上的變化。
然後,最最重要的部分,是將我們的程式擷取麥克風的聲音。
// 同樣在setup()中 mic = new Tone.UserMedia(); mic.open(); // 打開吧!麥克風! mic.connect(MeterAnalyser); //將麥克風接入音量分析器 mic.connect(fftAnalyser); //將麥克風接入聲音頻率分析器
一切就緒,在function draw() {} 中打入:
console.log(MeterAnalyser.getValue());
用這行音量的讀數來測試我們的麥克風有沒有開啟成功,如果有開啟,應當會顯示即時的音量變化,然後屏息以待地,按下左上方的play:
登登!你這時可能會看到這一行錯誤訊息。
NotAllowedError: Permission denied
為什麼?怎麼回事?
在你開始打開新分頁爬文之前,先別急,鴨編已經爬好了。在擷取任何設備的輸入源前,都需要存取權詢問,這其實也不難。在網址欄之前,可以看到一個鎖頭。把鎖頭點開,可以看到該頁面存取多少內容(如cookie、攝影機等)
這時你可能會發現,麥克風存取權限是被關閉的。所以你只需要把它開啟,再按下play鍵之後,你的程式就會出現讀數了。
好耶!
3. 觀測數值
做任何數據的應用之前,我們先必須觀察。單單只有純粹的數字太抽象,而且不輸出多筆讀數看起來很混亂,我們可以將部分資料初步地視覺化之後,再來看哪邊有可用之處。
在function draw(){}中,輸入:
console.log(round(MeterAnalyser.getValue())); // 用console讀數看音量變化(用round換成整數較好閱讀)for(let i=0; i<fftAnalyser.getValue().length;i++){
let amp=fftAnalyser.getValue(); // 擷取我們聲音頻率分析器的32筆資料陣列
let y = map(amp[i],-200,0,height,0); // 將每筆資料的數值轉換進0到畫面高度的範圍內以方便觀察
fill(0,y);
rect(i*width/32,height,width/64, y-height); // 將資料的變化反應在由長方形組成的長條圖上
}
你會看到這一些長條,該圖的最左邊就是音頻陣列的第一筆數據,為最低頻,一路往右邊到第32筆為最高頻。同時,也可以在console後台看到當前音量的讀數。這時,可以試著在麥克風前面說說話,或敲敲鍵盤(畢竟是要拍程式教學影片用的),觀察自己聲音的頻率分布在哪些區間。
知道自己的聲音有哪個頻率的長方形有比較明顯的變化以後,抓出訊號在未說話狀態與說話狀態的最大值和最小值範圍,我們便可以用該筆資料做視覺畫的應用。看得差不多了後,可以將以上的程式先註解掉,避免lag程式和干擾畫面。建議可以利用一低頻和一高頻做變化,讓變化更加豐富。
鴨編個人喜歡呼吸或敲鍵盤時的收到的偏高頻,咳嗽和說話時會偏向低頻較多,所以我就從音頻分析器裡擷取分別兩個變化弧度較明顯的變數,各用在眉毛的動態和眨眼上。而嘴吧說話時,以較為直接的音量大小變化做嘴部的開合控制。
4. 用數據控制影像
首先在外程式的外面宣布三組全域變數:
let mouthRange; //嘴吧開合幅度 let eyebrowRange; //眉毛揚起幅度 let eyeOpenRange; //眨眼
再進到第一步宣佈好的drawface(){} 中,把變數套上音頻數據。
以說話音量控制嘴吧開合:
//用弧形畫嘴吧 push(); fill(0,0,0); mouthRange= map(round(MeterAnalyser.getValue()8),-40,0,0,20,true); // 抓出音量在沒說話的最小值與最大值後,用map將數值範圍轉換為方便使用的0-20的範圍內 arc(width/2,height/2-mouthRange*0.5, 40,10+mouthRange*4.5,QUARTER_PI*0.5,-QUARTER_PI*4.5, PIE); //把mouthRange 用來控制嘴部的開合大小 pop();
用音頻的變化量控制眼睛和眉毛
// 雙眼 push(); eyeOpenRange= map(round(fftAnalyser.getValue()[30]),-100,-80,30,0,true); //用第28個變數(第四高的音頻,對清脆的打字聲最有反應)來控制眨眼與張眼 noStroke(); fill(255); ellipse(width/2-20,height/2-30,20,eyeOpenRange); ellipse(width/2+20,height/2-30,20,eyeOpenRange); pop(); // 眉毛 push(); eyebrowRange=map(round(fftAnalyser.getValue()[5]),-200,-10,0,50);//用第5個變數(低頻)來控制眉毛起伏 fill(0); rect(width/2-20,height/2-90+eyebrowRange,20,5); rect(width/2+20,height/2-90+eyebrowRange,20,5); pop();
按下運作鍵,對麥克風說說話測試,你將發現這個本來沒有感情的小圓球,已經被賦予了生命,你說話時他也張開嘴吧,就像另一個你一樣。
還可以加一點變化,例如讓小臉的五官隨著滑鼠移動而有小小轉動的效果:
let faceX=map(mouseX,-500,300,-10,10);
let faceY=map(mouseY,-500,300,-10,10); //讀取滑鼠位置push();
//眼睛
eyeOpenRange= map(round(fftAnalyser.getValue()[30]),-100,-80,30,0,true);
translate(faceX,faceY);//移動加在這裡!
noStroke();
fill(255);
ellipse(width/2-20,height/2-30,20,eyeOpenRange);
ellipse(width/2+20,height/2-30,20,eyeOpenRange);
pop();
// 眉毛
push();
translate(faceX,faceY);//移動加在這裡!
eyebrowRange=map(round(fftAnalyser.getValue()[5]),-200,-10,0,50);fill(0);
rect(width/2-20,height/2-90+eyebrowRange,20,5);
rect(width/2+20,height/2-90+eyebrowRange,20,5);
pop();
push();
//嘴吧
push();
fill(0,0,0);
translate(faceX,faceY);//移動加在這裡!
mouthRange= map(round(MeterAnalyser.getValue()8),-40,0,0,20,true);
arc(width/2,height/2-mouthRange*0.5, 40,10+mouthRange*4.5,QUARTER_PI*0.5,-QUARTER_PI*4.5, PIE);
pop();
到這裡,基本上我們完成了。然而,這時我們要怎麼讓這個小化身,在錄製教學其他程式影片的時候安靜的待在程式畫面的右下角呢?
這個時候,我們就要用Instance Mode以開啟多個canvas在同個網頁之中。
5. 全部包起來!
我們需要在程式的最外面定義一個變數把所有的東西包起來。
var face = function(p) { //取一個名字和開頭 //你的整個程式 }
這個function 括號裡的 p 可以用任何名稱替代,但鴨編認為以好打為優先。
因為你必須把你剛剛寫的所有程式、變數等的功能開頭加上該名稱。
let MeterAnalyser; 改成 p. MeterAnalyser;
function setup(){} 變成:
p.setup = function{ //你的function setup裡的內容物 p.rectMode(p.CENTER); p.createCanvas(300, 300); p.fftAnalyser= new Tone.Analyser({ "type" : "fft", "size" : 32, "smoothingg":0.9 }); p.MeterAnalyser= new Tone.Meter(0.8); p.mic = new Tone.UserMedia(); p.mic.open(); p.mic.connect(p.MeterAnalyser); p.mic.connect(p.fftAnalyser); }
//function draw() 也是變成 p.draw = function(){ p.background(10,200,200); p.drawface(); } p.drawface= function(){//drawface裡的一切 p.facemoveX=p.map(p.mouseX,-500,300,-5,5); p.facemoveY=p.map(p.mouseY,-500,300,-5,5); . . . //舉凡事map()、fill()、ellipse()等圖形相關、for迴圈裡的變數、呼叫先前定義的變數、pop()、push()等都需加上開頭 }
這是一個浩大的工程,但也可以趁機找找看哪些東西可以優化。
所有的東西都包好好後,我們在目前所有程式的最外面 加上:
var myface = new p5(face,’newface’);
myface 就是原本程式的分身,並且可以用後面的’newface’作為該canvas在style.css排版裡所用的id 。
將style.css打開,加上:
#newface{ float:right; // 放在網頁的頁面右邊 margin-top: 5px; //留一點與上面的空格 //想怎麼排就怎麼排! }
再新添一個空白js檔案(我取名為”main.js”),並將其加到主要的html檔案的body之中:
<body>
<script src=“sketch.js”></script> //我是舊的程式
<script src=“main.js”></script> //我是新創建的程式
</body>
最後,到main.js 裡創建上主要coding 要用的畫布程式:
function setup(){
createCanvas(500,500);
textAlign(CENTER);
textSize(40);
console.log("hello");//測試用
}
function draw(){
background(0);
fill(255);
text("your tutorial code here !" ,width/2,height/2);
}
按下運作:
大功告成!這樣一來,就可以用main.js 演示錄製程式教學影片,又可以有自己的小化身在頁面裡,讓Coding 影片變有趣!點我去程式編輯頁面,開啟麥克風權限後動手寫寫看!
延伸閱讀: p5.js 、 Tone.js、手機三軸感測器實作教學 —— 單手拍手機?!
參考資料:
https://github.com/Tonejs/Tone.js/issues/490
https://github.com/processing/p5.js/wiki/Global-and-instance-mode
一隻喜歡科技藝術、遊戲和冷知識的鴨子。