役演亭 -Yakuentei- Roleplay with your own characters.
  1. ホーム
  2. 技術記事
  3. 信号処理

【Unity 実例有】ゼロから音楽・音色・効果音を合成して演奏

公開日 (Published) : 更新日 (Modified) :

音声ファイルを一切使わずソフトウェア・シンセサイザーを実現する手法の説明です。

【Unity 実例有】ソフトウェア音源 - 音楽・効果音をゼロから合成

入力音声ファイル (mp3, ogg 等) を一切使わずに、プログラミングのみで、音色 (波形) も含めて音楽・効果音をゼロから合成するソフトウェア音源 (シンセサイザー) を独力で実装するテクニックを説明していきます。

目次

まえがき (対象読者など)

本記事は、以下の 2 つの記事の応用編です。

これらの記事で述べられている内容を応用して、実際に波形メモリベースのソフトウェア音源 (シンセサイザー) を実装する方法と、その原理を説明していきます。

本記事の内容を理解することで、読者は

  • ライブラリ等に頼らずに「音楽」を演奏すること。
  • 上記を実現するための、独自の「音色」を自作すること。
  • 音楽だけでなく、独自の「効果音」も自作すること。

ができるようになります。

もちろん、本記事の内容がすべてではありません。サウンドプログラミングの世界には、さらに高度なテクニックが存在します。

本記事の内容が、より高度なテクニックを (Web 上の情報や書籍などを頼りながら) 自力で身に付けていくための基礎知識になるはずです。

事前準備 - Unity プロジェクト環境

別記事「Unity でプログラミングのみ (C# Script のみ) でゲームを作る方法」の C# Script の作成と Main Camera への割当まで済ませることで、本記事の実装例 (C# Script) を Unity で実行できる状態になります。

必要な「前提知識」について

まえがきでも述べたとおり、以下の記事の内容を理解している前提で説明していきます。

本記事で使用する C# Script

理論面や処理の解説は後回しにして、実際に動かして試すことのできるコードを提示します。

以下のスクリプトを事前準備で用意した C# Script に貼り付けることで、実行して「ド、レ、ミ、ファ、ソ、ラ、シ、ド」が順番に鳴るのを確かめられます。

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    // 音色情報を格納する構造体
    struct Tone
    {
        public float[] Wave; // 音色の 1 周期分の波形
        public float VolP; // ADSR の A-D 間 (=ピーク) の音量
        public float VolS; // ADSR の S の音量
        public int LenA; // ADSR の A までの長さ (サンプル数)
        public int LenD; // ADSR の D までの長さ (サンプル数)
        public int LenS; // ADSR の S までの長さ (サンプル数)
        public int LenR; // ADSR の R までの長さ (サンプル数)
    }

    // 定数定義
    const int SampRate = 48000; // DAT と同じサンプリングレート
    const int WaveBGMLen = SampRate * 8; // 8 秒
    float[] _waveBGM = new float[WaveBGMLen];

    void Start()
    {
        // ↓↓↓ 音色定義 ここから ↓↓↓
        Tone tone = new Tone
        {
            Wave = new float[] { 1, -1, -1, -1 }, // 矩形波 (1:3)
            VolP = 1,
            VolS = 0.5f,
            LenA = (int)(SampRate * 0.05),
            LenD = (int)(SampRate * 0.1),
            LenS = (int)(SampRate * 0.2),
            LenR = (int)(SampRate * 0.5),
        };
        // ↑↑↑ 音色定義 ここまで ↑↑↑

        // ↓↓↓ 楽譜定義 ここから ↓↓↓
        SetTone(tone, (int)(SampRate * 0.0), (int)(SampRate * 0.5), -9);
        SetTone(tone, (int)(SampRate * 0.5), (int)(SampRate * 0.5), -7);
        SetTone(tone, (int)(SampRate * 1.0), (int)(SampRate * 0.5), -5);
        SetTone(tone, (int)(SampRate * 1.5), (int)(SampRate * 0.5), -4);
        SetTone(tone, (int)(SampRate * 2.0), (int)(SampRate * 0.5), -2);
        SetTone(tone, (int)(SampRate * 2.5), (int)(SampRate * 0.5), 0);
        SetTone(tone, (int)(SampRate * 3.0), (int)(SampRate * 0.5), 2);
        SetTone(tone, (int)(SampRate * 3.5), (int)(SampRate * 0.5), 3);
        // ↑↑↑ 楽譜定義 ここまで ↑↑↑

        AudioClip clipBGM = AudioClip.Create(
            "clipBGM", WaveBGMLen, 1, SampRate, false);
        clipBGM.SetData(_waveBGM, 0);

        AudioSource audioSrc =
            new GameObject("音楽再生用").AddComponent<AudioSource>();
        audioSrc.clip = clipBGM;
        audioSrc.Play();
    }

    void SetTone(Tone tone, int start, int len, int key)
    {
        // 音色の周波数を計算 (注:本計算式は音色波形が 1 周期だと仮定)
        double originalToneHz = (double)SampRate / tone.Wave.Length;

        // 指定されたキー番号の音階の周波数を計算 (平均律)
        double targetToneHz = 440 * System.Math.Pow(2, key / 12.0);

        // 鳴らしたい音階の周波数と、音色の周波数の比から、再生速度を計算
        double tonePlaySpeed = targetToneHz / originalToneHz;

        // ADSR を計算しつつ DDS/NCO 方式で音声波形を発振・合成
        for (int offset = 0; offset < len; offset++)
        {
            // ADSR エンベロープを計算
            float adsr = 0;
            if (offset < tone.LenA)
            {
                // A-D 間の直線の式 (一次関数) を計算
                adsr = tone.VolP * offset / tone.LenA;
            }
            else if (offset < tone.LenD)
            {
                // D-S 間の直線の式 (一次関数) を計算
                float diffVol = tone.VolS - tone.VolP;
                int diffOffs = offset - tone.LenA;
                int diffLen = tone.LenD - tone.LenA;
                adsr = tone.VolP + diffVol * diffOffs / diffLen;
            }
            else if (offset < tone.LenS)
            {
                // S は音量一定
                adsr = tone.VolS;
            }
            else if (offset < tone.LenR)
            {
                // S-R 間の直線の式 (一次関数) を計算
                int diffOffs = offset - tone.LenS;
                int diffLen = tone.LenR - tone.LenS;
                adsr = tone.VolS - tone.VolS * diffOffs / diffLen;
            }

            // 音色波形の周期内のサンプル位置 (位相) を計算
            int sampInTone =
                (int)(offset * tonePlaySpeed) % tone.Wave.Length;

            // 2 和音以上にも対応できるよう「足し算」で音色を合成
            _waveBGM[start + offset] += adsr * tone.Wave[sampInTone];
        }
    }
}

