LPC810メモ:疑似正弦波で音楽を鳴らす(おじいさんの古時計)

前々回http://d.hatena.ne.jp/mikkabo/20160527/1464358308:title=前回]で実現した正弦波による音階の出力を使って、音楽を鳴らしてみる。

短いけど「おじいさんの古時計」を入力してYouTubeにアップしてみた。音だけで絵は動かないのであまり動画の意味ないが、YouTube使ってみたかったので。

前提とする環境はこちら→LPC810メモ 共通の準備
SCTなど基本的なサンプルについてはこちら→LPC810みっかぼ自作サンプル集

音を並べて曲にするには、なんらか譜面をデータ化する必要があって、容量を節約する方法を考えようとそれなりに迷った。最終的に以下のとおりとした。

  • 1コマンド1byte(uint8_t)で表す
  • 上位4bitでコマンドの種類(16種類、音階、休符、オクターブ指定など)を表す。enumで定義。
  • 下位4bitで、1-16のパラメータを表す(1始まりで16段階)。

コマンドとパラメータの表は以下。

コマンド コマンドの意味 パラメータの意味 備考
C 0 音の長さ
Cs 1 ド# 音の長さ
D 2 音の長さ
Ds 3 レ# 音の長さ
E 4 音の長さ
F 5 ファ 音の長さ
Fs 6 ファ# 音の長さ
G 7 音の長さ
Gs 8 ソ# 音の長さ
A 9 音の長さ
As 10 ラ# 音の長さ
B 11 音の長さ
R 12 休符 休符の長さ
T 13 前の音の継続 音の長さ 16を超える長さを実現したい場合に使う
O 14 オクターブ指定 オクターブ 3..6で指定。「O4のド」がピアノ中央のド
V 15 音量指定 音の大きさ 1..10で指定

なお、音の長さは「数字に比例する」長さであり、C(1)が一番短くて、C(16)がその16倍。C(8)が8分音符とか、そういう気のきいた意味ではない。
C(1)が16分音符、C(4)が4分音符、C(16)が全音符、という使い方を想定。

例えば以下のように書く。

// アンパンマン体操の歌いだしの部分
constexpr uint8_t part0[] {
  C(4),C(4),C(2),D(2),E(2),F(2), // ド、ド、ドレミファ
  G(4),F(2),E(2),D(4),R(4), // ソ、ファミレ、(休符)
};

ここで、C,D,Eのような関数はconstexprで定義していて、コンパイル時にuint8_tの定数になる。つまり、1コマンド1byteになる。

利便性のために、多用しそうなパラメータ1,2,4の音符などは、カッコなしで使えるように別途定数を定義しておく。なので、上記の例は以下のようにも書ける。

// アンパンマン体操の歌いだしの部分
constexpr uint8_t part0[] {
  C4,C4,C2,D2,E2,F2, // ド、ド、ドレミファ
  G4,F2,E2,D4,R4, // ソ、ファミレ、(休符)
};

そして、この形式で格納されたuint8_tの配列を解釈して、PWMに渡すdutyを決定するための処理をクラス化(以下サンプルのSctTonePart)。

サンプルコード

SctSoundUtil.h -> 前々回のサンプルから持ってくる。
SctSoundUtil.cpp -> 前々回のサンプルから持ってくる。

SctTonePart.h

#ifndef SCTTONEPART_H_
#define SCTTONEPART_H_

#include "LPC8xx.h"

class SctTonePart {
public:
	void setMml(const uint16_t u, const uint8_t *const *m);
	uint8_t proceed(); // 1/10000秒進めて、その時点でのdutyを返す
	bool eol() const; // 末尾に到達していたらtrue

	// コマンド種類の定義
	enum : uint8_t {
		ComKind_C = 0 << 4, ComKind_Cs = 1 << 4, ComKind_D = 2 << 4,
		ComKind_Ds = 3 << 4, ComKind_E = 4 << 4, ComKind_F = 5 << 4,
		ComKind_Fs = 6 << 4, ComKind_G = 7 << 4, ComKind_Gs = 8 << 4,
		ComKind_A = 9 << 4, ComKind_As = 10 << 4, ComKind_B = 11 << 4,
		ComKind_R = 12 << 4, ComKind_T = 13 << 4,	ComKind_O = 14 << 4,
		ComKind_V = 15 << 4,
	};

	// コマンド種類(上位4bit)とパラメータ(下位4bit)を組み合わせてコマンドを作る
	constexpr static uint8_t MakeCommand(const uint8_t command, const uint8_t arg) {
		return command | (arg - 1);
	}

	// 減衰の状態を取得(後々の都合で、外部から見れるようにしてある)
	uint16_t getDamper() const { return damper; }

private:
	uint16_t unitTime; // パラメータ1あたりの時間
	const uint8_t *const *mmlLines; // 複数行の譜面格納用
	const uint8_t *mml; // 現在の譜面
	int currentMmlLine; // 現在の譜面が何行目か
	int8_t octave; // 現在のオクターブ(3 .. 6)
	int32_t position; // mmlの何文字目を処理中か
	uint8_t volume; // volume(1 .. 10)
	uint8_t readNextTone(); // 次のトークンを読む
	uint16_t toneVerocity; // 現在の音の角速度
	uint32_t remainLength; // 現在の音の残り長さ
	uint16_t angle; // 現在の角度
	uint16_t damper; // 減衰の状態、0..31
	uint16_t damperCounter; // 減衰用のカウンタ
};

