p5.jsでシンセサイザーを作る 第18話 PCのキーボードで演奏する
Javascriptとp5.jsを使って、オリジナルなシンセサイザーを作るプログラミングの記事です。とりあえず何を作るのかを手っ取り早くお伝えしたいので、第0話で公開している完成品もチェックしてみてください。
PCのキーボードで演奏する
p5.soundを使って音階の設定を行い、マウスクリックで奏でるところまできました。今回は操作をPCのキーボードに割り当てます。
第17話のコードを記述
前回の記事から話がつながっているので、ここまでつくってきたコードを再掲します。ここから、PCのキーを画面上のキーボードに割り当てしていきます。
let keySize = 50; // キーのサイズ
let keyTop = keySize / 1.4; // キートップのサイズ
let topSize = 7; // キートップの位置を中央に寄せる幅
let keyX = 50; // キーの位置X
let keyY = 150; // キーの位置Y
let keyInterval = 50;
let keyStat = [false, false, false, false, false, false, false, false]; // 白鍵の判定
let keyStatS = [false, false, false, false, false, false]; // 黒鍵の判定
let strokeValue = 2; // 枠線の太さ
let osc;
let baseFreq = 0;
let freqKey = [130.813,146.832,164.814,174.614,195.998,220.000,246.942,261.626];
let freqKeyS = [138.591,155.563,0,184.997,207.652,233.082];
function setup() {
createCanvas(500, 400);
osc = new p5.Oscillator('sawtooth'); // ハコの中にノコギリ波を入れる
}
function draw() {
background(250); //背景の色
drawHousing();
drawInterface();
}
function drawHousing(){
stroke(150, 100, 100);
strokeWeight(strokeValue);
////////ここから白鍵////////////////
fill(230, 230, 230); // キーのベース
for(let i = 0; i < 8; i++){
rect(keyX + keyInterval * i, keyY, keySize, keySize);
}
fill(100, 100, 100); // キーの影部分の色
for(let i = 0; i < 8; i++){
triangle(
keyX + keyInterval * i, keyY, //左上
keyX + keyInterval * i, keyY + keySize, //左下
keyX + keyInterval * i+ keySize, keyY + keySize //右下
);
}
fill(250, 250, 250) // キーのトップ部分
for(let i = 0; i < 8; i++){
rect(keyX + keyInterval * i + topSize, keyY + topSize, keyTop, keyTop);
}
////////ここから黒鍵////////////////
fill(230, 230, 230); // キーのベース
for(let i = 0; i < 6; i++){
if(i != 2){
rect(keyX + keyInterval * i + keySize / 2, keyY - keySize, keySize, keySize);
}
}
fill(100, 100, 100); // キーの影部分の色
for(let i = 0; i < 6; i++){
if(i != 2){
triangle(
keyX + keyInterval * i + keySize / 2, keyY - keySize, //左上
keyX + keyInterval * i + keySize / 2, keyY, //左下
keyX + keyInterval * i + keySize / 2 + keySize, keyY //右下
);
}
}
fill(250, 250, 250) // キーのトップ部分
for(let i = 0; i < 6; i++){
if(i != 2){
rect(keyX + keyInterval * i + topSize + keySize / 2, keyY- keySize + topSize, keyTop, keyTop);
}
}
}
function drawInterface(){
for(let i = 0; i < 8; i++){
if (keyStat[i] == true){ // オレンジ色で白鍵のキートップを塗り替える
fill(255, 125, 0);
rect(keyX + keyInterval * i + topSize, keyY + topSize, keyTop, keyTop);
}
}
for(let i = 0; i < 6; i++){
if (keyStatS[i] == true){ // オレンジ色で白鍵のキートップを塗り替える
fill(255, 125, 0);
rect(keyX + keyInterval * i + topSize + keySize / 2, keyY- keySize + topSize, keyTop, keyTop);
}
}
}
function noteOn(){
for (let i = 0; i < 8; i++) {
if (keyStat[i] == true){ //キーが押されたら
osc.freq(baseFreq); //oscのハコに周波数を入れる
osc.start(); //オシレーターを発音する
}
}
for (let i = 0; i < 6; i++) {
if (keyStatS[i] == true){ //キーが押されたら
osc.freq(baseFreq); //oscのハコに周波数を入れる
osc.start(); //キーが押されたらオシレーターを発音する
}
}
}
function noteOff(){
for (let i = 0; i < 8; i++) {
if (keyStat[i] == false){
osc.stop(); //キーが離されたらオシレータを止める
}
}
for (let i = 0; i < 6; i++) {
if (keyStat[i] == false){
osc.stop(); //キーが離されたらオシレータを止める
}
}
}
function touchStarted() {
for (let i = 0; i < 8; i++) {
if (
mouseX > keyX + keySize * i &&
mouseX < keyX + keySize * i + keySize &&
mouseY > keyY&&
mouseY < keyY + keySize
) {
baseFreq = freqKey[i];
keyStat[i] = true;
noteOn();
}
}
for (let i = 0; i < 6; i++) {
if (
mouseX > keyX + keySize / 2 + keySize * i &&
mouseX < keyX + keySize / 2 + keySize * i + keySize &&
mouseY > keyY - keySize &&
mouseY < keyY
) {
if(i != 2){
baseFreq = freqKeyS[i];
keyStatS[i] = true;
noteOn();
}
}
}
}
function touchEnded() {
for (let i = 0; i < 8; i++) {
keyStat[i] = false;
noteOff();
}
for (let i = 0; i < 6; i++) {
keyStatS[i] = false;
noteOff();
}
}
マウスクリックでそれぞれのキーが反応するところまでは問題ありません。
ここからは、p5.soundを使ったオシレータが押されたキーに対応して音階を奏でるようにしていきます。
①宣言の部
変数の宣言には、キーを割り当てる配列を追記します。
let keyboard = ["s","d","f","g","h","j","k","l"]; //白鍵のキー
let keyboardS = ["e","r","t","y","u","i"]; //黒鍵のキー
①初期化の部
function setup() {
createCanvas(500, 400);
osc = new p5.Oscillator('sawtooth'); // ハコの中にノコギリ波を入れる
}
特に変更せず進んでいきます。
③実行の部
function draw() {
background(250); //背景の色
drawHousing();
drawInterface();
}
ここも、特に変更はありません。
④動作の部
この部分では、前回マウスクリックの開始と終了を検知して、noteOn()とnoteOff()を呼び出していました。
今回は同じロジックでPCのキー入力開始と終了を検知し、noteOn()とnoteOff()を呼び出す記述を追記します。
キーボードの入力とキーボードが離されたことを検知する関数
function keyPressed(){
for (let i = 0; i < 8; i++){ //白鍵の処理
if (key == keyboard[i]){
baseFreq = freqKey[i];
keyStat[i] = true;
noteOn();
}
}
for (let i = 0; i < 6; i++){ //黒鍵の処理
if (key == keyboardS[i]){
baseFreq = freqKeyS[i];
keyStatS[i] = true;
noteOn();
}
}
}
function keyReleased() {
for (let i = 0; i < 8; i++) {
keyStat[i] = false;
noteOff();
}
for (let i = 0; i < 6; i++) {
keyStatS[i] = false;
noteOff();
}
}
ここでは、キーボードが入力された時に自動的に呼び出される関数keyPressed()とキーボードが離された時に自動的に呼び出される関数keyReleased()という二つの関数を記述しました。この二つの関数はtouchStarted()や、touchEnded()と同様に、p5.jsのライブラリに標準実装されている機能で、操作を自動検知して呼び出してくれます。
keyPressed()の中身については、押されたキーの配列要素番号と、周波数の配列要素番号がついになる様にfor文の中で処理しています。
これを白鍵と黒鍵でそれぞれ処理しています。
keyReleased()の中身については、単に離されたキーを検出したら、keyStat[]とkeyStatS[]をfalseにして、音を止めるためのnoteOff();を呼び出すようにしてみました。
なんか、もうこれだけでいけそう。。。。?
実行してみます。
確かにドレミファソラシドーという感じで音は出ました!
嬉しいのですが、その後、キーボードをガチャガチャと押しまくると、音が途切れ途切れ。ブツブツと途切れてしまいます。
音がブツ切れになる原因
function keyReleased() {
for (let i = 0; i < 8; i++) {
keyStat[i] = false;
noteOff();
}
for (let i = 0; i < 6; i++) {
keyStatS[i] = false;
noteOff();
}
}
この部分では、キーが離されたら、とにかくnoteOff()を呼び出して音を止めてしまっている様です。
楽器の演奏でいうところのレガートの状態になると音が途切れてしまうという現象が出ている様です。
ここは結構重要で、多くの楽器というものは、入力(打つ、弾く、押すなど)が終わると即、音が消える。というものではありません。何かしらの余韻を残して音が消えるべきです。
至極単純でもいいので、そのような機構によって発音を止める様に改良します。
改良方法
シンプルなアプローチで発音が止まるまでの処理を記述していきます。
今回は、最後の発音からしばらく置いて音が止まるという考え方で実装していきます。
①宣言の部に変数を追加
let noteCount = 0;
let noteStartValue = 30;
let isPlaying = false;
noteCountは、最後の音が出てから止まるまでのカウントダウンをする役割に使用します。
noteStartValueは、発音がされるたびにnoteCountがリセットされるための初期値を格納しています。
isPlayingは何かしらの発音がされているかどうかを判別するために使用します。
③実行の部に発音のカウントダウンを入れる
function draw() {
background(250); //背景の色
drawHousing();
drawInterface();
if (isPlaying == true){
noteCount--
if(noteCount < 0){
noteOff();
}
}
}
draw()の中にあるif文がカウントダウンです。音が出ている(isPlayingがtrue)の間、カウントダウンが実行されます。noteCountが0を下回ると、noteOff()が呼び出される仕組みです。
④動作の部を書き換える
function noteOn(){
for (let i = 0; i < 8; i++) {
if (keyStat[i] == true){ //キーが押されたら
osc.freq(baseFreq); //oscのハコに周波数を入れる
noteCount = noteStartValue;
isPlaying = true;
osc.start(); //オシレーターを発音する
}
}
for (let i = 0; i < 6; i++) {
if (keyStatS[i] == true){ //キーが押されたら
osc.freq(baseFreq); //oscのハコに周波数を入れる
noteCount = noteStartValue;
isPlaying = true;
osc.start(); //キーが押されたらオシレーターを発音する
}
}
}
function noteOff(){
osc.stop();
}
noteOnの中。発音の直前の処理でnoteCountにnoteStartValueを代入します。これがタイマーのリセットの働きをします。
その後、isPlayingをtrueにすることで、発音中というフラグに切り替えます。
noteOff()についてはそのまま音を止める動作のみ担当しています。
実行してみましょう
うまくいっています。今回の場合noteStartValue = 30という間を置いていますが、これはp5.jsが毎秒60フレームで動作しているため、その半分。つまり0.5秒で音が止まるようにしているからです。
もっとシンセサイザーとして余韻を表現するためにはエンベロープという機能が必要になりますが、p5.soundのライブラリにもエンベロープを実装する機能が備わっています。今後のブラッシュアップでこの辺りも実装していけば、表情豊かな演奏ができる様になります。
今後はエンベロープをはじめボリューム調整やフィルターの取扱いなども行なっていきます。どんどんシンセっぽくなっていきます。
この記事のコード全文
let keySize = 50; // キーのサイズ
let keyTop = keySize / 1.4; // キートップのサイズ
let topSize = 7; // キートップの位置を中央に寄せる幅
let keyX = 50; // キーの位置X
let keyY = 150; // キーの位置Y
let keyInterval = 50;
let keyStat = [false, false, false, false, false, false, false, false]; // 白鍵の判定
let keyStatS = [false, false, false, false, false, false]; // 黒鍵の判定
let strokeValue = 2; // 枠線の太さ
let osc;
let baseFreq = 0;
let freqKey = [130.813,146.832,164.814,174.614,195.998,220.000,246.942,261.626];
let freqKeyS = [138.591,155.563,0,184.997,207.652,233.082];
let keyboard = ["s","d","f","g","h","j","k","l"]; //白鍵のキー
let keyboardS = ["e","r","t","y","u","i"]; //黒鍵のキー
let noteCount = 0;
let noteStartValue = 30;
let isPlaying = false;
function setup() {
createCanvas(500, 400);
osc = new p5.Oscillator('sawtooth'); // ハコの中にノコギリ波を入れる
}
function draw() {
background(250); //背景の色
drawHousing();
drawInterface();
if (isPlaying == true){
noteCount--
if(noteCount < 0){
noteOff();
}
}
}
function drawHousing(){
stroke(150, 100, 100);
strokeWeight(strokeValue);
////////ここから白鍵////////////////
fill(230, 230, 230); // キーのベース
for(let i = 0; i < 8; i++){
rect(keyX + keyInterval * i, keyY, keySize, keySize);
}
fill(100, 100, 100); // キーの影部分の色
for(let i = 0; i < 8; i++){
triangle(
keyX + keyInterval * i, keyY, //左上
keyX + keyInterval * i, keyY + keySize, //左下
keyX + keyInterval * i+ keySize, keyY + keySize //右下
);
}
fill(250, 250, 250) // キーのトップ部分
for(let i = 0; i < 8; i++){
rect(keyX + keyInterval * i + topSize, keyY + topSize, keyTop, keyTop);
}
////////ここから黒鍵////////////////
fill(230, 230, 230); // キーのベース
for(let i = 0; i < 6; i++){
if(i != 2){
rect(keyX + keyInterval * i + keySize / 2, keyY - keySize, keySize, keySize);
}
}
fill(100, 100, 100); // キーの影部分の色
for(let i = 0; i < 6; i++){
if(i != 2){
triangle(
keyX + keyInterval * i + keySize / 2, keyY - keySize, //左上
keyX + keyInterval * i + keySize / 2, keyY, //左下
keyX + keyInterval * i + keySize / 2 + keySize, keyY //右下
);
}
}
fill(250, 250, 250) // キーのトップ部分
for(let i = 0; i < 6; i++){
if(i != 2){
rect(keyX + keyInterval * i + topSize + keySize / 2, keyY- keySize + topSize, keyTop, keyTop);
}
}
}
function drawInterface(){
for(let i = 0; i < 8; i++){
if (keyStat[i] == true){ // オレンジ色で白鍵のキートップを塗り替える
fill(255, 125, 0);
rect(keyX + keyInterval * i + topSize, keyY + topSize, keyTop, keyTop);
}
}
for(let i = 0; i < 6; i++){
if (keyStatS[i] == true){ // オレンジ色で白鍵のキートップを塗り替える
fill(255, 125, 0);
rect(keyX + keyInterval * i + topSize + keySize / 2, keyY- keySize + topSize, keyTop, keyTop);
}
}
}
function noteOn(){
for (let i = 0; i < 8; i++) {
if (keyStat[i] == true){ //キーが押されたら
osc.freq(baseFreq); //oscのハコに周波数を入れる
noteCount = noteStartValue;
isPlaying = true;
osc.start(); //オシレーターを発音する
}
}
for (let i = 0; i < 6; i++) {
if (keyStatS[i] == true){ //キーが押されたら
osc.freq(baseFreq); //oscのハコに周波数を入れる
noteCount = noteStartValue;
isPlaying = true;
osc.start(); //キーが押されたらオシレーターを発音する
}
}
}
function noteOff(){
osc.stop();
}
function keyPressed(){
for (let i = 0; i < 8; i++){ //白鍵の処理
if (key == keyboard[i]){
baseFreq = freqKey[i];
keyStat[i] = true;
noteOn();
}
}
for (let i = 0; i < 6; i++){ //黒鍵の処理
if (key == keyboardS[i]){
baseFreq = freqKeyS[i];
keyStatS[i] = true;
noteOn();
}
}
}
function keyReleased() {
for (let i = 0; i < 8; i++) {
if(key == keyboard[i]){
keyStat[i] = false;
}
}
for (let i = 0; i < 6; i++) {
if(key == keyboardS[i]){
keyStatS[i] = false;
}
}
}
function touchStarted() {
for (let i = 0; i < 8; i++) {
if (
mouseX > keyX + keySize * i &&
mouseX < keyX + keySize * i + keySize &&
mouseY > keyY&&
mouseY < keyY + keySize
) {
baseFreq = freqKey[i];
keyStat[i] = true;
noteOn();
}
}
for (let i = 0; i < 6; i++) {
if (
mouseX > keyX + keySize / 2 + keySize * i &&
mouseX < keyX + keySize / 2 + keySize * i + keySize &&
mouseY > keyY - keySize &&
mouseY < keyY
) {
if(i != 2){
baseFreq = freqKeyS[i];
keyStatS[i] = true;
noteOn();
}
}
}
}
function touchEnded() {
for (let i = 0; i < 8; i++) {
keyStat[i] = false;
noteOff();
}
for (let i = 0; i < 6; i++) {
keyStatS[i] = false;
noteOff();
}
}
実際にコピペして試してみてください!