音色波形の重要性と、自作のためのテクニック

音楽・効果音の世界で、「音そのものの聞こえ方」を最も特徴づけるのが音色の波形です。波形の形状が、音の第一印象を決めると言っても過言ではありません。

なので、まずは「波形」の説明からしていきます。

本記事のソースコードでは、Tone 構造体のメンバー変数 Wave で、音色波形をカスタマイズできるようになっています。

// ↓↓↓ 音色定義 ここから ↓↓↓
Tone tone = new Tone
{
    Wave = new float[] { 1, -1, -1, -1 }, // 矩形波 (1:3)
    VolP = 1,
    VolS = 0.5f,
    LenA = (int)(SampRate * 0.05),
    LenD = (int)(SampRate * 0.1),
    LenS = (int)(SampRate * 0.2),
    LenR = (int)(SampRate * 0.5),
};
// ↑↑↑ 音色定義 ここまで ↑↑↑

応用元の記事

では、実際のピアノの音 (波形) が録音された音声ファイル (wav) を使っていましたが、本記事では「独自に定義した波形データ」に置き換え、かつその波形パターンをループ再生する仕組みにしています。

現在、メンバー変数 Wave に入れている波形は、「Unity の C# Script で「無」から音を産み出す方法」で紹介した「矩形波」のデューティー比を 1:1 から 1:3 に変更したものです。

ファミコン音源が対応している矩形波

同じ矩形波でも、+1 と -1 の割合である「デューティー比」を変えるだけで、だいぶ音の鳴り方が違って聞こえるようになります。

Wave = new float[] { 1, -1 }, // 矩形波 (1:1)
Wave = new float[] { 1, -1, -1, -1 }, // 矩形波 (1:3)
Wave = new float[] { 1, -1, -1, -1, -1, -1, -1, -1 }, // 矩形波 (1:7)

上記の 3 種類は、実際にファミコン音源でも採用されているデューティー比です。

変数 Wave に入れるものを変えて「聴き比べ」してみると、その違いがよく分かると思います。

矩形波以外の波形について

