見出し画像

p5.jsでシンセサイザーを作る 第21話 ツマミを使ったコントロール

Javascriptとp5.jsを使って、オリジナルなシンセサイザーを作るプログラミングの記事です。とりあえず何を作るのかを手っ取り早くお伝えしたいので、第0話で公開している完成品もチェックしてみてください。

開発の大詰めです。

今までパーツを作り、音を出して、パラメータの指定をしてきましたが、実際に演奏するためのインターフェースと発音の機構を一つにまとめる段階にきました。
開発日記のクライマックスですね。

長くなりますが、ご容赦ください。

今回使うコード

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 env;
let decay = 0.50;

let fil;
let cutOffValue = 1550.8;
let resValue = 21.9;

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'); // ハコの中にノコギリ波を入れる
  env = new p5.Envelope(); //ここはエンベロープを入れるハコ
  fil = new p5.LowPass(); //ここはローパスフィルターを入れるハコ
}

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){ //キーが押されたら
      env.setADSR(0.01,decay,0.0,0.0); // ADSRの値を入れておく
      fil.set(cutOffValue,resValue);//カットオフとレゾナンスを設定
      osc.connect(fil);//オシレータにフィルターを接続
      osc.freq(baseFreq); //oscのハコに周波数を入れる
      noteCount = noteStartValue;
      isPlaying = true;
      osc.start(); //オシレーターを発音する
      env.play(osc); //発音したオシレータを制御する
    }
  }
  for (let i = 0; i < 6; i++) {
    if (keyStatS[i] == true){ //キーが押されたら
      env.setADSR(0.01,decay,0.0,0.0); // ADSRの値を入れておく
      fil.set(cutOffValue,resValue);//カットオフとレゾナンスを設定
      osc.connect(fil);//オシレータにフィルターを接続
      osc.freq(baseFreq); //oscのハコに周波数を入れる
      noteCount = noteStartValue;
      isPlaying = true;
      osc.start(); //キーが押されたらオシレーターを発音する
      env.play(osc); //発音したオシレータを制御する
    }
  }
}

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;
    }
  }
}

ツマミ操作に必要な追加項目

  • マウス座標を取り扱う変数の宣言

  • パーツの配置

  • ツマミからのデータ取得

  • エンベロープとフィルターへの反映

このようになります。

①宣言の部

let knobX = 200;  //ノブのX位置
let knobY = 50; //ノブのY位置
let knobInterval = 60; //ノブの間隔
let knobSize = 50;//ノブのサイズ
let knobStat = [false, false, false]; //ノブの当たり判定に使う配列
let mousePosY; //マウスのY座標を格納する変数
let angle = [-1.0, -0.8, -0.6]; //ノブの角度に使う配列
let speed = 0; //回転の速さ
let angleMax = 0.8; //ノブの回転上限
let angleMin = -3.95; //ノブの回転下限

const radius = knobSize/2-6; //回転角度を指定する変数

コメントに記載した内容の変数を宣言しています。配列で宣言している部分は左から、カットオフ、レゾナンス、ディケイという順番で使用していきます。

②初期化の部

function setup() {
  createCanvas(500, 400);
  osc = new p5.Oscillator('sawtooth'); // ハコの中にノコギリ波を入れる
  env = new p5.Envelope(); //ここはエンベロープを入れるハコ
  fil = new p5.LowPass(); //ここはローパスフィルターを入れるハコ
}

変更はありません。

③実行の部

function draw() {
  background(250);
  drawHousing();
  drawInterface();
  knobDraw(); //ノブの回転を反映する関数を追加
  
  if (isPlaying == true){
    noteCount--
    if(noteCount < 0){
      noteOff();
    }
  }
}

後述するknobDraw();の呼び出しを追加しました。

④動作の部

ここは色々と変更があります。

drawHousing()に追加する項目

 fill(150, 150, 150);
  for(let i = 0; i < 3; i++){
    
    circle(knobX - 3 + knobInterval * i, knobY + 3 ,knobSize); //影の描画
  }
  stroke(150, 100, 100); 
  strokeWeight(strokeValue); // 枠線の太さ
  
  fill(200, 200, 200); 
  for(let i = 0; i < 3; i++){
    circle(knobX + knobInterval * i, knobY ,knobSize); // ノブのベース
  }
  
  fill(250, 250, 250); //
  for(let i = 0; i < 3; i++){
    circle(knobX + knobInterval * i, knobY, knobSize / 2); // ノブのトップ
  }

ここはノブのパーツのうち、操作しても変更のない「背景」の部分を描画する記述を追加しています。3つのノブに対応する背景が配列とfor文で記述されます。

