曲と同期するためのclassを作る

More than 1 year has passed since last update.


はじめに

例えば音楽ゲームのような曲と同期が必要になるものを作るときに、現在の再生位置から計算するものが多いと思いますが、今回は現在の再生位置やBPMから今この曲が、何小節目で何拍目かを取得するclassの作り方を紹介します。


環境

今回使用する環境はC++とSiv3Dというライブラリですが、

アルゴリズム的には、現在の曲の再生位置が何かしらの形で取得できれば、単位をあわせて同じ計算をしてやれば再現できると思います。


ちなみに

今回使用するSiv3Dには最初からs3d::SoundBeatという曲と同期するためのclassが用意されています。

使い方は


Main.cpp

void Main()
{
int bpm = 134; //曲のbpm
int32 offsetSample = 54500; //曲のオフセット(単位サンプリング数)

const SoundBeat beat(offsetSample, bpm);//同期class

const Sound sound(L"Example/風の丘.mp3");//曲

while (System::Update())
{
const auto b = beat(sound,4,4);

b.bar; //現在の小節
b.beat;//現在の拍
b.f; //現在の拍の初めからどんなもんか [0,1)
}
}


といったものですが、気になる点が2点あります。

1. 内部で持っているbpmが整数型

2. 内部で使用されている再生位置を取得する関数が毎フレーム更新されない

精度が大切になる音楽ゲームでは更新は頻繁なほうがよいでしょうし、bpmが整数のみのところが楽曲によっては使えなくなってしまいます。

したがって今回はこれらの点を改善した独自のSoundBeatを作ることとします。


コード

s3d::SoundBeatと全く同じ使い方ができるように作ってみることにしますが、何拍目かまでは計算しなくても使いやすかったりするので、計算量を減らして、何小節目のどんなもんかを取得するだけでも構わないとは思います。


全体像


MySoundBeat.h

namespace Mahou

{
//現在の拍や小節などの情報を持った構造体
struct BeatCount
{
int64 bar; //現在の小節
int32 beat; //現在の小節の何拍目か
double f; //現在の拍のどんなもんか [0,1)
BeatCount(int64 bar,int32 beat, double f) :
bar(bar),
beat(beat),
f(f)
{}
};

//同期class
class SoundBeat
{
private:
double m_bpm;
int64 m_offsetSample;

public:
SoundBeat() :
m_bpm(0),
m_offsetSample(0)
{}
SoundBeat(const int64 offsetSample, const double bpm) :
m_bpm(bpm),
m_offsetSample(offsetSample)
{}

//a/b拍子
BeatCount operator()(const Sound& sound, const int32 a = 4,const int32 b = 4)const;

}


s3d::SoundBeat同様にoperator()で引数にSound型と、その曲の拍子の情報を与えることで何小節目か計算します。


取得関数


はじめに

s3d::SoundBeatの内部で使用されている関数はおそらくSound::streamPosSample()だと思いますが、この関数は現在50Hzでしか更新されないらしく、60FPSの場合6フレームに1回、更新されない時があります。

対して、Sound::samplesPlayed()関数は毎フレーム更新が行われるので、こちらを使用することにします。


コード


MySoundBeat.cpp

namespace Mahou

{
BeatCount SoundBeat::operator()(const Sound& sound, const int32 a,const int32 b)const
{
//1小節のサンプリング数
const int64 samplePerBar = static_cast<int64>(60.0f * 4.0f*a/b / m_bpm * 44100.0f);
//1拍のサンプリング数
const int32 samplePerBeat = static_cast<int32>(samplePerBar / a);
//現在のサンプリング数
const int64 currentSample = sound.samplesPlayed() - m_offsetSample;

//現在が何小節目か
const int64 currentBar = currentSample / samplePerBar;
const int64 restSample = currentSample%samplePerBar; //余ったサンプリング数

//現在が何拍目か
const int32 currentBeat = static_cast<int32>(restSample/ samplePerBeat);
const int32 restBeatSample = static_cast<int32>(restSample%samplePerBeat); //余ったサンプリング数

const double f = (double)restBeatSample/ (samplePerBeat);

return BeatCount{ currentBar,currentBeat,f };
}

}



解説


単位

Sound::samplesPlayed()の扱う単位はサンプリング数です。

サンプリングレートが44100の場合、1秒間44100回サンプリング処理を行うことになります。

わかりやすく言うと

1秒=44100回

2秒=88200回



となるので、すべて秒数*44100で扱うということです。

ここの単位は、開発環境によって、秒だったりミリ秒だったりもすると思うので以降の計算も各自単位を合わせましょう。


1小節、1拍のサンプリング数を求める

//1小節のサンプリング数

const int64 samplePerBar = static_cast<int64>(60.0f * 4.0f*a/b / m_bpm * 44100.0f);

まず4/4拍子(1/1拍子)の曲でbpmが120の時1小節は何秒か考えます。

bpmとはbeat/minutesすなわち1分間にある拍数です。

bpm120ということは1分間に120拍ということなので、4/4拍子の場合

60秒(1分)=30小節(120拍)

から1小節=2秒と求めることができます。

同様にbpmがnで4/4拍子の時は

60秒=n/4小節から

1小節=240/n秒と求めることができますね。

では、a/b拍子の場合どうなるでしょうか?

これは簡単で

さっき得た値にa/bをかけるだけです。

よって

bpmがnでa/b拍子の時、1小節の秒数は

(240/n)*(a/b)となります。

今回はサンプリング数が単位なのでこれに44100をかけたものが1小節のサンプリング数となりますね!

1拍のサンプリング数は

1小節のサンプリング数をaで割れば出るので

//1拍のサンプリング数

const int32 samplePerBeat = static_cast<int32>(samplePerBar / a);

となります。


現在の曲が何小節の何拍目かを求める

まず、実際の曲データの再生位置とオフセットから改めて現在の再生位置を求めます。

これは再生位置からオフセットを引くだけです。