もちろん、矩形波以外にも波形は存在します。

もう 1 つの例として、同じくファミコン音源で採用されている三角波を紹介します。

ファミコン音源が対応している三角波

厳密には、三角波はもっと滑らかな三角形の形状をしているのですが、あえてファミコン音源と同じ、粗い分解能 (32 サンプル) の「階段型」の三角波にしています。

(注:Unity では振幅を ±1 以内に収める必要があるため、7 で割っています)

Wave = new float[]
{
    0, +1/7f, +2/7f, +3/7f, +4/7f, +5/7f, +6/7f, +7/7f,
    +7/7f, +6/7f, +5/7f, +4/7f, +3/7f, +2/7f, +1/7f, 0,
    0, -1/7f, -2/7f, -3/7f, -4/7f, -5/7f, -6/7f, -7/7f,
    -7/7f, -6/7f, -5/7f, -4/7f, -3/7f, -2/7f, -1/7f, 0,
}, // 三角波 (32 サンプル)

波形メモリの分解能も、音の印象を大きく左右する要素の 1 つで、

  • 分解能が粗いほど (サンプル数:小) レトロなメカっぽい音になる
  • 分解能が細かいほど (サンプル数:大) リアルで滑らかな音になる

という特徴があります。

実際に、波形メモリの分解能を 10 サンプルに削った「粗い」三角波と聞き比べてみましょう。

Wave = new float[]
{
    0, 0.5f, 1, 0.5f, 0, 0, -0.5f, -1, -0.5f, 0
}, // 粗い三角波 (10 サンプル)

ゲーム用の音楽・効果音では、あえて分解能を落として「いかにもメカっぽい音にする」のも効果的なので、あえてサンプル数の少ないオリジナルの形状を定義する手法も非常に有効です。

「加算合成」(Additive Synthesis) 法による音色作成

次に、シンセサイザーおよび音声信号処理を語るうえでは欠かせないテクニックである「加算合成」(Additive Synthesis) 法を説明します。

実際に FM 音源にも搭載されている機能の 1 つで、各オペレーターのキャリア (Carrier) のみを有効にして、モジュレーター (Modulator) 側を切ることで、実機でも「加算合成」を実現できます。

(モジュレーターを切った時点でそもそも FM 音源じゃないじゃん…というのは無しで…)

加算合成 (Additive Synthesis) 前の各正弦波の例 (基本波、2 倍音、5 倍音、7 倍音)

加算合成 (Additive Synthesis) 法とは、上記のような、周波数が異なる正弦波 (Sin 波、Cos 波) を、互いに異なる振幅バランスで足し合わせて音を作る方法です。

例として、上記のように、32 サンプルの分解能を持つ 4 種類の Sin 波 (互いに振幅の異なる基本波と 2、5、7 倍音) を足し算すると、以下のような波形になります。

加算合成 (Additive Synthesis) 後の波形の例

実際に本記事の C# Script で動くようにしたものが、以下のコードになります。
(差分のみ強調表示しています)

// ↓↓↓ 音色定義 ここから ↓↓↓
Tone tone = new Tone
{
    Wave = new float[32],
    VolP = 1,
    VolS = 0.5f,
    LenA = (int)(SampRate * 0.05),
    LenD = (int)(SampRate * 0.1),
    LenS = (int)(SampRate * 0.2),
    LenR = (int)(SampRate * 0.5),
};
float basePhasePerSample = 2 * Mathf.PI / tone.Wave.Length;
for (int sample = 0; sample < tone.Wave.Length; sample++)
{
    // 加算合成 (Additive Synthesis) 方式で音色を生成
    // (例として、基本波に加えて、2 倍、5 倍、7 倍音を合成)
    tone.Wave[sample] =
        0.4f * Mathf.Sin(basePhasePerSample * sample * 1) +
        0.3f * Mathf.Sin(basePhasePerSample * sample * 2) +
        0.2f * Mathf.Sin(basePhasePerSample * sample * 5) +
        0.1f * Mathf.Sin(basePhasePerSample * sample * 7);
}
// ↑↑↑ 音色定義 ここまで ↑↑↑

矩形波や三角波のような、単純な波形では得られない複雑な音を作れるという特徴があり、「離散フーリエ変換」として知られている性質上、世の中のあらゆる音をデジタル的に作り出すことができる万能手法です。

  • 「離散フーリエ変換」とは、簡単に言うと、世の中にあるすべてのデジタル波形は、正弦波 (Sin, Cos) の足し合わせだけで作ることができるというデジタル信号処理の理論です。
  • 詳細は、数学的に難しい内容となるため、いずれ別記事で解説する予定です。

