この記事では、音の高さを変える方法を説明し、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 やったことないです。