drawInterface()に追加する項目

for(let i = 0; i < 4; i++){
    if (knobStat == true){ // マウスクリックでオレンジ色でノブトップを塗り替える
      mousePosY = mouseY;
      fill(255, 125, 0);
      circle(knobX, knobY, knobSize / 2);
    }
  }

これはノブがマウスでクリックされた時にオレンジ色に光る記述を追加しています。

knobDraw()関数の追加

function knobDraw() {
  for (let i = 0; i < 3; i++) {
    if (knobStat[i] == true) {
      fill(255, 125, 0);
      mousePosY = mouseY;

      circle(knobX + knobInterval * i, knobY, knobSize / 2);
    }
    const sinValue = sin(angle[i]);
    const cosValue = cos(angle[i]);

    const knobVX = knobX + cosValue * radius + i * knobInterval;
    const knobVY = knobY + sinValue * radius;
    fill(250, 0, 0);
    circle(knobVX, knobVY, 10);
  }
}

これは、マウスがノブをドラッグした時にメモリが回転する処理と、その描画を記述する関数です。

マウス操作を制御する関数の追加

function touchStarted() {
  for (let i = 0; i < 3; i++) {
    if (
      mouseX > knobX + knobInterval * i - knobSize / 2 &&
      mouseX < knobX + knobInterval * i + knobSize / 2 &&
      mouseY > knobY - knobSize / 2 &&
      mouseY < knobY + knobSize / 2
    ) {
      knobStat[i] = true;
      mousePosY = mouseY;
    }
  }
}

function touchMoved() {
  for (let i = 0; i < 3; i++) {
    if (knobStat[i] === true) {
      if (angle[i] <= angleMax && angle[i] >= angleMin) {
        speed = (mousePosY - mouseY) / 30;
        angle[i] += speed;
      }
      if (angle[i] >= angleMax) {
        angle[i] = angleMax;
      }
      if (angle[i] <= angleMin) {
        angle[i] = angleMin;
      }
    }
  }
}

function touchEnded() {
  for (let i = 0; i < 3; i++) {
    knobStat[i] = false;
  }
}

touchStarted(),touchMoved(),touchEnded()の三つの関数を追加します。

ここまでのコードを一旦、確認します

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 env;
let decay = 0.50;

let fil;
let cutOffValue = 1550.8;
let resValue = 21.9;

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;

//////////ノブを取り扱う変数////////////

let knobX = 200;
let knobY = 50;
let knobInterval = 60;
let knobSize = 50;
let knobStat = [false, false, false];//////////ノブの当たり判定に使う配列
let mousePosY;
let angle = [-1.0, -0.8, -0.6];//////////ノブの角度に使う配列
let speed = 0;
let angleMax = 0.8;
let angleMin = -3.95;

const radius = knobSize/2-6;

function setup() {
  createCanvas(500, 400);
  osc = new p5.Oscillator('sawtooth'); // ハコの中にノコギリ波を入れる
  env = new p5.Envelope(); //ここはエンベロープを入れるハコ
  fil = new p5.LowPass(); //ここはローパスフィルターを入れるハコ
}

function draw() {
  background(250);
  drawHousing();
  drawInterface();
  knobDraw(); //ノブの回転を反映する関数を追加
  
  if (isPlaying == true){
    noteCount--
    if(noteCount < 0){
      noteOff();
    }
  }
}

function drawHousing(){
  stroke(150, 100, 100);
  strokeWeight(strokeValue);
  
  ////////ノブの描画////////////////
  fill(150, 150, 150);
  for(let i = 0; i < 3; i++){
    
    circle(knobX - 3 + knobInterval * i, knobY + 3 ,knobSize); //影の描画
  }
  stroke(150, 100, 100); 
  strokeWeight(strokeValue); // 枠線の太さ
  
  fill(200, 200, 200); 
  for(let i = 0; i < 3; i++){
    circle(knobX + knobInterval * i, knobY ,knobSize); // ノブのベース
  }
  
  fill(250, 250, 250); //
  for(let i = 0; i < 3; i++){
    circle(knobX + knobInterval * i, knobY, knobSize / 2); // ノブのトップ
  }

  ////////ここから白鍵////////////////
  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 < 3; i++){
    if (knobStat == true){ // マウスクリックでオレンジ色でノブトップを塗り替える
      mousePosY = mouseY;
      fill(255, 125, 0);
      circle(knobX, knobY, knobSize / 2);
    }
  }
  ////////ノブの描画ここまで////////////////
  
  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){ //キーが押されたら
      env.setADSR(0.01,decay,0.0,0.0); // ADSRの値を入れておく
      fil.set(cutOffValue,resValue);//カットオフとレゾナンスを設定
      osc.connect(fil);//オシレータにフィルターを接続
      osc.freq(baseFreq); //oscのハコに周波数を入れる
      noteCount = noteStartValue;
      isPlaying = true;
      osc.start(); //オシレーターを発音する
      env.play(osc); //発音したオシレータを制御する
    }
  }
  for (let i = 0; i < 6; i++) {
    if (keyStatS[i] == true){ //キーが押されたら
      env.setADSR(0.01,decay,0.0,0.0); // ADSRの値を入れておく
      fil.set(cutOffValue,resValue);//カットオフとレゾナンスを設定
      osc.connect(fil);//オシレータにフィルターを接続
      osc.freq(baseFreq); //oscのハコに周波数を入れる
      noteCount = noteStartValue;
      isPlaying = true;
      osc.start(); //キーが押されたらオシレーターを発音する
      env.play(osc); //発音したオシレータを制御する
    }
  }
}

