見出し画像

p5.jsでシンセサイザーを作る 第19話 エンベロープ

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

音の余韻です

前回、キーボードによる演奏がある程度できる様になりましたので、ここからは楽器としての完成度を上げるフェーズになります。
エンベロープを実装していくことにします。これは音の余韻を設定するための機能で、シンセサイザーには欠かせない要素になります。

今回使うコード

開発を進めるにあたってnoteに書き込むためのソースコードが長くなってきたので、今回使うコードは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];

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

実行結果

正しく動作しています。このコードでは、最後の発音から30フレーム後にnoteOff()が呼び出される様になっています。ここからは、エンベロープによる音の減衰によって発音が終了するように実装していきます。

p5.soundライブラリのsetADSR()という機能を使って実装していきます。p5.jsのリファレンスには詳しい作例があるため、これを応用することができます。
このリファレンスによるとsetADSR()の関数ではそれぞれ
attack,decay,sustain,releaseというシンセサイザーにおける一般的なADSRの値を指定できる様です。

2024-07-02追記
p5.jsのリファレンスが大幅にリニューアルされたようです。リファレンスのリンクはアーカイブに移行されたドキュメントを掲載しています。

今回はdecayの値に論点を絞って余韻を表現することにします。

①宣言の部

変数の宣言には、エンベロープの機能を格納するハコとしてenvという変数を宣言しました。
ADSRのうちdecayにかかる値は0.50で仮置きしておきます。

let env;
let decay = 0.50;

②初期化の部

ここではオシレーターの後にエベロープのパーツを宣言しています。

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

envにはエンベロープの部品を入れ込みます。

③実行の部

function draw() {
  background(250); //背景の色
  drawHousing();
  drawInterface();
  
  if (isPlaying == true){
    noteCount--
    if(noteCount < 0){
      noteOff();
    }
  }
}

特に変更はありません。

④動作の部

noteOn()がとても大切です。
キーが押された時の動作は以下のように順番に実行していきます。

  1. エンベロープにADSRの値を入れておく

  2. オシレータに音階となる周波数を入れておく

  3. 発音を開始して、発音停止のカウントダウンをリセットする

  4. 発音が実行されているフラグを立てる

  5. オシレータの発音を開始する

  6. 発音された音源をエンベロープで制御する

ここでいいことに気がつきました。減算アナログシンセサイザーというのは、とりえずオシレータによってフルパワーで発音して置いてフィルターとかエンベロープによって余計な音を間引いて行くことで、音色を作るのですね!!
理屈では知っていましたが、すごく実感できます。

コードはこうなります。

function noteOn(){
  for (let i = 0; i < 8; i++) {
    if (keyStat[i] == true){ //キーが押されたら
      env.setADSR(0.01,decay,0.0,0.0); // ADSRの値を入れておく
      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の値を入れておく
      osc.freq(baseFreq); //oscのハコに周波数を入れる
      noteCount = noteStartValue;
      isPlaying = true;
      osc.start(); //キーが押されたらオシレーターを発音する
      env.play(osc); //発音したオシレータを制御する
    }
  }
}

実行結果

うまく動いています。エンベロープの制御によって発音から消音までの余韻が生まれています。

env.setADSR(0.01,decay,0.0,0.0);

ここで指定するdecayの値を変更することによって、発音時間を色々と試すことができます。
小刻みに音を切ったり、長く伸ばしたりするような演奏は、このsetADSR()によって幅広く応用できそうです。

この記事のコード全文

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 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(); //ここはエンベロープを入れるハコ
}

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の値を入れておく
      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の値を入れておく
      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;
    }
  }
}

実際にコピペして試してみてください!
decayの変数をいじってみて、変化を見てみてください!


この記事が参加している募集

#つくってみた

19,676件

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