倍音の組み合わせ方の例と、実際に出来上がる音の傾向

傾向としては、高倍率の倍音 (例えば 11 倍とか 13 倍とか) を、高倍率の振幅で含んでいると、金属音っぽい響きになります。

tone.Wave[sample] =
    0.1f * Mathf.Sin(basePhasePerSample * sample * 1) +
    0.1f * Mathf.Sin(basePhasePerSample * sample * 5) +
    0.2f * Mathf.Sin(basePhasePerSample * sample * 7) +
    0.2f * Mathf.Sin(basePhasePerSample * sample * 11) +
    0.3f * Mathf.Sin(basePhasePerSample * sample * 13);

逆に、低倍率の倍音 (2 倍、3 倍あたり) のみで作ると、エレクトリックピアノやビブラフォンのような滑らかな音になります。

tone.Wave[sample] =
    0.5f * Mathf.Sin(basePhasePerSample * sample * 1) +
    0.3f * Mathf.Sin(basePhasePerSample * sample * 3);

なお、さらに高倍率の倍音を試してみたい場合は、分解能が 32 サンプルだと足りないため、例えば 64, 128 などに増やしてみて試すのも有りです。

音色・効果音の自作時に欠かせないエンベロープ (包絡線) と ADSR

最初の記事「Unity の C# Script で「無」から音を産み出す方法」でも簡単に触れましたが、音色や効果音の作成時に、もう 1 つ欠かせない要素が、音の大きさ (=波形の振幅) を時間とともに変化させる「エンベロープ」 (日本語で包絡線) です。

特にオーディオの世界では、以下に示す ADSR エンベロープが一般的に普及しています。
(DTM/DAW 経験者の方ならご存じの方も多いと思います)

以下は、矩形波 (1:1) に対して、ADSR エンベロープを適用した例です。

ADSR エンベロープの例

  • A (Attack) : アタック (立ち上がり)
    • 音を鳴らし始めたときの「立ち上がり」までにかかる時間です。
    • 短ければピアノやギター風、長ければバイオリン風になります。
  • D (Decay) : ディケイ (減衰)
    • ピークに達した後、一定レベルに減衰するまでにかかる時間です。
    • アタックの立ち上がりを強調して楽器っぽくするために調整します。
  • S (Sustain) : サステイン (継続)
    • 管楽器やバイオリンのように鳴り続ける楽器の場合に長くします。
  • R (Release) : リリース (解放)
    • ピアノの鍵盤を離した後や、ギターの弦を弾いた後の減衰時間です。
    • 短ければギター風、長ければピアノ風の印象になります。

実際に、普通の矩形波 (デューティー比 1:1) をベースに、いくつかの ADSR を試してみましょう。

// ↓↓↓ 音色定義 ここから ↓↓↓
Tone tone = new Tone
{
    Wave = new float[] { 1, -1 }, // 矩形波 (1:1)
    VolP = 1,
    VolS = 0.5f,
    LenA = (int)(SampRate * 0.05),
    LenD = (int)(SampRate * 0.1),
    LenS = (int)(SampRate * 0.2),
    LenR = (int)(SampRate * 0.5),
};
// ↑↑↑ 音色定義 ここまで ↑↑↑

変数 VolP, VolS, LenA, LenD, LenS, LenR の意味は、次のとおりです。
(これ以外にもいろいろな設定方法が考えられますので、本ソースコードにおける ADSR の設定方法は、あくまでも一例だと思って下さい。)

本記事のソースコードにおける ADSR 設定 (変数 VolP, VolS, LenA, LenD, LenS, LenR) と、実際のエンベロープ形状との対応図

例えば、以下のように A および D までの時間を短めにして、S の音量を下げると、同じ「矩形波」でも、リバーブをかけつつ、ギターを強く弾いたときのような鳴り方に変わります。

// ↓↓↓ 音色定義 ここから ↓↓↓
Tone tone = new Tone
{
    Wave = new float[] { 1, -1 }, // 矩形波 (1:1)
    VolP = 1,
    VolS = 0.2f,
    LenA = (int)(SampRate * 0.0),
    LenD = (int)(SampRate * 0.05),
    LenS = (int)(SampRate * 0.2),
    LenR = (int)(SampRate * 0.5),
};
// ↑↑↑ 音色定義 ここまで ↑↑↑