function noteOff(){
  osc.stop();
}

function knobDraw() {
  for (let i = 0; i < 3; i++) {
    if (knobStat[i] == true) {
      fill(255, 125, 0);
      mousePosY = mouseY;

      circle(knobX + knobInterval * i, knobY, knobSize / 2);
    }
    const sinValue = sin(angle[i]);
    const cosValue = cos(angle[i]);

    const knobVX = knobX + cosValue * radius + i * knobInterval;
    const knobVY = knobY + sinValue * radius;
    fill(250, 0, 0);
    circle(knobVX, knobVY, 10);
  }
}

function touchStarted() {
  for (let i = 0; i < 3; i++) {
    if (
      mouseX > knobX + knobInterval * i - knobSize / 2 &&
      mouseX < knobX + knobInterval * i + knobSize / 2 &&
      mouseY > knobY - knobSize / 2 &&
      mouseY < knobY + knobSize / 2
    ) {
      knobStat[i] = true;
      mousePosY = mouseY;
    }
  }
}

function touchMoved() {
  for (let i = 0; i < 3; i++) {
    if (knobStat[i] === true) {
      if (angle[i] <= angleMax && angle[i] >= angleMin) {
        speed = (mousePosY - mouseY) / 30;
        angle[i] += speed;
      }
      if (angle[i] >= angleMax) {
        angle[i] = angleMax;
      }
      if (angle[i] <= angleMin) {
        angle[i] = angleMin;
      }
    }
  }
}

function touchEnded() {
  for (let i = 0; i < 3; i++) {
    knobStat[i] = false;
  }
}


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;
    }
  }
}

このコードを実行すると、PCのキーボード操作で発音すること。ノブがマウスドラッグで回転するところまで、動作させることができます。
一度確認してみましょう。

ここまで配置したパーツはすべて正常に動作しています。ここから先は、ノブを使ってパラメータが変化するように、記述を加えていきます。

①宣言の部

//let cutOffValue = 1550.8; //第20話で宣言済み
//let resValue = 21.9; //第20話で宣言済み
let envValue = 0.171;

ここはエンベロープのenvValue変数を追加しました。

②初期化の部

function setup() {
  createCanvas(500, 400);
  osc = new p5.Oscillator('sawtooth'); // ハコの中にノコギリ波を入れる
  env = new p5.Envelope(); //ここはエンベロープを入れるハコ
  fil = new p5.LowPass(); //ここはローパスフィルターを入れるハコ
}

変更はありません。

③実行の部

function draw() {
  background(250);
  drawHousing();
  drawInterface();
  knobDraw(); //ノブの回転を反映する関数を追加
  
  if (isPlaying == true){
    noteCount--
    if(noteCount < 0){
      noteOff();
    }
  }
}

変更はありません。

④動作の部

ここに追記があります。順番に説明します。

touchMoved()の書き換え

function touchMoved() {  
 
  for (let i = 0; i < 3; i++) {
    if (knobStat[i] === true) {
      if (angle[i] <= angleMax && angle[i] >= angleMin) {
        speed = (mousePosY - mouseY) / 30;
        angle[i] += speed;
      }
      if (angle[i] >= angleMax) {
        angle[i] = angleMax;
      }
      if (angle[i] <= angleMin) {
        angle[i] = angleMin;
      }
      
//////////ノブの値を発音に反映させるための処理//////////////
      if (i == 0){
        cutOffValue = map(angle[0], angleMin, angleMax,150, 5335);
      }
      if (i == 1){
        resValue = map(angle[1], angleMin, angleMax,0.001, 45);
      }
      if (i == 2){
        envValue = map(angle[2], angleMin, angleMax,0.01, 0.4);
      }
      if (i == 3){
        synVolValue = map(angle[3], angleMin, angleMax,0.0001, 1.0);
      }
    }
  }
//////////ノブの値を発音に反映させるための処理  ここまで//////////////
}

