37. オーディオ再生¶
効果音や音楽の再生を制御する方法を学びます。
再生する音声データはオーディオクラス Audio
で管理します。オーディオはいくつかの方法で作成できます。オーディオの作成にはコストがかかるため、通常はメインループの前で行います。メインループ内で作成する必要がある場合には、毎フレーム作成されないような制御が必要です。
37.1 音声ファイルの読み込みと再生¶
Audio 変数名{ U"ファイルパス" };
で、音声ファイルから Audio
を作成できます。ファイルパスは、実行ファイルがあるフォルダ(開発中は App
フォルダ)を基準とする相対パスか、絶対パスを使用します。
Siv3D では、次のような音声フォーマットの読み込みがサポートされています。一部の音声フォーマットは OS によって対応状況が異なります。
フォーマット | 拡張子 | Windows | macOS | Linux | Web |
---|---|---|---|---|---|
WAVE | .wav | ✔ | ✔ | ✔ | ✔ |
MP3 | .mp3 | ✔ | ✔ | ✔ | ✔* |
AAC | .m4a | ✔ | ✔ | ✔ | ✔* |
OggVorbis | .ogg | ✔ | ✔ | ✔ | ✔ |
Opus | .opus | ✔ | ✔ | ✔ | ✔ |
MIDI | .mid | ✔ | ✔ | ✔ | ✔ |
WMA | .wma | ✔ | |||
FLAC | .flac | ✔ | ✔ | ||
AIFF | .aif, .aiff, .aifc | ✔ |
(*) ビルドオプションの設定と、特別な関数の使用が必要です。
オーディオを再生するには Audio
の .play()
を呼びます。すでに再生中のオーディオに対して .play()
をしても何も起こりません。同じオーディオを重ねて再生したい場合は、37.5 の .playOneShot()
を使います。
# include <Siv3D.hpp>
void Main()
{
// 音声ファイルを読み込んで Audio を作成する
const Audio audio{ U"example/test.mp3" };
// オーディオを再生する
audio.play();
while (System::Update())
{
}
}
オーディオの再生中はオーディオオブジェクトが存在していないといけないため、Audio{ U"example/test.mp3" }.play();
のようなコードでは音声は再生できません。
37.2 ストリーミング再生用のオーディオを作成する¶
WAVE, MP3, OggVorbis, FLAC 形式の音声ファイルは、ストリーミング再生に対応しています。ストリーミング再生とは、最初にファイル内容の全部を読み込むのではなく、一部だけを読み込んでオーディオを再生しながら、続く部分を逐次読み込む方式のことです。ストリーミング再生を使うと、メモリの使用量やファイルのロード時間が大幅に削減されます。
Audio
でストリーミング再生を有効にするには、Audio
のコンストラクタに Audio::Stream
を渡してストリーミング再生をリクエストします。もし Audio::Stream
を指定したファイルがストリーミング再生をサポートしていなかった場合は通常の読み込みが行われます。Audio
でストリーミング再生が有効になっているかは、.isStreaming()
で調べられます。
# include <Siv3D.hpp>
void Main()
{
// 音声ファイルを読み込んで Audio を作成(ストリーミング再生をリクエスト)
const Audio audio{ Audio::Stream, U"example/test.mp3" };
// ストリーミング再生になるかを取得
Print << audio.isStreaming();
// オーディオを再生
audio.play();
while (System::Update())
{
}
}
ストリーミング再生を有効にした Audio
については、次のような制約がありますが、通常のオーディオ再生用途では問題になりません。
- 区間ループ再生において、ループ末尾位置をオーディオ終端以外に設定できない
.getSamples()
でオーディオの音声波形データにアクセスできない- FFT を行うときのサンプル数が少なくなる
37.3 楽器の音のオーディオを作成する¶
楽器の種類と音の高さ、長さなどをもとにプログラムで楽器の音を生成して Audio
を作成できます。Audio
のコンストラクタに GMInstrument
で列挙される楽器名、PianoKey
で列挙される音の高さ、音の長さを渡します。
音の長さは
- ノート・オン(鳴らす)時間
- ノート・オン時間とノート・オフ(鳴らし終える)時間
の 2 通りの指定方法があります。前者では 1.0 秒のノート・オフ時間が追加で設定されます。
楽器の一覧
定数 | 楽器名 |
---|---|
GMInstrument::Piano1 |
アコースティックピアノ |
GMInstrument::Piano2 |
ブライトピアノ |
GMInstrument::Piano3 |
エレクトリック・グランドピアノ |
GMInstrument::Piano4 |
ホンキートンクピアノ |
GMInstrument::ElectricPiano1 |
エレクトリック・グランド・ピアノ |
GMInstrument::ElectricPiano2 |
FM エレクトリックピアノ |
GMInstrument::Harpsichord |
ハープシコード |
GMInstrument::Clavinet |
クラビネット |
GMInstrument::Celesta |
チェレスタ |
GMInstrument::Glockenspiel |
グロッケンシュピール |
GMInstrument::MusicBox |
オルゴール |
GMInstrument::Vibraphone |
ヴィブラフォン |
GMInstrument::Marimba |
マリンバ |
GMInstrument::Xylophone |
シロフォン |
GMInstrument::TubularBells |
チューブラーベル |
GMInstrument::Dulcimer |
ダルシマー |
GMInstrument::DrawbarOrgan |
ドローバー・オルガン |
GMInstrument::PercussiveOrgan |
パーカッシブ・オルガン |
GMInstrument::RockOrgan |
ロック・オルガン |
GMInstrument::ChurchOrgan |
チャーチ・オルガン |
GMInstrument::ReedOrgan |
リード・オルガン |
GMInstrument::Accordion |
アコーディオン |
GMInstrument::Harmonica |
ハーモニカ |
GMInstrument::TangoAccordion |
タンゴ・アコーディオン |
GMInstrument::NylonGuitar |
ナイロン・ギター |
GMInstrument::SteelGuitar |
スティール・ギター |
GMInstrument::JazzGuitar |
ジャズ・ギター |
GMInstrument::CleanGuitar |
クリーン・ギター |
GMInstrument::MutedGuitar |
ミュート・ギター |
GMInstrument::OverdrivenGuitar |
オーバードライブ・ギター |
GMInstrument::DistortionGuitar |
ディストーション・ギター |
GMInstrument::GuitarHarmonics |
ギター・ハーモニクス |
GMInstrument::AcousticBass |
アコースティック・ベース |
GMInstrument::FingeredBass |
フィンガー・ベース |
GMInstrument::PickedBass |
ピック・ベース |
GMInstrument::FretlessBass |
フレットレス・ベース |
GMInstrument::SlapBass1 |
スラップ・ベース 1 |
GMInstrument::SlapBass2 |
スラップ・ベース 2 |
GMInstrument::SynthBass1 |
シンセ・ベース 1 |
GMInstrument::SynthBass2 |
シンセ・ベース 2 |
GMInstrument::Violin |
ヴァイオリン |
GMInstrument::Viola |
ヴィオラ |
GMInstrument::Cello |
チェロ |
GMInstrument::Contrabass |
コントラバス |
GMInstrument::TremoloStrings |
トレモロ・ストリングス |
GMInstrument::PizzicatoStrings |
ピッチカート・ストリングス |
GMInstrument::OrchestralHarp |
オーケストラ・ハープ |
GMInstrument::Timpani |
ティンパニ |
GMInstrument::StringEnsemble1 |
ストリング・アンサンブル 1 |
GMInstrument::StringEnsemble2 |
ストリング・アンサンブル 2 |
GMInstrument::SynthStrings1 |
シンセ・ストリングス 1 |
GMInstrument::SynthStrings2 |
シンセ・ストリングス 2 |
GMInstrument::ChoirAahs |
声「アー」 |
GMInstrument::VoiceOohs |
声「オー」 |
GMInstrument::SynthChoir |
シンセヴォイス |
GMInstrument::OrchestraHit |
オーケストラ・ヒット |
GMInstrument::Trumpet |
トランペット |
GMInstrument::Trombone |
トロンボーン |
GMInstrument::Tuba |
チューバ |
GMInstrument::MutedTrumpet |
ミュート・トランペット |
GMInstrument::FrenchHorn |
フレンチ・ホルン |
GMInstrument::BrassSection |
ブラス・セクション |
GMInstrument::SynthBrass1 |
シンセ・ブラス 1 |
GMInstrument::SynthBrass2 |
シンセ・ブラス 2 |
GMInstrument::SopranoSax |
ソプラノ・サックス |
GMInstrument::AltoSax |
アルト・サックス |
GMInstrument::TenorSax |
テナー・サックス |
GMInstrument::BaritoneSax |
バリトン・サックス |
GMInstrument::Oboe |
オーボエ |
GMInstrument::EnglishHorn |
イングリッシュ・ホルン |
GMInstrument::Bassoon |
ファゴット |
GMInstrument::Clarinet |
クラリネット |
GMInstrument::Piccolo |
ピッコロ |
GMInstrument::Flute |
フルート |
GMInstrument::Recorder |
リコーダー |
GMInstrument::PanFlute |
パン・フルート |
GMInstrument::BlownBottle |
ブロウン・ボトル |
GMInstrument::Shakuhachi |
尺八 |
GMInstrument::Whistle |
口笛 |
GMInstrument::Ocarina |
オカリナ |
GMInstrument::SquareWave |
矩形波 |
GMInstrument::SawWave |
ノコギリ波 |
GMInstrument::SynCalliope |
カリオペリード |
GMInstrument::ChifferLead |
チフリード |
GMInstrument::Charang |
チャランゴリード |
GMInstrument::SoloVox |
声リード |
GMInstrument::FifthSawWave |
フィフスズリード |
GMInstrument::BassAndLead |
ベース + リード |
GMInstrument::Fantasia |
ファンタジア |
GMInstrument::WarmPad |
ウォーム・パッド |
GMInstrument::Polysynth |
ポリシンセ |
GMInstrument::SpaceVoice |
スペース・ヴォイス |
GMInstrument::BowedGlass |
ボウド・グラス |
GMInstrument::MetalPad |
メタル・パッド |
GMInstrument::HaloPad |
ハロー・パッド |
GMInstrument::SweepPad |
スイープ・パッド |
GMInstrument::IceRain |
雨 |
GMInstrument::Soundtrack |
サウンドトラック |
GMInstrument::Crystal |
クリスタル |
GMInstrument::Atmosphere |
アトモスフィア |
GMInstrument::Brightness |
ブライトネス |
GMInstrument::Goblin |
ゴブリン |
GMInstrument::EchoDrops |
エコー・ドロップス |
GMInstrument::StarTheme |
スター・テーマ |
GMInstrument::Sitar |
シタール |
GMInstrument::Banjo |
バンジョー |
GMInstrument::Shamisen |
三味線 |
GMInstrument::Koto |
琴 |
GMInstrument::Kalimba |
カリンバ |
GMInstrument::Bagpipe |
バグパイプ |
GMInstrument::Fiddle |
フィドル |
GMInstrument::Shanai |
シャハナイ |
GMInstrument::TinkleBell |
ティンクル・ベル |
GMInstrument::Agogo |
アゴゴ |
GMInstrument::SteelDrums |
スチール・ドラム |
GMInstrument::Woodblock |
ウッドブロック |
GMInstrument::TaikoDrum |
太鼓 |
GMInstrument::MelodicTom |
メロディック・タム |
GMInstrument::SynthDrum |
シンセ・ドラム |
GMInstrument::ReverseCymbal |
リバース・シンバル |
GMInstrument::GuitarFretNoise |
ギター・フレット・ノイズ |
GMInstrument::BreathNoise |
ブレス・ノイズ |
GMInstrument::Seashore |
海岸 |
GMInstrument::BirdTweet |
鳥のさえずり |
GMInstrument::TelephoneRing |
電話のベル |
GMInstrument::Helicopter |
ヘリコプター |
GMInstrument::Applause |
拍手 |
GMInstrument::Gunshot |
銃声 |
# include <Siv3D.hpp>
void Main()
{
// ピアノの C4 (ド) の音
const Audio piano{ GMInstrument::Piano1, PianoKey::C4, 0.5s };
// クラリネットの D4 (レ) の音
const Audio clarinet{ GMInstrument::Clarinet, PianoKey::D4, 0.5s };
// トランペットの E4 (ミ) の音。ノート・オン 2.0 秒、ノート・オフ 0.5 秒
const Audio trumpet{ GMInstrument::Trumpet, PianoKey::E4, 2.0s, 0.5s };
while (System::Update())
{
if (SimpleGUI::Button(U"Piano", Vec2{ 20, 20 }))
{
piano.play();
}
if (SimpleGUI::Button(U"Clarinet", Vec2{ 20, 60 }))
{
clarinet.play();
}
if (SimpleGUI::Button(U"Trumpet", Vec2{ 20, 100 }))
{
trumpet.play();
}
}
}
37.4 音声波形データからオーディオを作成する¶
プログラムで生成・加工した音声波形データ(Wave
クラス)からオーディオを作成できます。Wave
クラスについては チュートリアル 54. 音声波形 を参照してください。
# include <Siv3D.hpp>
Wave MakeWave()
{
Wave wave{ Wave::DefaultSampleRate * 2 };
for (size_t i = 0; i < wave.size(); ++i)
{
const double t = (static_cast<double>(i) / Wave::DefaultSampleRate);
wave[i] = static_cast<float>(Math::Sin(t * 220.0 * 2_pi));
}
return wave;
}
void Main()
{
// 音声波形データからオーディオを作成する
const Audio audio{ MakeWave() };
audio.play();
while (System::Update())
{
}
}
37.5 同じオーディオを重ねて再生する¶
1 つの Audio
を重ねて再生したい場合には .play()
の代わりに .playOneShot()
を使います。.playOneShot()
の引数には音量、パン、再生スピードを渡せます。
# include <Siv3D.hpp>
void Main()
{
// ピアノの C4 (ド) の音。
const Audio piano{ GMInstrument::Piano1, PianoKey::C4, 0.5s };
// クラリネットの D4 (レ) の音
const Audio clarinet{ GMInstrument::Clarinet, PianoKey::D4, 0.5s };
// トランペットの E4 (ミ) の音
const Audio trumpet{ GMInstrument::Trumpet, PianoKey::E4, 0.5s };
while (System::Update())
{
if (SimpleGUI::Button(U"Piano", Vec2{ 20, 20 }))
{
// 音量 0.5 で再生
piano.playOneShot(0.5);
}
if (SimpleGUI::Button(U"Clarinet", Vec2{ 20, 60 }))
{
clarinet.playOneShot(0.5);
}
if (SimpleGUI::Button(U"Trumpet", Vec2{ 20, 100 }))
{
trumpet.playOneShot(0.5);
}
}
}
同時に再生する音声が多くならないようにする
同時に再生する音声が多くなると(とくに 64 を超えると)動作が不安定になる場合があります。音声の時間を短くしたり、重ねて再生する頻度を減らすなどして、同時に再生する音声の数を減らしてください。
37.6 空のオーディオ¶
Audio
型の変数は、デフォルトでは空のオーディオを持っています。音声ファイルのロードに失敗した場合も空のオーディオになります。
空のオーディオは、「フワ」と鳴る 0.5 秒の音で、有効なオーディオと同じように扱うことができ、再生してもエラーは発生しません。
空のオーディオであるかを調べるには、if (audio.isEmpty())
, if (audio)
, if (not audio)
を使います。
# include <Siv3D.hpp>
void Main()
{
// 初期データを与えないと、空のオーディオになる
Audio audioA;
if (not audioA)
{
Print << U"audioA is empty";
}
// 音声ファイルの読み込みに失敗すると、空のオーディオになる
Audio audioB{ U"aaa/bbb.wav" };
if (not audioB)
{
Print << U"audioB is empty";
}
while (System::Update())
{
if (SimpleGUI::Button(U"Play A", Vec2{ 200, 20 }))
{
audioA.playOneShot();
}
if (SimpleGUI::Button(U"Play B", Vec2{ 200, 60 }))
{
audioB.playOneShot();
}
}
}
37.7 一時停止と停止¶
再生中のオーディオを一時停止するには .pause()
, 停止して再生位置を最初に戻すには .stop()
を呼びます。
オーディオが再生中であるかは .isPlaying()
, 一時停止中であるかは .isPaused()
で取得できます。
# include <Siv3D.hpp>
void Main()
{
const Audio audio{ Audio::Stream, U"example/test.mp3" };
while (System::Update())
{
ClearPrint();
// 再生されているか
Print << U"isPlaying: " << audio.isPlaying();
// 一時停止中であるか
Print << U"isPaused: " << audio.isPaused();
if (SimpleGUI::Button(U"Play", Vec2{ 200, 20 }))
{
// 再生・再開
audio.play();
}
if (SimpleGUI::Button(U"Pause", Vec2{ 200, 60 }))
{
// 一時停止
audio.pause();
}
if (SimpleGUI::Button(U"Stop", Vec2{ 200, 100 }))
{
// 停止して再生位置を最初に戻す
audio.stop();
}
}
}
37.8 音量を変える¶
個別のオーディオの音量を変更するには .setVolume()
に 0.0~1.0 の値を設定します。デフォルトの音量は 1.0 です。設定されている音量は .getVolume()
で取得できます。
.setVolume()
は、.playOneShot()
で再生している音声には適用されません。
音量は等比で動かすと自然に聞こえる
音量と耳に聞こえる音の大きさの関係は、人間の耳の特性により、線形ではなく対数的な関係になります。1.0 → 0.8 → 0.6 → 0.4 → 0.2 のように等差で変更すると、0 に近づくにつれ耳が感じる音量の変化が大きくなってしまいます。代わりに 1.0 → 0.8 → 0.64 → 0.512 → 0.4096 のように等比で変更すると、より自然な音量変化として聞こえます。
# include <Siv3D.hpp>
void Main()
{
const Audio audio{ Audio::Stream, U"example/test.mp3" };
audio.play();
double volume = 1.0;
while (System::Update())
{
ClearPrint();
// 現在の音量を取得
Print << audio.getVolume();
if (SimpleGUI::Slider(U"volume: {:.2f}"_fmt(volume), volume, Vec2{ 200, 20 }, 160, 140))
{
// 音量を設定
audio.setVolume(volume);
}
}
}
37.9 フェードイン・フェードアウト¶
.play()
, .pause()
, .stop()
の引数に時間を渡すと、その時間をかけて音量がフェードイン・フェードアウトします。一時停止と停止は、フェードアウトが完了するまで遅延されます。
# include <Siv3D.hpp>
void Main()
{
const Audio audio{ Audio::Stream, U"example/test.mp3" };
while (System::Update())
{
ClearPrint();
// 再生されているか
Print << U"isPlaying: " << audio.isPlaying();
// 一時停止中であるか
Print << U"isPaused: " << audio.isPaused();
// 現在の音量
Print << audio.getVolume();
if (SimpleGUI::Button(U"Play", Vec2{ 200, 20 }))
{
// 2 秒かけて再生・再開
audio.play(2s);
}
if (SimpleGUI::Button(U"Pause", Vec2{ 200, 60 }))
{
// 2 秒かけて一時停止
audio.pause(2s);
}
if (SimpleGUI::Button(U"Stop", Vec2{ 200, 100 }))
{
// 2 秒かけて停止して再生位置を最初に戻す
audio.stop(2s);
}
}
}
37.10 再生中に音量を徐々に変える¶
.fadeVolume(volume, duration)
を使うと、指定した時間 duration
だけかけて、音量が徐々に volume
へ変化します。
# include <Siv3D.hpp>
void Main()
{
const Audio audio{ Audio::Stream, U"example/test.mp3" };
audio.play();
while (System::Update())
{
ClearPrint();
// 現在の音量
Print << audio.getVolume();
if (SimpleGUI::Button(U"1.0", Vec2{ 200, 20 }))
{
// 2 秒かけて音量を 1.0 に
audio.fadeVolume(1.0, 2s);
}
if (SimpleGUI::Button(U"0.5", Vec2{ 200, 60 }))
{
// 1 秒かけて音量を 0.5 に
audio.fadeVolume(0.5, 1s);
}
if (SimpleGUI::Button(U"0.1", Vec2{ 200, 100 }))
{
// 1.5 秒かけて音量を 0.1 に
audio.fadeVolume(0.1, 1.5s);
}
}
}
37.11 再生スピードを変える¶
再生スピードを変えるには .setSpeed(speed)
または .fadeSpeed(speed, duration)
を使って、再生スピードの倍率を設定します。デフォルトは 1.0 です。再生スピードが速くなると音は高く聞こえ、遅くなると低く聞こえます。スピードを早くしても音の高低が発生しないようにするには、37.18 の「ピッチシフト」と組み合わせます。
現在の再生スピードは .getSpeed()
で取得できます。
# include <Siv3D.hpp>
void Main()
{
const Audio audio{ Audio::Stream, U"example/test.mp3" };
audio.play();
while (System::Update())
{
ClearPrint();
// 現在のスピード
Print << audio.getSpeed();
if (SimpleGUI::Button(U"1.2", Vec2{ 200, 20 }))
{
// 2 秒かけてスピードを 1.2 に
audio.fadeSpeed(1.2, 2s);
}
if (SimpleGUI::Button(U"1.0", Vec2{ 200, 60 }))
{
// 1 秒かけてスピードを 1.0 に
audio.fadeSpeed(1.0, 1s);
}
if (SimpleGUI::Button(U"0.8", Vec2{ 200, 100 }))
{
// 1.5 秒かけてスピードを 0.8 に
audio.fadeSpeed(0.8, 1.5s);
}
}
}
37.12 パンを変える¶
左右の音量バランス(パン)を変えるには .setPan(pan)
または .fadePan(pan, duration)
を使って、パンを -1.0~1.0 の範囲で設定します。-1.0 は最も左、1.0 は最も右、デフォルトは中央の 0.0 です。
現在のパンは .getPan()
で取得できます。
# include <Siv3D.hpp>
void Main()
{
const Audio audio{ Audio::Stream, U"example/test.mp3" };
audio.play();
while (System::Update())
{
ClearPrint();
// 現在のパン
Print << audio.getPan();
if (SimpleGUI::Button(U"0.9", Vec2{ 200, 20 }))
{
// 2 秒かけてパンを 0.9 に
audio.fadePan(0.9, 2s);
}
if (SimpleGUI::Button(U"0.0", Vec2{ 200, 60 }))
{
// 1 秒かけてパンを 0.0 に
audio.fadePan(0.0, 1s);
}
if (SimpleGUI::Button(U"-0.9", Vec2{ 200, 100 }))
{
// 1.5 秒かけてパンを -0.9 に
audio.fadePan(-0.9, 1.5s);
}
}
}
37.13 再生位置を取得する¶
オーディオの合計再生時間(秒)は .lengthSec()
, 合計再生サンプルは .samples()
で取得できます。現在の再生位置を .posSec()
では秒、 .posSample()
ではサンプル数で取得できます。
音楽のタイミングに合わせた演出や、音楽ゲームの判定に、これらの値を使うことができます。
# include <Siv3D.hpp>
void Main()
{
const Audio audio{ Audio::Stream, U"example/test.mp3" };
audio.play();
while (System::Update())
{
ClearPrint();
// 曲全体の長さ
Print << U"all: {:.1f} sec ({} samples)"_fmt(audio.lengthSec(), audio.samples());
// 現在の再生位置
Print << U"play: {:.1f} sec ({} samples)"_fmt(audio.posSec(), audio.posSample());
}
}
37.14 再生位置を変更する¶
再生位置を変更するには、.seekSamples()
で移動先の位置をサンプル単位で指定するか、.seekTime()
で移動先の位置を時間(秒)で指定します。
# include <Siv3D.hpp>
void Main()
{
const Audio audio{ Audio::Stream, U"example/test.mp3" };
audio.play();
while (System::Update())
{
ClearPrint();
// 曲全体
Print << U"all: {:.1f} sec ({} samples)"_fmt(audio.lengthSec(), audio.samples());
// 再生位置
Print << U"play: {:.1f} sec ({} samples)"_fmt(audio.posSec(), audio.posSample());
if (SimpleGUI::Button(U"0 samples", Vec2{ 300, 20 }))
{
// 0 サンプル目(曲の先頭)に移動
audio.seekSamples(0);
}
if (SimpleGUI::Button(U"441,000 samples", Vec2{ 300, 60 }))
{
// 441,000 サンプル目に移動
audio.seekSamples(441000);
}
if (SimpleGUI::Button(U"20.0 sec", Vec2{ 300, 100 }))
{
// 20 秒の位置に移動
audio.seekTime(20.0);
}
}
}
37.15 ループ再生する¶
曲の再生が終端に到達したとき、自動的に先頭から再び再生(ループ)させたい場合は、Audio
のコンストラクタに Loop::Yes
を指定します。
# include <Siv3D.hpp>
void Main()
{
const Audio audio{ Audio::Stream, U"example/test.mp3", Loop::Yes };
audio.play();
while (System::Update())
{
ClearPrint();
// ループが設定されているか
Print << audio.isLoop();
// ループ回数
Print << audio.loopCount();
// 曲全体
Print << U"all: {:.1f} sec ({} samples)"_fmt(audio.lengthSec(), audio.samples());
// 再生位置
Print << U"play: {:.1f} sec ({} samples)"_fmt(audio.posSec(), audio.posSample());
}
}
37.16 区間を指定してループ再生する¶
オーディオの再生が指定したループ終端位置に到達したとき、指定したループ先頭位置に戻って区間ループ再生させるには、ループ区間の先頭位置を Arg::loopBegin
, 終端位置を Arg::loopEnd
で指定します。位置の指定方法はサンプル数か時間かを選べますが、begin と end で揃える必要があります。
Arg::loopEnd
を指定するとそれ以降のオーディオデータは保持せず、メモリの消費量が節約されます。
ストリーミング再生では、Arg::loopEnd
を指定できない制約があります。必要な場合は音声データをあらかじめ事前にカットする加工が必要です。
波形のずれにより、ループの瞬間にノイズ音が生じる場合があります。
# include <Siv3D.hpp>
void Main()
{
// 1.5 秒~44.5 秒の区間をループ
const Audio audio{ U"example/test.mp3", Arg::loopBegin = 1.5s, Arg::loopEnd = 44.5s };
audio.play();
while (System::Update())
{
ClearPrint();
// ループが設定されているか
Print << audio.isLoop();
// ループ回数
Print << audio.loopCount();
// 曲全体
Print << U"all: {:.1f} sec ({} samples)"_fmt(audio.lengthSec(), audio.samples());
// 再生位置
Print << U"play: {:.1f} sec ({} samples)"_fmt(audio.posSec(), audio.posSample());
}
}
37.17 ミキシングバスとグローバルオーディオ¶
オーディオを BGM, 環境音、キャラクターボイスなどのグループに分類し、グループごとに音量などを制御したい場合、ミキシングバス が役に立ちます。すべてのオーディオは MixBus0 ~ MixBus3 の 4 つのグループのいずれかのミキシングバスを通り、ミキシングバスを通過したすべてのオーディオはグローバルオーディオを通って再生されます。
個々のミキシングバスでは次のような操作ができます。 - 音量の調整 - 再生中の波形サンプルの取得 - 再生中の波形の FFT の結果取得 - オーディオフィルタの適用(次節で説明)
グローバルオーディオでは次のような操作ができます。 - オーディオの一斉停止・再開 - 音量の調整 - 再生中の波形サンプルの取得 - 再生中の波形の FFT の結果取得
最終的に出力される音量は、
Audio
で設定された音量 × ミキシングバスの音量 × グローバルオーディオの音量 です。
Audio
をどのミキシングバスで再生するかを指定するには、.play()
または .playOneShot()
の引数に MixBus0
~ MixBus3
を指定します(デフォルトでは MixBus0
)。
ミキシングバスの音量を調整するには GlobalAudio::BusSetVolume(busIndex, volume)
, GlobalAudio::BusFadeVolume(busIndex, volume, duration)
を呼びます。
グローバルオーディオの音量を調整するには GlobalAudio::SetVolume(volume)
, GlobalAudio::FadeVolume(volume, duration)
を呼びます。
# include <Siv3D.hpp>
void Main()
{
// ピアノの C4 (ド) の音
const Audio pianoC{ GMInstrument::Piano1, PianoKey::C4, 0.5s };
// ピアノの D4 (レ) の音
const Audio pianoD{ GMInstrument::Piano1, PianoKey::D4, 0.5s };
// ピアノの E4 (ミ) の音
const Audio pianoE{ GMInstrument::Piano1, PianoKey::E4, 0.5s };
double globalVolume = 1.0, mixBus0Volume = 1.0, mixBus1Volume = 1.0;
while (System::Update())
{
if (SimpleGUI::Slider(U"Global Vol", globalVolume, Vec2{ 20, 20 }, 120, 200))
{
// グローバルオーディオの音量を変更
GlobalAudio::SetVolume(globalVolume);
}
if (SimpleGUI::Slider(U"Bus0 Vol", mixBus0Volume, Vec2{ 20, 60 }, 120, 120))
{
// MixBus0 の音量を変更
GlobalAudio::BusSetVolume(MixBus0, mixBus0Volume);
}
if (SimpleGUI::Slider(U"Bus1 Vol", mixBus1Volume, Vec2{ 300, 60 }, 120, 120))
{
// MixBus1 の音量を変更
GlobalAudio::BusSetVolume(MixBus1, mixBus1Volume);
}
if (SimpleGUI::Button(U"C Bus0", Vec2{ 20, 100 }))
{
pianoC.playOneShot(MixBus0, 0.5);
}
if (SimpleGUI::Button(U"D Bus0", Vec2{ 20, 140 }))
{
pianoD.playOneShot(MixBus0, 0.5);
}
if (SimpleGUI::Button(U"E Bus0", Vec2{ 20, 180 }))
{
pianoE.playOneShot(MixBus0, 0.5);
}
if (SimpleGUI::Button(U"C Bus1", Vec2{ 300, 100 }))
{
pianoC.playOneShot(MixBus1, 0.5);
}
if (SimpleGUI::Button(U"D Bus1", Vec2{ 300, 140 }))
{
pianoD.playOneShot(MixBus1, 0.5);
}
if (SimpleGUI::Button(U"E Bus1", Vec2{ 300, 180 }))
{
pianoE.playOneShot(MixBus1, 0.5);
}
}
}
37.18 ピッチシフト¶
ピッチシフトとは、再生スピードを変えずに音の高さを変えることです。ミキシングバスには、ピッチシフトフィルタを適用することができます。
GlobalAudio::BusSetPitchShiftFilter(mixbus, index, pitchShift)
で、ミキシングバスの index
番目のフィルタにピッチシフトを設定します。pitchShift
は 0.0 がピッチを変えないことを表します。12.0 が 1 オクターブ上げることを表します。-12.0 が 1 オクターブ下げることを表します。
# include <Siv3D.hpp>
void Main()
{
// 音声ファイルを読み込んで Audio を作成する
const Audio audio{ U"example/test.mp3" };
// オーディオを再生する
audio.play();
// ピッチシフトの量
double pitchShift = 0.0;
while (System::Update())
{
if (SimpleGUI::Slider(U"pitchShift: {:.2f}"_fmt(pitchShift), pitchShift, -12.0, 12.0, Vec2{ 40, 40 }, 160, 300))
{
// MixBus0 の 0 番目のフィルタにピッチシフトを設定する
GlobalAudio::BusSetPitchShiftFilter(MixBus0, 0, pitchShift);
}
}
}
37.19 オーディオの波形を取得する¶
ストリーミングでないオーディオからは、.getSamples(channel)
でオーディオ全体の波形データを取得できます。channel
は、0 が左チャンネル、1 が右チャンネルを表し、それぞれのチャンネルの波形の先頭のポインタ const float*
を返します。
# include <Siv3D.hpp>
void Main()
{
const Audio audio{ U"example/test.mp3" };
audio.play();
const float* pSamples = audio.getSamples(0);
LineString lines(800);
while (System::Update())
{
const int64 posSample = audio.posSample();
// 現在の再生位置から 800 サンプル分の波形を取得する
for (int64 i = posSample; i < (posSample + 800); ++i)
{
if (i < audio.samples())
{
lines[i - posSample].set((i - posSample), (300 + pSamples[i] * 200));
}
else
{
lines[i - posSample].set((i - posSample), 300);
}
}
// 波形を描画する
lines.draw(2);
}
}
37.20 再生されている直近の波形を取得する¶
ミキシングバスを通過した直近 256 サンプルの波形データを取得できます。ストリーミング等の有無にかかわらず、そのミキシングバスを通して再生されるすべてのオーディオの合成波形を取得します。
関数 | 取得先 |
---|---|
GlobalAudio::GetSamples() |
グローバルオーディオ |
GlobalAudio::BusGetSamples(mixbus) |
ミキシングバス |
# include <Siv3D.hpp>
void Main()
{
const Audio audio1{ U"example/test.mp3" };
const Audio audio2{ GMInstrument::Trumpet, PianoKey::E4, 0.5s };
audio1.play();
LineString lines(256);
Array<float> samples;
while (System::Update())
{
if (KeySpace.down())
{
audio2.playOneShot();
}
// グローバルオーディオを通過した直近 256 サンプル分の波形を取得する
GlobalAudio::GetSamples(samples);
for (size_t i = 0; i < samples.size(); ++i)
{
lines[i].set((i * 800.0 / 256.0), (300 + samples[i] * 200));
}
// 波形を描画する
lines.draw(2);
}
}
37.21 オーディオフィルタ¶
1 つのミキシングバスには最大 8 つのオーディオフィルタを設定し、オーディオの再生中に、リアルタイムでの音声波形加工ができます。
関数(引数は省略) | 説明 |
---|---|
GlobalAudio::BusClearFilter() |
設定されているフィルタをオフにします |
GlobalAudio::BusSetLowPassFilter() |
ローパスフィルタを設定します |
GlobalAudio::BusSetHighPassFilter() |
ハイパスフィルタを設定します |
GlobalAudio::BusSetEchoFilter() |
エコーフィルタを設定します |
GlobalAudio::BusSetReverbFilter() |
リバーブフィルタを設定します |
GlobalAudio::BusSetPitchShiftFilter() |
ピッチシフトフィルタを設定します |
Web 版では、ピッチシフトフィルタを利用できません。GlobalAudio::SupportsPitchShift()
を使うと、現在の実行環境でピッチシフトフィルタを利用できるかを取得できます。
次のサンプルは、オーディオフィルタ機能のデモです。「Open audio file」をクリックすると、パソコンに保存されているオーディオファイルをオープンできます。詳しくはチュートリアル 46. ファイルダイアログを参照してください。
# include <Siv3D.hpp>
void Main()
{
Window::Resize(1280, 720);
Audio audio;
double posSec = 0.0;
double volume = 1.0;
double pan = 0.0;
double speed = 1.0;
bool loop = false;
Array<float> busSamples;
Array<float> globalSamples;
FFTResult busFFT;
FFTResult globalFFT;
LineString lines(256, Vec2{ 0, 0 });
bool pitch = false;
double pitchShift = 0.0;
bool lpf = false;
double lpfCutoffFrequency = 800.0;
double lpfResonance = 0.5;
double lpfWet = 1.0;
bool hpf = false;
double hpfCutoffFrequency = 800.0;
double hpfResonance = 0.5;
double hpfWet = 1.0;
bool echo = false;
double delay = 0.1;
double decay = 0.5;
double echoWet = 0.5;
bool reverb = false;
bool freeze = false;
double roomSize = 0.5;
double damp = 0.0;
double width = 0.5;
double reverbWet = 0.5;
while (System::Update())
{
ClearPrint();
Print << U"GlobalAudio::GetActiveVoiceCount(): " << GlobalAudio::GetActiveVoiceCount();
Print << U"isEmpty : " << audio.isEmpty();
Print << U"isStreaming : " << audio.isStreaming();
Print << U"sampleRate : " << audio.sampleRate();
Print << U"samples : " << audio.samples();
Print << U"lengthSec : " << audio.lengthSec();
Print << U"posSample : " << audio.posSample();
Print << U"posSec : " << (posSec = audio.posSec());
Print << U"isActive : " << audio.isActive();
Print << U"isPlaying : " << audio.isPlaying();
Print << U"isPaused : " << audio.isPaused();
Print << U"samplesPlayed : " << audio.samplesPlayed();
Print << U"isLoop : " << (loop = audio.isLoop());
Print << U"getLoopTimingtLoop : " << audio.getLoopTiming().beginPos << U", " << audio.getLoopTiming().endPos;
Print << U"loopCount : " << audio.loopCount();
Print << U"getVolume : " << (volume = audio.getVolume());
Print << U"getPan : " << (pan = audio.getPan());
Print << U"getSpeed : " << (speed = audio.getSpeed());
if (SimpleGUI::Button(U"Open audio file", Vec2{ 60, 560 }))
{
audio.stop(0.5s);
audio = Dialog::OpenAudio(Audio::Stream);
}
{
GlobalAudio::BusGetSamples(MixBus0, busSamples);
GlobalAudio::BusGetFFT(MixBus0, busFFT);
for (auto&& [i, s] : Indexed(busSamples))
{
lines[i].set((300.0 + i), (200.0 - s * 100.0));
}
if (busSamples)
{
lines.draw(2, Palette::Orange);
}
for (auto&& [i, s] : Indexed(busFFT.buffer))
{
RectF{ Arg::bottomLeft(300 + i, 300), 1, (s * 4) }.draw();
}
}
{
GlobalAudio::GetSamples(globalSamples);
GlobalAudio::GetFFT(globalFFT);
for (auto&& [i, s] : Indexed(globalSamples))
{
lines[i].set((300.0 + i), (550.0 - s * 100.0));
}
if (globalSamples)
{
lines.draw(2, Palette::Orange);
}
for (auto&& [i, s] : Indexed(globalFFT.buffer))
{
RectF{ Arg::bottomLeft(300 + i, 650), 1, (s * 4) }.draw();
}
}
if (SimpleGUI::Button(U"Play", Vec2{ 600, 20 }, 80, !audio.isPlaying()))
{
audio.play();
}
if (SimpleGUI::Button(U"Pause", Vec2{ 690, 20 }, 80, (audio.isPlaying() && !audio.isPaused())))
{
audio.pause();
}
if (SimpleGUI::Button(U"Stop", Vec2{ 780, 20 }, 80, (audio.isPlaying() || audio.isPaused())))
{
audio.stop();
}
if (SimpleGUI::Button(U"Play in 2s", Vec2{ 870, 20 }, 120, !audio.isPlaying()))
{
audio.play(2s);
}
if (SimpleGUI::Button(U"Pause in 2s", Vec2{ 1000, 20 }, 120, (audio.isPlaying() && !audio.isPaused())))
{
audio.pause(2s);
}
if (SimpleGUI::Button(U"Stop in 2s", Vec2{ 1130, 20 }, 120, (audio.isPlaying() || audio.isPaused())))
{
audio.stop(2s);
}
if (SimpleGUI::Slider(U"{:.1f} / {:.1f}"_fmt(posSec, audio.lengthSec()), posSec, 0.0, audio.lengthSec(), Vec2{ 600, 60 }, 160, 360))
{
if (MouseL.down() || !Cursor::DeltaF().isZero()) // シークの連続(ノイズの原因)を防ぐ
{
audio.seekTime(posSec);
}
}
if (SimpleGUI::CheckBox(loop, U"Loop", Vec2{ 1130, 60 }))
{
audio.setLoop(loop);
}
if (SimpleGUI::Slider(U"volume: {:.2f}"_fmt(volume), volume, Vec2{ 600, 110 }, 140, 130))
{
audio.setVolume(volume);
}
if (SimpleGUI::Button(U"0.0 in 2s", Vec2{ 880, 110 }, 110, audio.isActive()))
{
audio.fadeVolume(0.0, 2s);
}
if (SimpleGUI::Button(U"0.5 in 2s", Vec2{ 1000, 110 }, 110, audio.isActive()))
{
audio.fadeVolume(0.5, 2s);
}
if (SimpleGUI::Button(U"1.0 in 2s", Vec2{ 1120, 110 }, 110, audio.isActive()))
{
audio.fadeVolume(1.0, 2s);
}
if (SimpleGUI::Slider(U"pan: {:.2f}"_fmt(pan), pan, -1.0, 1.0, Vec2{ 600, 150 }, 140, 130))
{
audio.setPan(pan);
}
if (SimpleGUI::Button(U"-1.0 in 2s", Vec2{ 880, 150 }, 110, audio.isActive()))
{
audio.fadePan(-1.0, 2s);
}
if (SimpleGUI::Button(U"0.0 in 2s", Vec2{ 1000, 150 }, 110, audio.isActive()))
{
audio.fadePan(0.0, 2s);
}
if (SimpleGUI::Button(U"1.0 in 2s", Vec2{ 1120, 150 }, 110, audio.isActive()))
{
audio.fadePan(1.0, 2s);
}
if (SimpleGUI::Slider(U"speed: {:.3f}"_fmt(speed), speed, 0.0, 4.0, Vec2{ 600, 190 }, 140, 130))
{
audio.setSpeed(speed);
}
if (SimpleGUI::Button(U"0.8 in 2s", Vec2{ 880, 190 }, 110, audio.isActive()))
{
audio.fadeSpeed(0.8, 2s);
}
if (SimpleGUI::Button(U"1.0 in 2s", Vec2{ 1000, 190 }, 110, audio.isActive()))
{
audio.fadeSpeed(1.0, 2s);
}
if (SimpleGUI::Button(U"1.2 in 2s", Vec2{ 1120, 190 }, 110, audio.isActive()))
{
audio.fadeSpeed(1.2, 2s);
}
bool updatePitch = false;
bool updateLPF = false;
bool updateHPF = false;
bool updateEcho = false;
bool updateReverb = false;
if (SimpleGUI::CheckBox(pitch, U"Pitch", Vec2{ 600, 240 }, 120, GlobalAudio::SupportsPitchShift()))
{
if (pitch)
{
updatePitch = true;
}
else
{
GlobalAudio::BusClearFilter(MixBus0, 0);
}
}
updatePitch |= SimpleGUI::Slider(U"pitchShift: {:.2f}"_fmt(pitchShift), pitchShift, -12.0, 12.0, Vec2{ 720, 240 }, 160, 300);
if (SimpleGUI::CheckBox(lpf, U"LPF", Vec2{ 600, 280 }, 120))
{
if (lpf)
{
updateLPF = true;
}
else
{
GlobalAudio::BusClearFilter(MixBus0, 1);
}
}
updateLPF |= SimpleGUI::Slider(U"cutoffFrequency: {:.0f}"_fmt(lpfCutoffFrequency), lpfCutoffFrequency, 10, 4000, Vec2{ 720, 280 }, 220, 240);
updateLPF |= SimpleGUI::Slider(U"resonance: {:.2f}"_fmt(lpfResonance), lpfResonance, 0.1, 8.0, Vec2{ 720, 310 }, 220, 240);
updateLPF |= SimpleGUI::Slider(U"wet: {:.2f}"_fmt(lpfWet), lpfWet, Vec2{ 720, 340 }, 220, 240);
if (SimpleGUI::CheckBox(hpf, U"HPF", Vec2{ 600, 380 }, 120))
{
if (hpf)
{
updateHPF = true;
}
else
{
GlobalAudio::BusClearFilter(MixBus0, 2);
}
}
updateHPF |= SimpleGUI::Slider(U"cutoffFrequency: {:.0f}"_fmt(hpfCutoffFrequency), hpfCutoffFrequency, 10, 4000, Vec2{ 720, 380 }, 220, 240);
updateHPF |= SimpleGUI::Slider(U"resonance: {:.2f}"_fmt(hpfResonance), hpfResonance, 0.1, 8.0, Vec2{ 720, 410 }, 220, 240);
updateHPF |= SimpleGUI::Slider(U"wet: {:.2f}"_fmt(hpfWet), hpfWet, Vec2{ 720, 440 }, 220, 240);
if (SimpleGUI::CheckBox(echo, U"Echo", Vec2{ 600, 480 }, 120))
{
if (echo)
{
updateEcho = true;
}
else
{
GlobalAudio::BusClearFilter(MixBus0, 3);
}
}
updateEcho |= SimpleGUI::Slider(U"delay: {:.2f}"_fmt(delay), delay, Vec2{ 720, 480 }, 220, 240);
updateEcho |= SimpleGUI::Slider(U"decay: {:.2f}"_fmt(decay), decay, Vec2{ 720, 510 }, 220, 240);
updateEcho |= SimpleGUI::Slider(U"wet: {:.2f}"_fmt(echoWet), echoWet, Vec2{ 720, 540 }, 220, 240);
if (SimpleGUI::CheckBox(reverb, U"Reverb", Vec2{ 600, 580 }, 120))
{
if (reverb)
{
updateReverb = true;
}
else
{
GlobalAudio::BusClearFilter(MixBus0, 4);
}
}
updateReverb |= SimpleGUI::CheckBox(freeze, U"freeze", Vec2{ 720, 580 }, 110);
updateReverb |= SimpleGUI::Slider(U"roomSize: {:.2f}"_fmt(roomSize), roomSize, 0.001, 1.0, { 830, 580 }, 150, 200);
updateReverb |= SimpleGUI::Slider(U"damp: {:.2f}"_fmt(damp), damp, Vec2{ 720, 610 }, 220, 240);
updateReverb |= SimpleGUI::Slider(U"width: {:.2f}"_fmt(width), width, Vec2{ 720, 640 }, 220, 240);
updateReverb |= SimpleGUI::Slider(U"wet: {:.2f}"_fmt(reverbWet), reverbWet, Vec2{ 720, 670 }, 220, 240);
if (pitch && updatePitch)
{
GlobalAudio::BusSetPitchShiftFilter(MixBus0, 0, pitchShift);
}
if (lpf && updateLPF)
{
GlobalAudio::BusSetLowPassFilter(MixBus0, 1, lpfCutoffFrequency, lpfResonance, lpfWet);
}
if (hpf && updateHPF)
{
GlobalAudio::BusSetHighPassFilter(MixBus0, 2, hpfCutoffFrequency, hpfResonance, hpfWet);
}
if (echo && updateEcho)
{
GlobalAudio::BusSetEchoFilter(MixBus0, 3, delay, decay, echoWet);
}
if (reverb && updateReverb)
{
GlobalAudio::BusSetReverbFilter(MixBus0, 4, freeze, roomSize, damp, width, reverbWet);
}
}
// 再生中の音声があれば、フェードアウトさせてから終了
if (GlobalAudio::GetActiveVoiceCount())
{
GlobalAudio::FadeVolume(0.0, 0.5s);
System::Sleep(0.5s);
}
}
37.22 オーディオグループ¶
オーディオグループを使うと、複数のオーディオを完全に同期するタイミングで再生できます。 詳しいサンプルとサンプル用の音声ファイルは Siv3D-Sample | BGM クロスフェード を参照してください。
37.23 波形のリアルタイム書き込み¶
IAudioStream
を継承したクラスを作成することで、波形のリアルタイム書き込みを行うことができます。
void getAudio(float* left, float* right, const size_t samplesToWrite) override
は、波形の書き込みを行うための関数です。left
と right
は、それぞれ左右のチャンネルの波形を書き込むためのバッファで、書き込む必要があるサンプル数が samplesToWrite
によって渡されます。再生を終了する場合 bool hasEnded() override
をオーバーライドして true
を返します。先頭への巻き戻しが行われる場合に void rewind() override
が呼ばれます。
# include <Siv3D.hpp>
class MyAudioStream : public IAudioStream
{
public:
void setFrequency(int32 frequency)
{
m_oldFrequency = m_frequency.load();
m_frequency.store(frequency);
}
private:
size_t m_pos = 0;
std::atomic<int32> m_oldFrequency = 440;
std::atomic<int32> m_frequency = 440;
void getAudio(float* left, float* right, const size_t samplesToWrite) override
{
const int32 oldFrequency = m_oldFrequency;
const int32 frequency = m_frequency;
const float blend = (1.0f / samplesToWrite);
for (size_t i = 0; i < samplesToWrite; ++i)
{
const float t0 = (2_piF * oldFrequency * (static_cast<float>(m_pos) / Wave::DefaultSampleRate));
const float t1 = (2_piF * frequency * (static_cast<float>(m_pos) / Wave::DefaultSampleRate));
const float a = (Math::Lerp(std::sin(t0), std::sin(t1), (blend * i))) * 0.5f;
*left++ = *right++ = a;
++m_pos;
}
m_oldFrequency = frequency;
m_pos %= Math::LCM(frequency, Wave::DefaultSampleRate);
}
bool hasEnded() override
{
return false;
}
void rewind() override
{
m_pos = 0;
}
};
void Main()
{
std::shared_ptr<MyAudioStream> audioStream = std::make_shared<MyAudioStream>();
Audio audio{ audioStream };
audio.play();
double frequency = 440.0;
while (System::Update())
{
if (SimpleGUI::Slider(U"{}Hz"_fmt(static_cast<int32>(frequency)), frequency, 220.0, 880.0, Vec2{ 40, 40 }, 120, 200))
{
audioStream->setFrequency(static_cast<int32>(frequency));
}
}
}