さらに、上記の S および R までの時間を短くすれば、リバーブがかからなくなり、スタッカートで演奏したときのような感じになります。

// ↓↓↓ 音色定義 ここから ↓↓↓
Tone tone = new Tone
{
    Wave = new float[] { 1, -1 }, // 矩形波 (1:1)
    VolP = 1,
    VolS = 0.2f,
    LenA = (int)(SampRate * 0.0),
    LenD = (int)(SampRate * 0.05),
    LenS = (int)(SampRate * 0.1),
    LenR = (int)(SampRate * 0.2),
};
// ↑↑↑ 音色定義 ここまで ↑↑↑

他にも、A と R の時間を長めにとると、何とも言えない曖昧な感じの鳴り方になります。

// ↓↓↓ 音色定義 ここから ↓↓↓
Tone tone = new Tone
{
    Wave = new float[] { 1, -1 }, // 矩形波 (1:1)
    VolP = 1,
    VolS = 0.5f,
    LenA = (int)(SampRate * 0.2),
    LenD = (int)(SampRate * 0.25),
    LenS = (int)(SampRate * 0.3),
    LenR = (int)(SampRate * 0.5),
};
// ↑↑↑ 音色定義 ここまで ↑↑↑

特に ADSR は、BGM 用の音色だけでなく、ゲーム等の効果音を自作する場合にも非常に役に立つので、様々な音色の波形で、様々な ADSR を試してみると表現の幅が広がるでしょう。

実際に ADSR の制御を行っている箇所のソースコードについて

ADSR の制御は、SetTone() 関数の中で、A, D, S, R の各領域に分けて計算しています。

// A-D 間の直線の式 (一次関数) を計算
adsr = tone.VolP * offset / tone.LenA;
// D-S 間の直線の式 (一次関数) を計算
float diffVol = tone.VolS - tone.VolP;
int diffOffs = offset - tone.LenA;
int diffLen = tone.LenD - tone.LenA;
adsr = tone.VolP + diffVol * diffOffs / diffLen;
// S は音量一定
adsr = tone.VolS;
// S-R 間の直線の式 (一次関数) を計算
int diffOffs = offset - tone.LenS;
int diffLen = tone.LenR - tone.LenS;
adsr = tone.VolS - tone.VolS * diffOffs / diffLen;

個々の行のコードについての説明は割愛しますが、それほど複雑ではない「直線の式」(一次関数) で ADSR を計算しているため、本記事に掲載した図と見比べながらコードを読んでみる (必要に応じて Debug.Log() 等でダンプしてみる) と、おそらく理解できるのではないかと思います。

ポイントは、S 以外の領域はすべて斜めの線であることと、A は常に増加 (斜め上)、D, R は常に減少 (斜め下) の直線 であることです。

効果音、パーカッション等で多用されるランダムノイズ波形

さて、再び「波形」の話に戻ります。

効果音、パーカッション (ドラムスパート) などで多用される「ランダムノイズ」という特殊な波形が存在します。

例えば、ファミコン音源で使われているランダムノイズ波形の 1 つは、1 周期あたり 32767 サンプルという非常に長いものです。

以下の例では、Unity の乱数生成 APIをもちいてノイズを生成 (+1 または -1 をランダムに生成) していますが、ファミコン音源では 15-bit の LFSR (ローテートシフトを行うシフトレジスタ) と XOR 演算をもちいた 「M 系列」と呼ばれる乱数生成アルゴリズム (別名:PN15)をもちいて同様の波形 (+1 または -1 の乱数) を生成しています。

// ↓↓↓ 音色定義 ここから ↓↓↓
Tone noise = new Tone
{
    Wave = new float[32767],
    VolP = 1,
    VolS = 0.5f,
    LenA = (int)(SampRate * 0.05),
    LenD = (int)(SampRate * 0.1),
    LenS = (int)(SampRate * 0.15),
    LenR = (int)(SampRate * 0.75),
};
for (int sample = 0; sample < noise.Wave.Length; sample++)
{
    // ノイズ波形 (=ランダム波形) を生成
    noise.Wave[sample] = Random.Range(0, 2) * 2 - 1;
}
// ↑↑↑ 音色定義 ここまで ↑↑↑

長周期かつランダムな波形であるため、音階指定のキー番号も「非常に低い値」にしなければ、音の高さが変わっているように聞こえないという特徴があります。