constexpr uint8_t C(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_C, arg); }
constexpr uint8_t Cs(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_Cs, arg); }
constexpr uint8_t D(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_D, arg); }
constexpr uint8_t Ds(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_Ds, arg); }
constexpr uint8_t E(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_E, arg); }
constexpr uint8_t F(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_F, arg); }
constexpr uint8_t Fs(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_Fs, arg); }
constexpr uint8_t G(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_G, arg); }
constexpr uint8_t Gs(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_Gs, arg); }
constexpr uint8_t A(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_A, arg); }
constexpr uint8_t As(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_As, arg); }
constexpr uint8_t B(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_B, arg); }
constexpr uint8_t R(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_R, arg); }
constexpr uint8_t T(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_T, arg); }
constexpr uint8_t O(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_O, arg); }
constexpr uint8_t V(const uint8_t arg) { return SctTonePart::MakeCommand(SctTonePart::ComKind_V, arg); }
constexpr uint8_t EOD {0xff};

constexpr uint8_t C1 {C(1)}; constexpr uint8_t Cs1 {Cs(1)}; constexpr uint8_t D1 {D(1)};
constexpr uint8_t Ds1 {Ds(1)}; constexpr uint8_t E1 {E(1)}; constexpr uint8_t F1 {F(1)};
constexpr uint8_t Fs1 {Fs(1)}; constexpr uint8_t G1 {G(1)}; constexpr uint8_t Gs1 {Gs(1)};
constexpr uint8_t A1 {A(1)}; constexpr uint8_t As1 {As(1)}; constexpr uint8_t B1 {B(1)};
constexpr uint8_t R1 {R(1)}; constexpr uint8_t T1 {T(1)};

constexpr uint8_t C2 {C(2)}; constexpr uint8_t Cs2 {Cs(2)}; constexpr uint8_t D2 {D(2)};
constexpr uint8_t Ds2 {Ds(2)}; constexpr uint8_t E2 {E(2)}; constexpr uint8_t F2 {F(2)};
constexpr uint8_t Fs2 {Fs(2)}; constexpr uint8_t G2 {G(2)}; constexpr uint8_t Gs2 {Gs(2)};
constexpr uint8_t A2 {A(2)}; constexpr uint8_t As2 {As(2)}; constexpr uint8_t B2 {B(2)};
constexpr uint8_t R2 {R(2)}; constexpr uint8_t T2 {T(2)};

constexpr uint8_t C4 {C(4)}; constexpr uint8_t Cs4 {Cs(4)}; constexpr uint8_t D4 {D(4)};
constexpr uint8_t Ds4 {Ds(4)}; constexpr uint8_t E4 {E(4)}; constexpr uint8_t F4 {F(4)};
constexpr uint8_t Fs4 {Fs(4)}; constexpr uint8_t G4 {G(4)}; constexpr uint8_t Gs4 {Gs(4)};
constexpr uint8_t A4 {A(4)}; constexpr uint8_t As4 {As(4)}; constexpr uint8_t B4 {B(4)};
constexpr uint8_t R4 {R(4)}; constexpr uint8_t T4 {T(4)};

constexpr uint8_t O3 {O(3)}; constexpr uint8_t O4 {O(4)};
constexpr uint8_t O5 {O(5)}; constexpr uint8_t O6 {O(6)};

#endif /* SCTTONEPART_H_ */

SctTonePart.cpp

#include "SctTonePart.h"
#include "SctSoundUtil.h"

// 1/10000秒進めて、その時点でのduty(0 - 40)を返す
uint8_t SctTonePart::proceed() {
	if(eol()) { return 0; }

	// 音の長さが0になったら次を読む
	if(remainLength == 0) {
		uint8_t kind {readNextTone()};
		if(kind != ComKind_T) {
			// 音の継続でない限り、減衰の状態をリセット
			damper = 0; damperCounter = 0;
		}
	}

	// 角度を1/10000秒分進めつつ、音の残り時間を減らす
	angle += toneVerocity; remainLength--;

	// 1定時間毎に音量を落とし、減衰する音にする
	damperCounter++;
	if(damper < 31 && damperCounter == (64 + damper * 32)) {
		damper++;
		damperCounter = 0;
	}

	return 20 + SctSoundUtil::sin(angle) * (32 - damper) * volume / 320;
}


void SctTonePart::setMml(const uint16_t u, const uint8_t *const *m) {
	mmlLines = m; mml = mmlLines[currentMmlLine = 0];
	unitTime = u; octave = 4; angle = 0; position = 0; volume = 10;
}


