LoginSignup
3
0

Siv3D で Trombone Champ 的なものを作る

Posted at

この記事では、音の高さを変える方法を説明し、Trombone Champ のようなシステムの実装を紹介します。

音の高さとは

音とは空気や水などの振動であり、1次元の波で表されます。
コンピュータ上でも、音声は波として1次元配列で扱われます。

ここで、音の高さとは、その波の周波数の高さです。
周波数の高い音ほど高い音に聞こえ、周波数の低い音ほど低い音に聞こえます。

周波数と音の高さ

音の高さが1オクターブ高くなると、その周波数は2倍になります。
つまり、音の周波数が2倍になると、その音は1オクターブ高くなります。

例えば、ドの音の周波数を2倍にすると、1つ高いドになります。
4倍にすると、2つ高いドに、8倍にすると、3つ高いドになります。
逆に、1/2倍すると、1つ低いドになります。

形式的に表すと、ある音より$n$オクターブ高い音の周波数は、もとの音の周波数の$2^n$倍です。

また、音の高さが1半音高くなると、その周波数は$2^{1/12}$倍になります。
つまり、音の周波数が$2^{1/12}$倍になると、その音は1半音高くなります。
ただし、これは平均律という音律における話です。

平均律は、1オクターブを12等分するもので、1半音は1/12オクターブ高い音になります。
$n$オクターブ高くなると、周波数は$2^n$倍になるから、1半音、すなわち1/12オクターブ高くなると、周波数は$2^{1/12}$倍になるというわけです。

ドの音の周波数を$2^{1/12}$倍すると、ド♯(レ♭)になります。
$2^{2/12}$倍するとレ、$2^{3/12}$倍するとレ♯(ミ♭)、$2^{4/12}$倍するとミになります。
逆に、$2^{-1/12}$倍するとシ、$2^{-2/12}$倍するとシ♭(ラ♯)、$2^{-3/12}$倍するとラになります。

音の高さを変えるには

Siv3D のプログラムで音の高さを変える方法について説明します。

Audio::setSpeed()

音の高さを変えるためには、その周波数を変える必要があります。
しかし、Audio にはその周波数を変更するようなメンバ関数は用意されていません。

そこで、Audio::setSpeed() を使います。
再生速度と周波数は比例するため、再生速度を変化させることで音の高さを変えることができます。

ただし、再生速度を変化させると、もちろん再生時間も変化します。
音を高くするために再生速度を上げると、再生時間は短くなります。

再生速度を上げると周波数が高くなり音が高くなるが、再生時間は短くなる

GlobalAudio::BusSetPitchShiftFilter()

音声の再生時間を変えずに音を高くしたいときは、ピッチシフトが使用できます。

Siv3D では、GlobalAudio::BusSetPitchShiftFilter() を設定することによって、リアルタイムでピッチシフトすることができます。

ただし、音の高さを変化させたときに波形が繋がるとは限らず、ノイズが入ることがあるので、頻繁に音の高さを変化させることはできません。

ピッチシフト

Siv3D で Trombone Champ 的なもの

Audio::setSpeed() を用いて、Trombone Champ 的なものを実装してみました。

実装

マウスを上下に動かすことで音の高さを変えることができ、スペースキーを押すとトロンボーンの音が再生されます。

# include <Siv3D.hpp> // Siv3D v0.6.13

double PosYToPitch(const double y)
{
	return ((0.5 - y / Scene::Height()) * 30);
}

double PitchToPosY(const double pitch)
{
	return ((0.5 - pitch / 30) * Scene::Height());
}

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	const Audio sound{ GMInstrument::Trombone, PianoKey::C4, 15s };

	while (System::Update())
	{
		if (KeySpace.down())
		{
			sound.play();
		}
		else if (KeySpace.up())
		{
			sound.stop();
		}

		const double pitch = Clamp(PosYToPitch(Cursor::PosF().y), -13.0, 13.0);

		const double vibrato = (Periodic::Sine1_1(0.2s) * 0.25);

		sound.setSpeed(Exp2((pitch + vibrato) / 12));

		for (const auto p : { -12, 0, 12 })
		{
			Line{ 0, PitchToPosY(p), Arg::direction(500, 0) }.draw(5, ColorF{ 1, 1.0 }, ColorF{ 1, 0 });
		}

		for (const auto p : { -10, -8, -7, -5, -3, -1, 2, 4, 5, 7, 9, 11 })
		{
			Line{ 0, PitchToPosY(p), Arg::direction(200, 0) }.draw(3, ColorF{ 1, 0.5 }, ColorF{ 1, 0 });
		}

		Line{ 100, 0, 100, Scene::Height() }
			.draw(12, ColorF{ 0 })
			.draw( 8, ColorF{ 1 });

		const Circle circle{ 100, PitchToPosY(pitch), 12 };

		if (sound.isPlaying())
		{
			Line{ 0, circle.y, Arg::direction(400, 0) }.draw(15, ColorF{ 1, 0.7 }, ColorF{ 1, 0 });

			circle.drawShadow(Vec2{ 0, 0 }, 15, 10, ColorF{ 1 });

			const double startAngle = (360_deg * sound.posSec() / sound.lengthSec());

			circle
				.draw(ColorF{ 0 })
				.stretched(-2)
				.drawPie(startAngle, (360_deg - startAngle), Palette::Hotpink);
		}
		else
		{
			circle
				.draw(ColorF{ 0 })
				.stretched(-2)
				.draw(ColorF{ 1 })
				.stretched(-3)
				.draw(Palette::Hotpink);
		}
	}
}

スクリーンショット・動画

スクリーンショット

おわりに

実は、Trombone Champ やったことないです。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0