追加されている3個のif文によって、それぞれのノブが回転にしたがって、パラメータを設定します。この反応にとってカットオフ、レゾナンス、ディケイの値を代入しています。この値が、実際に発音される音に反映される様になっています。

三つのノブが取得するパラメータのうち、cutOffValueがどの様に変化するのかを抜粋して説明します。

cutOffValue = map(angle[0], angleMin, angleMax,150, 5335);

マウスによってカットオフがドラッグされた時に上記のような処理が実行されます。これはmap()関数という機能を使っていますので、この引数を読み解くと理解できます。

文章で説明します。
angleMinは150でangleMaxは5335です。
この範囲の中にangle[0]はどの辺にあるのかを測定します。
測定結果であるangle[0]の値を150から5335までの値でcutOffValueに報告しますね。
こんな感じではないかと思います。

このmap()関数によって取得したカットオフ、レゾナンス、ディケイの値を、実際に発音するnoteOn()に渡しています。

noteOn()の内容

function noteOn(){
  for (let i = 0; i < 8; i++) {
    if (keyStat[i] == true){ //キーが押されたら
      env.setADSR(0.01,envValue,0.0,0.0); // ADSRの値を入れておく
      fil.set(cutOffValue,resValue);//カットオフとレゾナンスを設定
      osc.connect(fil);//オシレータにフィルターを接続
      osc.freq(baseFreq); //oscのハコに周波数を入れる
      noteCount = noteStartValue;
      isPlaying = true;
      osc.start(); //オシレーターを発音する
      env.play(osc); //発音したオシレータを制御する
    }
  }
  for (let i = 0; i < 6; i++) {
    if (keyStatS[i] == true){ //キーが押されたら
      env.setADSR(0.01,decay,0.0,0.0); // ADSRの値を入れておく
      fil.set(cutOffValue,resValue);//カットオフとレゾナンスを設定
      osc.connect(fil);//オシレータにフィルターを接続
      osc.freq(baseFreq); //oscのハコに周波数を入れる
      noteCount = noteStartValue;
      isPlaying = true;
      osc.start(); //キーが押されたらオシレーターを発音する
      env.play(osc); //発音したオシレータを制御する
    }
  }
}
env.setADSR(0.01,envValue,0.0,0.0); // ADSRの値を入れておく
fil.set(cutOffValue,resValue);//カットオフとレゾナンスを設定

マウスの動きによって回転したノブから撮ってきた変数をここで実際に反映しています。
envValue,cutOffValue,resValueの三つがそれぞれのパラメータに設定され、最後に発音される仕組みとなっています。

この記事のコード全文

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 env;
let decay = 0.50;

let fil;
let cutOffValue = 1550.8;
let resValue = 21.9;

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;

//////////ノブを取り扱う変数////////////

let knobX = 200;
let knobY = 50;
let knobInterval = 60;
let knobSize = 50;
let knobStat = [false, false, false];//////////ノブの当たり判定に使う配列
let mousePosY;
let angle = [-1.0, -0.8, -0.6];//////////ノブの角度に使う配列
let speed = 0;
let angleMax = 0.8;
let angleMin = -3.95;

const radius = knobSize/2-6;

//////////ノブを取り扱う変数////////////

//let cutOffValue = 1550.8; //第20話で宣言済み
//let resValue = 21.9; //第20話で宣言済み
let envValue = 0.171;

function setup() {
  createCanvas(500, 400);
  osc = new p5.Oscillator('sawtooth'); // ハコの中にノコギリ波を入れる
  env = new p5.Envelope(); //ここはエンベロープを入れるハコ
  fil = new p5.LowPass(); //ここはローパスフィルターを入れるハコ
}

function draw() {
  background(250);
  drawHousing();
  drawInterface();
  knobDraw(); //ノブの回転を反映する関数を追加
  
  if (isPlaying == true){
    noteCount--
    if(noteCount < 0){
      noteOff();
    }
  }
}