そのため、楽譜定義側も、例えば以下のように変える必要があります。

// ↓↓↓ 楽譜定義 ここから ↓↓↓
SetTone(noise, (int)(SampRate * 0.0), (int)(SampRate * 0.5), -168);
SetTone(noise, (int)(SampRate * 0.5), (int)(SampRate * 0.5), -162);
SetTone(noise, (int)(SampRate * 1.0), (int)(SampRate * 0.5), -156);
SetTone(noise, (int)(SampRate * 1.5), (int)(SampRate * 0.5), -150);
SetTone(noise, (int)(SampRate * 2.0), (int)(SampRate * 0.5), -144);
SetTone(noise, (int)(SampRate * 2.5), (int)(SampRate * 0.5), -138);
SetTone(noise, (int)(SampRate * 3.0), (int)(SampRate * 0.5), -132);
SetTone(noise, (int)(SampRate * 3.5), (int)(SampRate * 0.5), -126);
// ↑↑↑ 楽譜定義 ここまで ↑↑↑

上記を実際に鳴らしてみると、楽器の音とは完全に別物の、ホワイトノイズのような音に聞こえるかと思います。
(若干、昔のドラクエの「いてつくはどう」の効果音を意識した感じにしてみました。)

この音も、例えば以下のように ADSR エンベロープを調整することで、ややレトロな感じのパーカッション音に変えることができます。

VolP = 0.5f,
VolS = 0.1f,
LenA = (int)(SampRate * 0.0),
LenD = (int)(SampRate * 0.05),
LenS = (int)(SampRate * 0.01),
LenR = (int)(SampRate * 0.5),

実際に以下のような楽譜定義に変えることで、クローズドハイハットを鳴らしつつ、バスドラムとスネアドラムを交互に叩いているような音を合成できます。

// ↓↓↓ 楽譜定義 ここから ↓↓↓
// クローズド・ハイハット
SetTone(noise, (int)(SampRate * 0.00), (int)(SampRate * 0.02), -120);
SetTone(noise, (int)(SampRate * 0.25), (int)(SampRate * 0.02), -120);
SetTone(noise, (int)(SampRate * 0.50), (int)(SampRate * 0.02), -120);
SetTone(noise, (int)(SampRate * 0.75), (int)(SampRate * 0.02), -120);
SetTone(noise, (int)(SampRate * 1.00), (int)(SampRate * 0.02), -120);
SetTone(noise, (int)(SampRate * 1.25), (int)(SampRate * 0.02), -120);
SetTone(noise, (int)(SampRate * 1.50), (int)(SampRate * 0.02), -120);
SetTone(noise, (int)(SampRate * 1.75), (int)(SampRate * 0.02), -120);
// バスドラム & スネアドラム
SetTone(noise, (int)(SampRate * 0.0), (int)(SampRate * 0.25), -168);
SetTone(noise, (int)(SampRate * 0.5), (int)(SampRate * 0.5), -144);
SetTone(noise, (int)(SampRate * 1.0), (int)(SampRate * 0.25), -168);
SetTone(noise, (int)(SampRate * 1.5), (int)(SampRate * 0.5), -144);
// ↑↑↑ 楽譜定義 ここまで ↑↑↑

応用例:複数の音色、複数パートで音楽を演奏

最後に、ここまで紹介してきたテクニックを組み合わせて、複数の音色、複数パートで音楽を演奏する例を紹介して、本記事を締めくくりとしたいと思います。

  • パート 1 : 4 オペレーター 加算合成音
  • パート 2 : 矩形波 (1:3)
  • パート 3 : 三角波
  • パート 4 : ランダムノイズ

全パート合成後の波形の振幅値が ±1 を超えないよう、各音色の ADSR のボリューム設定を小さめにしています。

音色定義 (4 パート分)

// ↓↓↓ 音色定義 ここから ↓↓↓
Tone tone1 = new Tone
{
    Wave = new float[32],
    VolP = 1,
    VolS = 0.5f,
    LenA = (int)(SampRate * 0.05),
    LenD = (int)(SampRate * 0.1),
    LenS = (int)(SampRate * 0.15),
    LenR = (int)(SampRate * 0.5),
};
float basePhasePerSample = 2 * Mathf.PI / tone1.Wave.Length;
for (int sample = 0; sample < tone1.Wave.Length; sample++)
{
    tone1.Wave[sample] =
        0.4f * Mathf.Sin(basePhasePerSample * sample * 1) +
        0.3f * Mathf.Sin(basePhasePerSample * sample * 2) +
        0.2f * Mathf.Sin(basePhasePerSample * sample * 5) +
        0.1f * Mathf.Sin(basePhasePerSample * sample * 7);
}

