はじめに
Siv3D には組み込み音源があります。
この組み込み音源は使える音の種類がかなり多く、どの音を使うか迷いどころです。
そこで、組み込み音源を試聴するプログラムを作りました!
音源の内容を設定して試聴したり、設定した内容を Audio クラスにそのまま渡せる形としてクリップボードにコピーしたりできます。
音源の設定
Siv3D の組み込み音源は、楽器・音の高さ・音の長さ 3 つの値で音を決めます。
楽器
楽器については、GMInstrument::Piano1 のように指定して音色を使えます。
GMInstrument には General MIDI という MIDI の規格にならい 128 種類の音色が列挙されています。
General MIDI は音色を Piano や Guitar などからなる 16 種類の分類に分けており、更に各分類にはそれぞれ 8 つの具体的な楽器が含まれています。
これにならって、アプリ上では楽器分類と楽器の 2 つのプルダウンにより音色を特定するようにしています。
音の高さ
音の高さは PianoKey で決められます。
音の高さも計 128 種類あり、最も低い音が C_1, 最も高い音が G9 です。真ん中のドは C4 です。
-1 から 9 までの 11 オクターブ分が使えますが、最も高い 9 オクターブ目では GS9, A9, AS9, B9 の 4 つの音が扱えないので注意です。
アプリ上では、鍵盤からキーを決め、-1 ~ 9 のラジオボタンでオクターブを指定するようにして音の高さを決めます。
GS9 以降の4音に関しては、選択しても G9 が鳴るようにしています。
音の長さ
ノートオン・ノートオフはそれぞれ、直感的には音の長さ・音の減衰にかかる長さを示しています。
例えば、ノートオフが 0 だとノートオンの時間が過ぎた後一瞬で音が途切れ、1.5 だと音が 1.5 秒かけて減衰していきます。
パーカッシブ系など、楽器によってはノートオフの値が音の聞こえ方に影響を与えないことがあるみたいです。
ソースコード
プルダウンメニューは公式のサンプルを少し改変して利用しています。
ソースコード
# include <Siv3D.hpp>
enum struct InstrumentTag {
Piano,
ChromaticPercussion,
Organ,
Guitar,
Bass,
Strings,
Ensemble,
Brass,
Reed,
Pipe,
SynthLead,
SynthPad,
SynthEffects,
Ethnic,
Percussive,
SoundEffects
};
class Pulldown
{
public:
Pulldown() = default;
Pulldown(const Array<String>& items, const Font& font, const double fontSize, const Vec2& pos)
: m_font{ font }
, m_fontSize{ fontSize }
, m_items{ items }
, m_maxItemWidth{ calcMaxItemWidth() }
, m_rect{ calcRect(pos) } {
}
bool isEmpty() const noexcept
{
return m_items.empty();
}
void setPos(const Vec2& pos) noexcept
{
m_rect.setPos(pos);
}
void setItems(const Array<String>& items) noexcept {
m_items = items;
}
void setRectWidth(const double width) noexcept {
m_rect.w = width;
}
void setIndex(const size_t index) noexcept {
m_index = index;
}
[[nodiscard]]
const RectF& getRect() const noexcept
{
return m_rect;
}
[[nodiscard]]
size_t getIndex() const noexcept
{
return m_index;
}
[[nodiscard]]
const Array<String>& getItems() const noexcept
{
return m_items;
}
bool update()
{
if (isEmpty())
{
return false;
}
if (m_rect.leftClicked())
{
m_isOpen = (not m_isOpen);
MouseL.clearInput();
}
if (not m_isOpen)
{
return false;
}
if (MouseL.down()
&& not RectF{ m_rect.pos, SizeF{m_rect.w, (m_items.size() + 1) * m_rect.h} }.mouseOver())
{
m_isOpen = (not m_isOpen);
MouseL.clearInput();
}
Vec2 itemPos = m_rect.pos.movedBy(0, m_rect.h);
for (size_t i = 0; i < m_items.size(); ++i)
{
const RectF itemRect{ itemPos, m_rect.w, m_rect.h };
if (itemRect.leftClicked())
{
m_index = i;
m_isOpen = false;
MouseL.clearInput();
return true;
}
itemPos.y += m_rect.h;
}
return false;
}
void draw() const
{
m_rect.draw();
if (isEmpty())
{
return;
}
m_rect.drawFrame(1, 0, m_isOpen ? Palette::Orange : Palette::Gray);
m_font(m_items[m_index]).draw(m_fontSize, (m_rect.pos + Padding), TextColor);
Triangle{ (m_rect.rightX() - DownButtonSize / 2.0 - Padding.x), (m_rect.y + m_rect.h / 2.0),
(DownButtonSize * 0.5), 180_deg }.draw(TextColor);
if (not m_isOpen)
{
return;
}
Vec2 itemPos = m_rect.bl();
const RectF backRect{ itemPos, m_rect.w, (m_rect.h * m_items.size()) };
backRect.drawShadow({ 1, 1 }, 5, 0).draw();
for (const auto& item : m_items)
{
const RectF rect{ itemPos, m_rect.size };
if (rect.mouseOver())
{
rect.draw(Palette::Skyblue);
}
m_font(item).draw(m_fontSize, (itemPos + Padding), TextColor);
itemPos.y += m_rect.h;
}
backRect.drawFrame(1, 0, Palette::Gray);
}
private:
[[nodiscard]]
double calcMaxItemWidth() const
{
double result = 0.0;
for (const auto& item : m_items)
{
result = Max(result, (m_font(item).region(m_fontSize).w));
}
return result;
}
[[nodiscard]]
RectF calcRect(const Vec2& pos) const noexcept
{
const double fontHeight = (m_font.height() * (m_fontSize / m_font.fontSize()));
return{
pos,
(m_maxItemWidth + (Padding.x * 3 + DownButtonSize)),
(fontHeight + Padding.y * 2)
};
}
static constexpr Size Padding{ 8, 2 };
static constexpr int32 DownButtonSize = 16;
static constexpr ColorF TextColor{ 0.11 };
Font m_font;
double m_fontSize = 12;
Array<String> m_items;
size_t m_index = 0;
double m_maxItemWidth = 0;
RectF m_rect{ 0 };
bool m_isOpen = false;
};
const Array<String> InstrumentNames = {
U"Piano1",U"Piano2",U"Piano3",U"Piano4",U"ElectricPiano1",U"ElectricPiano2",U"Harpsichord",U"Clavinet",U"Celesta",U"Glockenspiel",U"MusicBox",U"Vibraphone",U"Marimba",U"Xylophone",U"TubularBells",U"Dulcimer",U"DrawbarOrgan",U"PercussiveOrgan",U"RockOrgan",U"ChurchOrgan",U"ReedOrgan",U"Accordion",U"Harmonica",U"TangoAccordion",U"NylonGuitar",U"SteelGuitar",U"JazzGuitar",U"CleanGuitar",U"MutedGuitar",U"OverdrivenGuitar",U"DistortionGuitar",U"GuitarHarmonics",U"AcousticBass",U"FingeredBass",U"PickedBass",U"FretlessBass",U"SlapBass1",U"SlapBass2",U"SynthBass1",U"SynthBass2",U"Violin",U"Viola",U"Cello",U"Contrabass",U"TremoloStrings",U"PizzicatoStrings",U"OrchestralHarp",U"Timpani",U"StringEnsemble1",U"StringEnsemble2",U"SynthStrings1",U"SynthStrings2",U"ChoirAahs",U"VoiceOohs",U"SynthChoir",U"OrchestraHit",U"Trumpet",U"Trombone",U"Tuba",U"MutedTrumpet",U"FrenchHorn",U"BrassSection",U"SynthBrass1",U"SynthBrass2",U"SopranoSax",U"AltoSax",U"TenorSax",U"BaritoneSax",U"Oboe",U"EnglishHorn",U"Bassoon",U"Clarinet",U"Piccolo",U"Flute",U"Recorder",U"PanFlute",U"Blownbottle",U"Shakuhachi",U"Whistle",U"Ocarina",U"SquareWave",U"SawWave",U"SynCalliope",U"ChifferLead",U"Charang",U"SoloVox",U"FifthSawWave",U"BassAndLead",U"Fantasia",U"WarmPad",U"Polysynth",U"SpaceVoice",U"BowedGlass",U"MetalPad",U"HaloPad",U"SweepPad",U"IceRain",U"Soundtrack",U"Crystal",U"Atmosphere",U"Brightness",U"Goblin",U"EchoDrops",U"StarTheme",U"Sitar",U"Banjo",U"Shamisen",U"Koto",U"Kalimba",U"Bagpipe",U"Fiddle",U"Shanai",U"TinkleBell",U"Agogo",U"SteelDrums",U"Woodblock",U"TaikoDrum",U"MelodicTom",U"SynthDrum",U"ReverseCymbal",U"GuitarFretNoise",U"BreathNoise",U"Seashore",U"BirdTweet",U"TelephoneRing",U"Helicopter",U"Applause",U"Gunshot",
};
const Array<String> InstrumentTagNames = {
U"Piano",U"Chromatic Percussion",U"Organ",U"Guitar",U"Bass",U"Strings",U"Ensemble",U"Brass",U"Reed",U"Pipe",U"Synth Lead",U"Synth Pad",U"Synth Effects",U"Ethnic",U"Percussive",U"SoundEffects"
};
void drawRoundRectBack(RoundRect const& rect, ColorF const& col, bool const reversal = false) {
rect.movedBy(2, 2).draw(col.lerp(reversal ? Palette::White : Palette::Black, 0.5));
rect.movedBy(-2, -2).draw(col.lerp(reversal ? Palette::Black : Palette::White, 0.5));
}
void drawRoundRectWithGradation(RoundRect const& rect, ColorF const& col1, ColorF const& col2) {
rect.draw(Arg::top = col1, Arg::bottom = col2);
}
Array<String> makeInstrumentView(size_t const tagIdx) {
Array<String> view;
for (int32 i = tagIdx * 8; i < (tagIdx + 1) * 8; ++i) {
view.emplace_back(InstrumentNames[i]);
}
return view;
}
void Main()
{
using Key = std::pair<bool, size_t>;
const Font font(18);
// 白鍵に対する黒鍵の位置{ 直後の白鍵のインデックス, 黒鍵の配置(0:左寄り, 1:右寄り, 2:中心)} のペア
const Array<std::pair<int32, int32>> BlackKeyMap = {
{1, 0},
{2, 1},
{4, 0},
{5, 2},
{6, 1},
};
// 白鍵のインデックス
const Array<int32> WhiteKeyMap = { 0, 2, 4, 5, 7, 9, 11 };
// キーの名前
const Array<String> KeyNameMap = {
U"C",
U"CS",
U"D",
U"DS",
U"E",
U"F",
U"FS",
U"G",
U"GS",
U"A",
U"AS",
U"B"
};
// キー入力のマップ
const Array<std::pair<Input, Key>> KeyBoardInputMap = {
{ KeyA, { true, 0 } },
{ KeyW, { false, 0 } },
{ KeyS, { true, 1 } },
{ KeyE, { false, 1 } },
{ KeyD, { true, 2 } },
{ KeyF, { true, 3 } },
{ KeyT, { false, 2 } },
{ KeyG, { true, 4 } },
{ KeyY, { false, 3 } },
{ KeyH, { true, 5 } },
{ KeyU, { false, 4 } },
{ KeyJ, { true, 6 } }
};
// オクターブ入力のマップ
const Array<std::pair<Input, size_t>> OctaveInputMap = {
{ KeyMinus, 0 },
{ Key0, 1 },
{ Key1, 2 },
{ Key2, 3 },
{ Key3, 4 },
{ Key4, 5 },
{ Key5, 6 },
{ Key6, 7 },
{ Key7, 8 },
{ Key8, 9 },
{ Key9, 10 },
};
// 定数
constexpr SizeF WhiteKeySize{ 50, 170 };
constexpr SizeF BlackKeySize{ 30, 100 };
constexpr ColorF SelectedColor{ 0.35, 0.7, 1.0 };
constexpr ColorF CircleColor{ 0.75 };
constexpr Color SelectedFrameColor = Palette::Black;
constexpr Color BaseUICol1{ 183, 194, 205 };
constexpr Color BaseUICol2 = BaseUICol1.lerp(Palette::Black, 0.1);
constexpr Color BaseUIConcaveCol1{ 203, 214, 225 };
constexpr Color BaseUIConcaveCol2 = BaseUIConcaveCol1.lerp(Palette::Black, 0.1);
constexpr double RectR = 4.0;
// 背景の色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
Window::Resize(Size{ 745, 575 });
Audio audio;
// 各選択要素
size_t selectedTag = 0;
size_t selectedInstrument = 0;
Key selectedKey = { true, 0 }; // { 白鍵かどうか, キー番号(白鍵:0-6, 黒鍵:0-4)}
size_t selectedOctave = 5;
double noteOn = 0.4;
double noteOff = 0.0;
double volume = 0.5;
// プルダウンメニュー
Pulldown tagPulldown{ InstrumentTagNames, font, static_cast<double>(font.fontSize()), Vec2{80, 23}};
Pulldown instrumentPulldown{ makeInstrumentView(0), font, static_cast<double>(font.fontSize()), Vec2{80, 63}};
instrumentPulldown.setRectWidth(tagPulldown.getRect().w);
// UIテキストの描画
const auto drawUIText = [](const DrawableText & text, const Vec2 pos) {
const RoundRect uiConcaveRect{ text.region().stretched(3).movedBy(pos), RectR };
drawRoundRectBack(uiConcaveRect, BaseUIConcaveCol1, true);
drawRoundRectWithGradation(uiConcaveRect, BaseUIConcaveCol1, BaseUIConcaveCol2);
text.draw(pos, Palette::Black);
};
// 鍵盤の長方形を計算
const auto makeKeyboardRect = [&](const bool isWhite, const int32 keyIdx) {
const Vec2 drawBasePos{ 360, 175 };
if (isWhite) {
return RectF{ drawBasePos.movedBy(keyIdx * WhiteKeySize.x, 0), WhiteKeySize };
}
else {
Vec2 drawPos = drawBasePos.movedBy(BlackKeyMap[keyIdx].first * WhiteKeySize.x - BlackKeySize.x / 2, 0);
if (BlackKeyMap[keyIdx].second == 0) {
drawPos.x -= BlackKeySize.x / 4;
}
else if (BlackKeyMap[keyIdx].second == 1) {
drawPos.x += BlackKeySize.x / 4;
}
return RectF{ drawPos, BlackKeySize };
}
};
// オクターブと選択中の鍵盤からキーの番号を計算
const auto calcKeyNum = [&]() {
// 鍵盤の番号
const int32 key = selectedKey.first ? WhiteKeyMap[selectedKey.second] : WhiteKeyMap[BlackKeyMap[selectedKey.second].first] - 1;
return Clamp(selectedOctave * 12 + key, 0ull, 127ull);
};
// 再生できる条件を計算
const auto isValidAudio = [&]() {
return (noteOn > 1e-9 || noteOff > 1e-9);
};
while (System::Update())
{
// 楽器種類の背景・文字の描画
{
const RoundRect uiRect{ Vec2{ 10, 10 }, SizeF{ 320, 100 }, RectR };
const double h = instrumentPulldown.getRect().h / 2;
drawRoundRectBack(uiRect, BaseUICol1);
drawRoundRectWithGradation(uiRect, BaseUICol1, BaseUICol2);
drawUIText(font(U"種類"), Vec2{ 30, 25 });
drawUIText(font(U"楽器"), Vec2{ 30, 65 });
}
// ノートオン・オフの背景の描画
{
const RoundRect uiRect{ Vec2{ 340, 10 }, SizeF{ 390, 100 }, RectR };
drawRoundRectBack(uiRect, BaseUICol1);
drawRoundRectWithGradation(uiRect, BaseUICol1, BaseUICol2);
drawUIText(font(U"ノートオン"), Vec2{ 360, 25 });
drawUIText(font(U"ノートオフ"), Vec2{ 360, 65 });
}
// キー選択の背景・文字の描画
{
const RoundRect uiRect{ Vec2{340, 120}, SizeF{390, 370}, RectR };
drawRoundRectBack(uiRect, BaseUICol1);
drawRoundRectWithGradation(uiRect, BaseUICol1, BaseUICol2);
drawUIText(font(U"キー"), Vec2{ 360, 135 });
drawUIText(font(U"オクターブ"), Vec2{ 360, 365 });
}
// 再生ボタンの背景の描画
{
const RoundRect uiRect{ Vec2{340, 500}, SizeF{390, 60}, RectR };
drawRoundRectBack(uiRect, BaseUICol1);
drawRoundRectWithGradation(uiRect, BaseUICol1, BaseUICol2);
}
// 音量の背景の描画
{
const RoundRect uiRect{ Vec2{10, 120}, SizeF{ 320, 60 }, RectR };
drawRoundRectBack(uiRect, BaseUICol1);
drawRoundRectWithGradation(uiRect, BaseUICol1, BaseUICol2);
drawUIText(font(U"音量"), Vec2{ 30, 135 });
}
// 操作説明
{
const RoundRect uiRect{ Vec2{10, 190}, SizeF{320, 370}, RectR };
drawRoundRectBack(uiRect, BaseUICol1);
drawRoundRectWithGradation(uiRect, BaseUICol1, BaseUICol2);
drawRoundRectBack(uiRect.stretched(-10, -10), BaseUIConcaveCol1, true);
drawRoundRectWithGradation(uiRect.stretched(-10, -10), BaseUIConcaveCol1, BaseUIConcaveCol2);
font(U"キーボード操作").draw(Vec2{ 30, 205 }, Palette::Black);
font(U"↑↓").draw(Vec2{ 30, 235 }, Palette::Black);
font(U"楽器選択").draw(Vec2{ 60, 260 }, Palette::Black);
font(U"←→(Shift)").draw(Vec2{ 30, 300 }, Palette::Black);
font(U"ノートオン(ノートオフ)").draw(Vec2{ 60, 325 }, Palette::Black);
font(U"AWSEDFTGYHUJ").draw(Vec2{ 30, 365 }, Palette::Black);
font(U"対応するキーの選択").draw(Vec2{ 60, 390 }, Palette::Black);
font(U"-0123456789").draw(Vec2{ 30, 430 }, Palette::Black);
font(U"対応するオクターブの選択").draw(Vec2{ 60, 455 }, Palette::Black);
font(U"Space").draw(Vec2{ 30, 495 }, Palette::Black);
font(U"再生").draw(Vec2{ 60, 520 }, Palette::Black);
}
// 楽器の選択
if (tagPulldown.update()
&& tagPulldown.getIndex() != selectedTag)
{
selectedTag = tagPulldown.getIndex();
instrumentPulldown.setItems(makeInstrumentView(selectedTag));
}
// タグ
if (instrumentPulldown.update()) {
selectedInstrument = instrumentPulldown.getIndex();
}
// 音量スライダー
SimpleGUI::Slider(U"{:.2f}"_fmt(volume), volume, 0.0, 1.5, Vec2{ 85, 130 }, 50.0, 180.0);
// ノートオフ・オンの設定
SimpleGUI::Slider(U"{:.2f}"_fmt(noteOn), noteOn, 0.0, 4.0, Vec2{465, 20}, 50.0, 200.0);
SimpleGUI::Slider(U"{:.2f}"_fmt(noteOff), noteOff, 0.0, 4.0, Vec2{465, 60}, 50.0, 200.0);
// ピアノの鍵盤のUI、キー選択
// 白鍵描画
for (int32 i = 0; i < 7; ++i) {
makeKeyboardRect(true, i).draw(Palette::White).drawFrame(2.0, Palette::Black);
}
if (selectedKey.first) {
makeKeyboardRect(true, selectedKey.second).draw(SelectedColor).drawFrame(2.0, SelectedFrameColor);
}
// 黒鍵描画
for (int32 i = 0; i < 5; ++i) {
makeKeyboardRect(false, i).draw(Palette::Black).drawFrame(2.0, Palette::Black);
}
if (not selectedKey.first) {
makeKeyboardRect(false, selectedKey.second).draw(SelectedColor).drawFrame(2.0, SelectedFrameColor);
}
// 黒鍵の方から処理
bool isMouseOverBlackKey = false;
for (int32 i = 0; i < 5; ++i) {
const RectF blackKey = makeKeyboardRect(false, i);
if (blackKey.mouseOver()) {
isMouseOverBlackKey = true;
if (MouseL.down()) {
selectedKey.first = false;
selectedKey.second = i;
}
}
}
// 白鍵
if (not isMouseOverBlackKey) {
for (int32 i = 0; i < 7; ++i) {
const RectF whiteKey = makeKeyboardRect(true, i);
if (whiteKey.leftClicked()) {
selectedKey.first = true;
selectedKey.second = i;
}
}
}
// オクターブ
RectF{ Vec2{360, 410}, SizeF{352, 60} }.draw(Palette::White);
SimpleGUI::HorizontalRadioButtons(selectedOctave, Array<String>{U"", U"", U"", U"", U"", U"", U"", U"", U"", U"", U""}, Vec2{360, 410}, 32);
for (int32 i = 0; i < 11; ++i) {
font(-1 + i).draw(Arg::center = Vec2{ 378 + i * 32, 450 }, Palette::Black);
// Circle{ Vec2{ 360 + i * 40, 420 }, 10 }.draw(Palette::White).drawFrame(2, CircleColor);
}
// 音の生成
if (SimpleGUI::Button(U"再生", Vec2{ 360, 510 }, unspecified, isValidAudio())
|| (KeySpace.down() && isValidAudio()))
{
const size_t keyNum = calcKeyNum();
audio = Audio{ static_cast<GMInstrument>(selectedTag * 8 + selectedInstrument), static_cast<PianoKey>(keyNum), SecondsF{noteOn}, SecondsF{noteOff} };
audio.playOneShot(volume);
}
// クリップボードにコピー
if (SimpleGUI::Button(U"クリップボードにコピー", Vec2{ 450, 510 }, unspecified, isValidAudio())) {
const size_t keyNum = calcKeyNum();
const String str = U"{{ GMInstrument::{}, PianoKey::{}{}, {:.2f}s, {:.2f}s }}"_fmt(
InstrumentNames[selectedTag * 8 + selectedInstrument],
KeyNameMap[keyNum % 12],
selectedOctave == 0 ? U"_1" : Format(selectedOctave - 1),
noteOn,
noteOff
);
Clipboard::SetText(str);
}
// その他のキー入力による操作
// 楽器選択
if (KeyUp.down()) {
if (selectedInstrument == 0) {
selectedTag = (selectedTag + 15) % 16;
selectedInstrument = 7;
tagPulldown.setIndex(selectedTag);
instrumentPulldown.setIndex(selectedInstrument);
instrumentPulldown.setItems(makeInstrumentView(selectedTag));
}
else {
--selectedInstrument;
instrumentPulldown.setIndex(selectedInstrument);
}
}
if (KeyDown.down()) {
if (selectedInstrument == 7) {
selectedTag = (selectedTag + 1) % 16;
selectedInstrument = 0;
tagPulldown.setIndex(selectedTag);
instrumentPulldown.setIndex(selectedInstrument);
instrumentPulldown.setItems(makeInstrumentView(selectedTag));
}
else {
++selectedInstrument;
instrumentPulldown.setIndex(selectedInstrument);
}
}
// ノートオン・ノートオフ
if (KeyLeft.pressed()) {
if (KeyShift.pressed()) {
noteOff = Max(noteOff - 0.5 * Scene::DeltaTime(), 0.0);
}
else {
noteOn = Max(noteOn - 0.5 * Scene::DeltaTime(), 0.0);
}
}
if (KeyRight.pressed()) {
if (KeyShift.pressed()) {
noteOff = Min(noteOff + 0.5 * Scene::DeltaTime(), 4.0);
}
else {
noteOn = Min(noteOn + 0.5 * Scene::DeltaTime(), 4.0);
}
}
// キー
for (auto const& input : KeyBoardInputMap) {
if (input.first.down()) {
selectedKey = input.second;
}
}
// オクターブ
for (auto const& input : OctaveInputMap) {
if (input.first.down()) {
selectedOctave = input.second;
}
}
instrumentPulldown.draw();
tagPulldown.draw();
}
}
雑感
とりあえず色々聞いてみたところ、ノートオンやノートオフを小さめに抑えると、簡単な衝突音やシステム音などに使えそうないい感じの音が出ました。
即席で小さいゲームを作る時の効果音の選択肢になりそうです。