function drawHousing(){
  stroke(150, 100, 100);
  strokeWeight(strokeValue);
  
  ////////ノブの描画////////////////
  fill(150, 150, 150);
  for(let i = 0; i < 3; i++){
    
    circle(knobX - 3 + knobInterval * i, knobY + 3 ,knobSize); //影の描画
  }
  stroke(150, 100, 100); 
  strokeWeight(strokeValue); // 枠線の太さ
  
  fill(200, 200, 200); 
  for(let i = 0; i < 3; i++){
    circle(knobX + knobInterval * i, knobY ,knobSize); // ノブのベース
  }
  
  fill(250, 250, 250); //
  for(let i = 0; i < 3; i++){
    circle(knobX + knobInterval * i, knobY, knobSize / 2); // ノブのトップ
  }

  ////////ここから白鍵////////////////
  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 < 3; i++){
    if (knobStat == true){ // マウスクリックでオレンジ色でノブトップを塗り替える
      mousePosY = mouseY;
      fill(255, 125, 0);
      circle(knobX, knobY, knobSize / 2);
    }
  }
  ////////ノブの描画ここまで////////////////
  
  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){ //キーが押されたら
      env.setADSR(0.01,envValue,0.0,0.0); // ADSRの値を入れておく
      fil.set(cutOffValue,resValue);//カットオフとレゾナンスを設定
      osc.connect(fil);//オシレータにフィルターを接続
      osc.freq(baseFreq); //oscのハコに周波数を入れる
      noteCount = noteStartValue;
      isPlaying = true;
      osc.start(); //オシレーターを発音する
      env.play(osc); //発音したオシレータを制御する
    }
  }
  for (let i = 0; i < 6; i++) {
    if (keyStatS[i] == true){ //キーが押されたら
      env.setADSR(0.01,decay,0.0,0.0); // ADSRの値を入れておく
      fil.set(cutOffValue,resValue);//カットオフとレゾナンスを設定
      osc.connect(fil);//オシレータにフィルターを接続
      osc.freq(baseFreq); //oscのハコに周波数を入れる
      noteCount = noteStartValue;
      isPlaying = true;
      osc.start(); //キーが押されたらオシレーターを発音する
      env.play(osc); //発音したオシレータを制御する
    }
  }
}

function noteOff(){
  osc.stop();
}

function knobDraw() {
  for (let i = 0; i < 4; i++) {
    if (knobStat[i] == true) {
      fill(255, 125, 0);
      mousePosY = mouseY;

      circle(knobX + knobInterval * i, knobY, knobSize / 2);
    }
    const sinValue = sin(angle[i]);
    const cosValue = cos(angle[i]);

    const knobVX = knobX + cosValue * radius + i * knobInterval;
    const knobVY = knobY + sinValue * radius;
    fill(250, 0, 0);
    circle(knobVX, knobVY, 10);
  }
}

function touchStarted() {
  for (let i = 0; i < 4; i++) {
    if (
      mouseX > knobX + knobInterval * i - knobSize / 2 &&
      mouseX < knobX + knobInterval * i + knobSize / 2 &&
      mouseY > knobY - knobSize / 2 &&
      mouseY < knobY + knobSize / 2
    ) {
      knobStat[i] = true;
      mousePosY = mouseY;
    }
  }
}

function touchMoved() {  
 
  for (let i = 0; i < 4; i++) {
    if (knobStat[i] === true) {
      if (angle[i] <= angleMax && angle[i] >= angleMin) {
        speed = (mousePosY - mouseY) / 30;
        angle[i] += speed;
      }
      if (angle[i] >= angleMax) {
        angle[i] = angleMax;
      }
      if (angle[i] <= angleMin) {
        angle[i] = angleMin;
      }
      
//////////ノブの値を発音に反映させるための処理//////////////
      if (i == 0){
        cutOffValue = map(angle[0], angleMin, angleMax,150, 5335);
      }
      if (i == 1){
        resValue = map(angle[1], angleMin, angleMax,0.001, 45);
      }
      if (i == 2){
        envValue = map(angle[2], angleMin, angleMax,0.01, 0.4);
      }
    }
  }
  //////////ノブの値を発音に反映させるための処理  ここまで//////////////
}

function touchEnded() {
  for (let i = 0; i < 4; i++) {
    knobStat[i] = false;
  }
}


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;
    }
  }
}

実際にコードをコピペして動かしてみてください!

キーボードによる発音、ノブによる音色の変化が反映されています。
ここまできて、ようやく、このスケッチはシンセサイザーであると胸を張って言えるようになったと思います。

開発としてはここで、大きな節目を迎えることになりました。次なるチャレンジに繋げていこうと思います!

この記事が気に入ったらサポートをしてみませんか?