 //現在のサンプリング数

const int64 currentSample = sound.samplesPlayed() - m_offsetSample;

もちろん別の環境の場合はsound.samplesPlayed()の部分を各環境の再生位置を取得する関数に置き換えて、オフセット等の単位を合わせればOKです。

次に、現在何小節目かを求めますが。

さっき1小節がどれだけのサンプリング数かを求めたので、

現在の再生位置/1小節のサンプリング数で求めることができます。

 //現在が何小節目か

const int64 currentBar = currentSample / samplePerBar;

同様に現在が何拍目かを計算します。

現在の再生位置-現在の小節*1小節のサンプリング数

で余りのサンプリング数を求めます。

今回、サンプリング数は整数型なので

現在の再生位置%1小節のサンプリング数

でも同様に求めることができます。

求めた余りのサンプリング数/1拍のサンプリング数

で現在何拍目か求めることができます。

const int64 restSample = currentSample%samplePerBar;    //余ったサンプリング数

//現在が何拍目か
const int32 currentBeat = static_cast<int32>(restSample/ samplePerBeat);

最後に、

さらに余った分を同様に計算し

その余りを1拍のサンプリング数で割った値が

現在の拍からどんなもんかになります。

 const int32 restBeatSample = static_cast<int32>(restSample%samplePerBeat); //余ったサンプリング数

const double f = (double)restBeatSample/ (samplePerBeat);

これで


  • 現在の小節

  • 現在の拍

  • 現在の拍からどんなもんか [0,1)

を求めることができました。

使い方は上で紹介したs3d::SoundBeatと同じです。


まとめ


  • 現在の再生位置とbpm、オフセット、(拍子)から現在の小節や拍を取得することができた!

  • 単位をあわせさえすれば同じアルゴリズムでさまざまな環境で実装可能


おまけ


拍が必要ない場合の例

以下は自分が作成した音楽ゲームColorfulToneで実際に使用したclassです。


SoundBar.h

using BPMType=double;

namespace Mahou
{

struct BarCount
{
int64 bar;

double f;
BarCount(int64 bar, double f) :bar(bar), f(f) {}
};

class SoundBar
{
private:
BPMType m_bpm;
int64 m_offsetSample;

public:
SoundBar() :
m_bpm(0),
m_offsetSample(0)
{}
SoundBar(int64 offsetSample, BPMType bpm) :
m_bpm(bpm),
m_offsetSample(offsetSample)
{}
BarCount operator()(const Sound& sound)const
{
//1小節のサンプル数
const int64 samplePerBar = 60.0f * 4.0f / m_bpm * 44100.0f;
#ifdef VIEWER_MODE
const int64 currentSample = sound.streamPosSample()- m_offsetSample;
#else
const int64 currentSample = sound.samplesPlayed() - m_offsetSample;

#endif

const int64 currentBar = currentSample / samplePerBar;
const int64 restSample = currentSample%samplePerBar;
const double f = (double)restSample / samplePerBar;

return BarCount{ currentBar,f };
}
const int64 getOffset()const { return m_offsetSample; }
const BPMType& getBPM()const { return m_bpm; }
};

}


s3d::Sound::samplesPlayed()はバッファに送信をしたサンプルの数なので再生位置を変更したりした場合は同期が崩れてしまいます。

したがって、自由に再生位置を変更できるビューワーのときのみs3d::Sound::streamPosSample()に切り替える仕組みになっています。