リムナンテスは愉快な気分

徒然なるままに、言語、数学、音楽、プログラミング、時々人生についての記事を書きます

【p5.js】絶対和音感アプリで和音を可視化する

f:id:frecafloros:20210613231655p:plain

こういうのを作ってます。一応今回で完結。


前回記事を見る場合はこちらから:
limnanthaceae.hatenablog.com



シリーズの最初から見る場合はこちらから:
limnanthaceae.hatenablog.com


せっかくp5.jsを使っているので、もう少し動的なあれこれを実装します。

その前にレイアウト変更

スマホで閲覧しても見やすいような配置に変更します。
constellation→chord→config→buttonの順番でしたが、configを前に持ってきてconfig→constellation→chord→buttonの順に。各要素が縦に並ぶようにして、constellation→chord→buttonが一画面に収まるようにします(configからスクロールしないと操作できないというのは些か面倒ではあるので折りたたみ式とかにするかも)。PCではconfigが左半分、それ以外が右半分という表示になります。

wrapを使うと、画面の横幅が小さくなった時に自動的に縦配列になります。

.container{
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

ついでなので、「全選択」とblack adder chord(イキスギコード)*1も追加。


See the Pen
chordtrainer-10
by frecafloros (@frecafloros)
on CodePen.

ボタン改良

「解答」ボタンと「次の問題」ボタンを統合します。こいつらは元々交互に押されるものであって2つもいらないので。

本当はjQueryを使うのが簡単に実装できる方法だとは思いますが、今回はcssjavascriptで制御します。

css側からテキストを制御するために、:before:after*2といった「擬似要素」と呼ばれるものを活用します。普通にcsscontentで指定するだけでは文字列の書き換えをすることが(多分)できないのですが、擬似要素で要素もどきを作ることで文字列の書き換えができるようになります。

:before:afterは要素の前後に内容を追加することができるもので、class名:beforeで要素の直前、class名:afterで要素の直後に内容を追加します。

「次の問題」のclassをqa-next、「解答」のclassをqa-answerとして、次のように指定します。

<div class="obj-qa">
	<button class="qa-next" id="btn-qa"> </button>
</div>
#btn-qa{
	width: 80px;
}

/* 次の問題ボタン */
.qa-next{
	font-size: 0px;
}

.qa-next:after{
	font-size: 14px;
	content: '次の問題';
}

/* 解答ボタン */
.qa-answer{
	font-size: 0px;
}

.qa-answer:after{
	font-size: 14px;
	content: '解答';
}

続いて、「次の問題」→「解答」→「次の問題」→…の(テキストと押したときの効果の)切り替えは要素のclassをjsで書き換えることにより実装します。ボタンを押すたびに関数の実行、qa-nextclassとqa-answerclassの入れ替えを実施します。

// qaボタン制御
let buttonQA = ()=>{
	const buttonQA = document.getElementById("btn-qa");
	let buttonQAClass = document.getElementsByClassName("qa-next");
	if(buttonQAClass.length == 0){
		// 現在のclassがqa-answer
		buttonAnswer(); // 解答の表示
		buttonQA.classList.remove("qa-answer");
		buttonQA.classList.add("qa-next");
	}else{
		// 現在のclassがqa-next
		buttonNext(); // 出題
		buttonQA.classList.remove("qa-next");
		buttonQA.classList.add("qa-answer");
	}
}

getElementsByClassNameは指定したclassの要素を抽出してくる関数で、要素をリスト形式で管理します。今回はフラグの代わりとして使っていて、リストの長さが0(=qa-nextのクラスとなっている要素が無い=今のクラスがqa-answer)かそうでないか(=今のクラスがqa-next)で処理を分岐させています。


ボタンのデザインも少し変更したものがこちら。


See the Pen
chordtrainer-11
by frecafloros (@frecafloros)
on CodePen.

サウンドビジュアライズ

本題。解答表示後、和音の可視化をします。

音の振幅値をとるためにp5.soundを導入します。これはp5.jsの拡張ライブラリです。(本当はTone.jsでやりたかったけど振幅値をリアルタイムで取れなかったのでp5側でやります。)
振幅値とるの無理だったので決めうちで実装。後述。

解答表示する前に可視化されると盛大なネタバレになってしまうので、表示したい時に表示できるようにしたい。

loop()noLoop()を使います。

まずsetup()の時点ではリアルタイムで描画しないのでnoLoop()で止めておきます。

draw()が実質メインループになりますので、ここで描画処理させます。今回はメインループの外に関数drawConstellation();soundVisualizer()を作りました。一定時間が経過するとnoLoop()を実行してループが停止します。あんまりメインループにループ停止書かないほうがいい気はする。

// chord constellationの描画
function setup(){
	createCanvas(canvasWidth, canvasHeight).parent('chord-constellation');

	textAlign(CENTER,CENTER);
	textSize(16);
	textFont('Optima');

	translate(canvasWidth/2, canvasHeight/2);
	const r = 80;
	for(let i=0;i<12;i++){
		text(pitch2keyname[i], r*sin(i*TAU/12), -1*r*cos(i*TAU/12));
	}

	// 描画停止
	noLoop();
}

// サウンドビジュアル描画(メインループ?)
function draw(){
	// user gesture
	onclickSound.onclick = buttonSound;
	onclickQA.onclick = buttonQA;

	if(t<60){
		// 描画
		drawConstellation();
		soundVisualizer();

		// buttonQAを非活性化
		buttonQAId.setAttribute("disabled", true);
	}else{
		// 描画停止
		noLoop();
		
		// buttonQAを活性化
		buttonQAId.removeAttribute("disabled");
	}
}


constellation の描画中に次の問題に行かれると次の問題の constellation を描画されてしまうので、constellation 描画中は「次の問題」ボタンを非活性にしています。非活性化はsetAttribute("disabled", true)、活性化はremoveAttribute("disabled")。"disabled"という属性をつけたり剥いだりして実装しています。


soundVisualizer()では、和音構成音に対応した円弧を描画します。

function soundVisualizer(){
	t = t + 1;

	// 円弧の size 設定
	if(t<4){
		// Attack
		size = rVisual/4*t;
	}else if(t<10){
		// Decay
		size = rVisual - rVisual/4*(t-4)/6;
	}else{
		// Release
		size = rVisual*3/4 - rVisual*3/4*(t-10)/50;
	}

	//円弧を描く
	colorMode(HSB);
	for(let i=0;i<chord.length;i++){
		tmpTone = (rootPc + chordTypes[chordNum]['chordKeys'][i])%12;
		fill(30*tmpTone,60,85,0.4);
		stroke(30*tmpTone,60,85);
		tmpRad = tmpTone*TAU/12 - HALF_PI;
		arc(canvasWidth/2,canvasHeight/2,size,size,tmpRad-0.2,tmpRad+0.2,PIE);
	}
}


上半分で、円弧のサイズを数式的に決め打ちしてます。最初に勢いよく大きくなって、少ししぼんだらゆっくり消えゆく感じ。

下半分で実際に円弧を描画しています。
音に沿って色相を動かしたかったので、HSBで色を指定しています。colorMode(HSB)をすると、fillstrokeの色指定がHSBになります。


下はデモです。



See the Pen
chordtrainer-12
by frecafloros (@frecafloros)
on CodePen.


一応やりたいことはこれでだいたい済んだので、あとはアプリで和音感を鍛える修行します。

おわり。

*1:解釈の仕方は色々ありますが、本アプリでの扱いは #IVaug/I [I blk]。

*2:css3ではコロン2つの::before、::after表記。特にcss3である必要がないと思うのでコロン1つでよいかと。