Tone tone2 = new Tone
{
    Wave = new float[] { 1, -1, -1, -1, -1, -1, -1, -1 },
    VolP = 0.3f,
    VolS = 0.15f,
    LenA = (int)(SampRate * 0.0),
    LenD = (int)(SampRate * 0.05),
    LenS = (int)(SampRate * 0.1),
    LenR = (int)(SampRate * 0.25),
};

Tone tone3 = new Tone
{
    Wave = new float[]
    {
        0, +1/7f, +2/7f, +3/7f, +4/7f, +5/7f, +6/7f, +7/7f,
        +7/7f, +6/7f, +5/7f, +4/7f, +3/7f, +2/7f, +1/7f, 0,
        0, -1/7f, -2/7f, -3/7f, -4/7f, -5/7f, -6/7f, -7/7f,
        -7/7f, -6/7f, -5/7f, -4/7f, -3/7f, -2/7f, -1/7f, 0,
    },
    VolP = 0.3f,
    VolS = 0.3f,
    LenA = (int)(SampRate * 0.0),
    LenD = (int)(SampRate * 0.0),
    LenS = (int)(SampRate * 1.5),
    LenR = (int)(SampRate * 1.5),
};

Tone noise = new Tone
{
    Wave = new float[32767],
    VolP = 0.5f,
    VolS = 0.1f,
    LenA = (int)(SampRate * 0.0),
    LenD = (int)(SampRate * 0.05),
    LenS = (int)(SampRate * 0.01),
    LenR = (int)(SampRate * 0.5),
};
for (int sample = 0; sample < noise.Wave.Length; sample++)
{
    // ノイズ波形 (=ランダム波形) を生成
    noise.Wave[sample] = Random.Range(0, 2) * 2 - 1;
}
// ↑↑↑ 音色定義 ここまで ↑↑↑

楽譜定義 (4 パート分)

// ↓↓↓ 楽譜定義 ここから ↓↓↓
SetTone(tone1, (int)(SampRate * 0.0), (int)(SampRate * 0.5), -9);
SetTone(tone1, (int)(SampRate * 0.5), (int)(SampRate * 0.5), -5);
SetTone(tone1, (int)(SampRate * 1.0), (int)(SampRate * 0.5), -2);
SetTone(tone1, (int)(SampRate * 1.5), (int)(SampRate * 0.5), -5);
SetTone(tone1, (int)(SampRate * 2.0), (int)(SampRate * 0.5), -2);
SetTone(tone1, (int)(SampRate * 2.5), (int)(SampRate * 0.5), 2);
SetTone(tone1, (int)(SampRate * 3.0), (int)(SampRate * 0.5), -2);
SetTone(tone1, (int)(SampRate * 3.5), (int)(SampRate * 0.5), 2);
SetTone(tone1, (int)(SampRate * 4.0), (int)(SampRate * 0.5), 5);
SetTone(tone1, (int)(SampRate * 4.5), (int)(SampRate * 1.5), 3);

SetTone(tone2, (int)(SampRate * 0.00), (int)(SampRate * 0.25), -5);
SetTone(tone2, (int)(SampRate * 0.25), (int)(SampRate * 0.25), -2);
SetTone(tone2, (int)(SampRate * 0.50), (int)(SampRate * 0.25), -5);
SetTone(tone2, (int)(SampRate * 0.75), (int)(SampRate * 0.25), -2);
SetTone(tone2, (int)(SampRate * 1.00), (int)(SampRate * 0.25), -5);
SetTone(tone2, (int)(SampRate * 1.25), (int)(SampRate * 0.25), -2);
SetTone(tone2, (int)(SampRate * 1.50), (int)(SampRate * 0.25), -2);
SetTone(tone2, (int)(SampRate * 1.75), (int)(SampRate * 0.25), 2);
SetTone(tone2, (int)(SampRate * 2.00), (int)(SampRate * 0.25), -2);
SetTone(tone2, (int)(SampRate * 2.25), (int)(SampRate * 0.25), 2);
SetTone(tone2, (int)(SampRate * 2.50), (int)(SampRate * 0.25), -2);
SetTone(tone2, (int)(SampRate * 2.75), (int)(SampRate * 0.25), 2);
SetTone(tone2, (int)(SampRate * 3.00), (int)(SampRate * 0.25), 2);
SetTone(tone2, (int)(SampRate * 3.25), (int)(SampRate * 0.25), 5);
SetTone(tone2, (int)(SampRate * 3.50), (int)(SampRate * 0.25), 2);
SetTone(tone2, (int)(SampRate * 3.75), (int)(SampRate * 0.25), 5);
SetTone(tone2, (int)(SampRate * 4.00), (int)(SampRate * 0.25), 2);
SetTone(tone2, (int)(SampRate * 4.25), (int)(SampRate * 0.25), 5);
SetTone(tone2, (int)(SampRate * 4.5), (int)(SampRate * 1.5), -5);