uint8_t SctTonePart::readNextTone() {
	uint8_t arg {1};
	uint8_t kind {0};
	while(mml != nullptr) {
		const uint8_t command { mml[position++] };
		kind = command & 0b11110000;
		arg = (command & 0b00001111) + 1;
		if (command == EOD) {
			mml = mmlLines[++currentMmlLine];
			position = 0;
		} else if(kind == ComKind_O) {
			if(arg >= 3 && arg <= 6) { octave = arg; }
		} else if(kind == ComKind_V) {
			if(arg >= 1 && arg <= 10) { volume = arg; }
		} else if(kind == ComKind_T) {
			break;
		} else if(kind == ComKind_R) {
			angle = 0;
			toneVerocity = 0;
			break;
		} else {
			// 音階
			toneVerocity = SctSoundUtil::toneAngularVelocity((kind >> 4) + (octave - 3) * 12);
			break;
		}
	}
	remainLength = arg * unitTime;
	return kind;
}

bool SctTonePart::eol() const { return mml == nullptr; }

sample.cpp

#include "LPC8xx.h"
#include "SctSoundUtil.h"
#include "SctTonePart.h"

constexpr uint8_t part0_0[] { // 主旋律
	O4,C4, F4,E2,F2,G4,F2,G2, A2,A2,As2,A2,D4,G2,G2, F4,F2,F2,E4,D2,E2, F(12),C2,C2,
    F4,E2,F2,G4,F2,G2, A2,A2,As2,A2,F4,G2,G2, F4,F2,F2,E4,D2,E2, F(12),F2,A2,
    O5,C4,O4,A2,G2,F4,E2,F2, G2,F2,E2,D2,C4,F2,A2, O5,C4,O4,A2,G2,F4,E2,F2, G(14),C2,
    O4,F2,F(6),G(8), A2,A2,As2,A2,D4,G2,G2, F(8),E(8), F(12),R4,
	EOD };

constexpr uint8_t part0_1[] { // 主旋律とハモるパート
	O3,A4,
	O4,C4,O3,As2,O4,C2,E4,D2,E2, F2,F2,G2,F2,D4,E2,E2, C4,C2,C2,C4,O3,As2,O4,C2, C(12),O3,A2,A2,
	O4,C4,O3,As2,O4,C2,E4,D2,E2, F2,F2,G2,F2,D4,E2,E2, C4,C2,C2,C4,O3,As2,O4,C2, C(12),C2,F2,
	O4,A4,F2,E2,C4,C2,C2, D2,D2,O3,As2,As2,A4,O4,C2,F2, A4,F2,E2,C4,C2,C2, E(14),R2,
	O4,C2,C(6),E(8), F2,F2,F2,F2,O3,As4,O4,D2,D2, C(8),C(8), C(12),R4,
	EOD };

constexpr uint8_t part0_2[] { // ベース
	R4,
	O3,A(8),O4,C(8), C(8),O3,As(8), A(8),G(8), A(12),R4,
	O3,A(8),O4,C(8), C(8),O3,As(8), A(8),G(8), A(12),R4,
	O4,C(8),O3,A(8), As4,F4,F(8), O4,C(8),O3,A(8), O4,C(14),R2,
	O3,A2,A(6),O4,C(8), C(8),O3,F(8), A(8),As(8),A(12),R4,
	EOD };

const uint8_t *part0[] { part0_0, nullptr };
const uint8_t *part1[] { part0_1, nullptr };
const uint8_t *part2[] { part0_2, nullptr };

static SctTonePart sctTonePart[3];
static void handler() {
	uint8_t duty{0};
	bool endFlag{true};
	for(auto &t : sctTonePart) {
		duty += t.proceed(); // 単位時間進めて、その時点でのdutyを取得
		if(!t.eol()) { endFlag = false; }
	}
	if(endFlag) {
		SctSoundUtil::stop();
	} else {
		SctSoundUtil::setDuty(duty); // dutyの合計値を設定
	}
}

int main(void) {
 	sctTonePart[0].setMml(1536, part0); // 1536は速度(大きいほど遅い)
	sctTonePart[1].setMml(1536, part1);
	sctTonePart[2].setMml(1536, part2);
	SctSoundUtil::setCallback(handler);
	SctSoundUtil::init();
	SctSoundUtil::start();
	return 0;
}

接続

前回と同じのため省略。
ただし、電池がCR2032だとどうも安定しないので、単3の充電池3本に変更。

補足/雑感

  • 実は最初は譜面を文字列で(よくあるMMLみたいので)表していたのだが、そうすると1音あたり2,3byteくらいになってしまい、ピアノの譜面みながら長めの曲を打ち込んでいたらあっという間に4Kbオーバー。紆余曲折の末に、文字列より入力面倒だけどこの方法となった。
  • この「おじいさんの古時計」の例でバイナリが1928byteなので、残り約2Kb、すなわち2000個程度のコマンドを追加可能。
  • 合計3Kbくらいになる大作も作ってるから、早めにアップして区切りつけたい。最近これに時間をかけすぎた。

みっかぼの無料Androidアプリはこちら。