SetTone(tone3, (int)(SampRate * 0.0), (int)(SampRate * 1.5), -21);
SetTone(tone3, (int)(SampRate * 1.5), (int)(SampRate * 1.5), -17);
SetTone(tone3, (int)(SampRate * 3.0), (int)(SampRate * 1.5), -14);
SetTone(tone3, (int)(SampRate * 4.5), (int)(SampRate * 0.5), -21);

SetTone(noise, (int)(SampRate * 0.0), (int)(SampRate * 0.5), -168);
SetTone(noise, (int)(SampRate * 0.5), (int)(SampRate * 0.5), -144);
SetTone(noise, (int)(SampRate * 1.0), (int)(SampRate * 0.5), -144);
SetTone(noise, (int)(SampRate * 1.5), (int)(SampRate * 0.5), -168);
SetTone(noise, (int)(SampRate * 2.0), (int)(SampRate * 0.5), -144);
SetTone(noise, (int)(SampRate * 2.5), (int)(SampRate * 0.5), -144);
SetTone(noise, (int)(SampRate * 3.0), (int)(SampRate * 0.5), -168);
SetTone(noise, (int)(SampRate * 3.5), (int)(SampRate * 0.5), -144);
SetTone(noise, (int)(SampRate * 4.0), (int)(SampRate * 0.5), -144);
SetTone(noise, (int)(SampRate * 4.5), (int)(SampRate * 1.5), -168);
// ↑↑↑ 楽譜定義 ここまで ↑↑↑

おわりに

3 記事に渡った内容となりましたが、ここまでの段階で、最低限「ソフトウェア・シンセサイザー」と呼べるものが実装できるだけの技術要素が揃いました。

しかし、「はじめに」にも書いたとおり、まだまだ高度なテクニックが存在することも事実です。

また、数学的な難解さを避けるために、三角関数そのものの説明や、離散フーリエ変換の話なども割愛したため、あまり「信号処理らしくない」内容であることも確かです。

それらの高度な話の内容も、いずれ本サイトの技術記事で拡充していく予定ですが、本記事の内容まで理解できれば「シンセサイザーの基礎」は押さえられていると思いますので、是非、他のより高度な内容に触れている Web サイトや書籍などの理解・実践に挑戦していただければと思います。

筆者「鈴木YE」が制作した効果音素材の紹介

本記事の筆者「鈴木YE」は、Unity、ツクール等で使えるレトロ効果音素材 (85 ファイル同梱) を、DLsite にて販売しています。

この効果音素材は、Unity で作ったものではありません (※ そもそも Unity が出るよりも前に作ったものです) が、実は、本記事までに紹介してきた手法の応用で十分作成が可能です。

役演亭 レトロゲーム効果音素材集「四面楚歌」

さらに言うと、体験版用に合成した BGM (不正利用防止用) も、本記事までの手法で実装したソフトウェア音源で演奏しています。

音楽や効果音を「自作」しようとすると、波形の組み合わせ方、重ね合わせ方などを試行錯誤する必要があり、時間がかかってしまうのが難点ですが、「オリジナルの音を自力で作成できる」こともゲーム作者としてのアドバンテージの 1 つになりますので、是非とも波形合成にチャレンジしていただけたらと思います。

主な更新履歴

  • 2023-04-29
    • 制作した効果音素材の紹介と絡めた説明を追加
  • 2023-03-31
    • 初版

「信号処理」一覧

【Unity 実例有】「ラ」の音だけで「猫ふんじゃった」を演奏
1 つの単音から、あらゆる音階の波形を合成して BGM を演奏する手法の説明です。
【Unity 実例有】ゼロから音楽・音色・効果音を合成して演奏
(現在開いているページです)