パラメトリック方程式でチャンスオペレーション

#はじめに
以前、[カオス(力学系)](https://ja.wikipedia.org/wiki/カオス理論)から[チャンスオペレーション(偶然性の音楽)](https://ja.wikipedia.org/wiki/偶然性の音楽)ができないか？という試みを[Siv3D](https://siv3d.github.io/ja-jp/)を使って行っていました。
その際はカオスの「一意性」「初期値鋭敏性」「有界性」を利用して偶然性の音楽を試してみたのですが、今回は三角関数を用いた[パラメトリック方程式](https://en.wikipedia.org/wiki/Parametric_equation)から周期性を利用しつつ、媒介変数(パラメータ)への微小変位を加えることで揺らぎを持たせることを試してみました。



#パラメトリック方程式での例
ここではWikipediaの[パラメトリック方程式](https://en.wikipedia.org/wiki/Parametric_equation)に記載のある式(j,k=3)を用いたサンプルをご紹介します。
<img width = 200 src = "https://wikimedia.org/api/rest_v1/media/math/render/svg/5d2a0d352a4cde4164998bdba6e393caeeb95647">
<img width = 200 src ="https://upload.wikimedia.org/wikipedia/commons/d/d3/Param_03.jpg">

動作例：https://twitter.com/i/status/1401353913371021313



#使い方
方程式の軌道はx-yの二次元で表現されますので、x軸に音階、z軸に音量を割り当てています。音階はコード理論を参考に並べるとイイ感じになります。カオスとは違い、2πの周期性を持つためパラメータが同じであれば同じフレーズを繰り返すことができる点になります。

そこで今回は、あるパラメータの組み合わせについて4回繰り返すことで4小節の単位を作成しています。4小節目が終わったタイミングでランダムでパラメータを変更し、ランダムで次のメロディが生成されます。

また、以下の試みを行っています。
・　2小節目と4小節目のパラメータに対して微小変位を加えることで摂動っぽさを与え、似たフレーズだけれども全く同じではないフレーズとなるモチーフ表現の試み
・　0.5小節単位で音階を転調することでコード進行が表現できるかの試み

結果としては、本来のコード進行ほど心地よさはないのですが、AメロBメロがずっと繰り返されるような印象が得られているのではないかと思います。一方で頭に残るフレーズが生み出せているかというと、まだ少し足りない印象です。



**参考**：
https://dn-voice.info/music-theory/godoken/
https://ja.wikipedia.org/wiki/Tonnetz
https://soundquest.jp/quest/melody/melody-mv1/motif-and-variations/
https://sakkyoku.info/beginner/popular-chord-progression-pattern/






#ソースコード
[OpenSiv3D(0.4.3)](https://siv3d.github.io/ja-jp/#opensiv3d-sdk-v043)を前提としています。素人コードなので冗長な部分があるかと思いますが、ご活用頂ければと思います。
こちらのサンプルでは、通称「小室進行」と呼ばれる Ⅵm→Ⅳ→Ⅴ→Ⅰ を繰り返す進行を採用しています。

```cpp:Main.cpp
# include <Siv3D.hpp>

//調一覧、#はS
enum KeyList {
    C, CS, D, DS, E, F, FS, G, GS, A, AS, B,
    Cm, CSm, Dm, DSm, Em, Fm, FSm, Gm, GSm, Am, ASm, Bm
};


//音データを作るためのクラス
class SoundSetting {

    //発音データ
    GMInstrument inst = GMInstrument::FifthSawWave; //FifthSawWaveはゲーム音楽感があっておすすめ
    SecondsF tpc = 0.5s; //発音継続時間
    Audio pC  = Audio(inst, PianoKey::C4, tpc);
    Audio pCS = Audio(inst, PianoKey::CS4, tpc);
    Audio pD  = Audio(inst, PianoKey::D4, tpc);
    Audio pDS = Audio(inst, PianoKey::DS4, tpc);
    Audio pE  = Audio(inst, PianoKey::E4, tpc);
    Audio pF  = Audio(inst, PianoKey::F4, tpc);
    Audio pFS = Audio(inst, PianoKey::FS4, tpc);
    Audio pG  = Audio(inst, PianoKey::G4, tpc);
    Audio pGS = Audio(inst, PianoKey::GS4, tpc);
    Audio pA  = Audio(inst, PianoKey::A4, tpc);
    Audio pAS = Audio(inst, PianoKey::AS4, tpc);
    Audio pB  = Audio(inst, PianoKey::B4, tpc);
    Array<Audio> key = { pC, pCS, pD, pDS, pE, pF, pFS, pG, pGS, pA, pAS, pB };
    Array<String> dispKey = { U"C. ", U"C#", U"D. ", U"D#", U"E. ", U"F. ", U"F#", U"G. ", U"G#", U"A. ", U"A#", U"B. " };

    Array<Audio> setKey(int s, bool major)
    {
        if (major)
        {
            return { key[(2 + s) % 12] , key[(11 + s) % 12], key[(7 + s) % 12] , key[(4 + s) % 12] , key[(0 + s) % 12] , key[(9 + s) % 12] , key[(5 + s) % 12] };
        }
        else
        {
            return { key[(11 + s) % 12] , key[(7 + s) % 12], key[(4 + s) % 12] , key[(0 + s) % 12] , key[(9 + s) % 12] , key[(5 + s) % 12] , key[(2 + s) % 12] };
        }
    };

    s3d::String setDispKey(int s, bool major)
    {
        if (major)
        {
            return dispKey[(2 + s) % 12] + U"  " + dispKey[(11 + s) % 12] + U"  " + dispKey[(7 + s) % 12] + U"  " + dispKey[(4 + s) % 12] + U"  " + dispKey[(0 + s) % 12] + U"  " + dispKey[(9 + s) % 12] + U"  " + dispKey[(5 + s) % 12];
        }
        else
        {
            return dispKey[(11 + s) % 12] + U"  " + dispKey[(7 + s) % 12] + U"  " + dispKey[(4 + s) % 12] + U"  " + dispKey[(0 + s) % 12] + U"  " + dispKey[(9 + s) % 12] + U"  " + dispKey[(5 + s) % 12] + U"  " + dispKey[(2 + s) % 12];
        }
    }

public:
    int beat = 200; //発音間隔[msec]
    int bar = beat * 16; //1小節(2π分)[msec]

    Array<Audio> CM = setKey(0, true);
    Array<Audio> CSM = setKey(1, true);
    Array<Audio> DM = setKey(2, true);
    Array<Audio> DSM = setKey(3, true);
    Array<Audio> EM = setKey(4, true);
    Array<Audio> FM = setKey(5, true);
    Array<Audio> FSM = setKey(6, true);
    Array<Audio> GM = setKey(7, true);
    Array<Audio> GSM = setKey(8, true);
    Array<Audio> AM = setKey(9, true);
    Array<Audio> ASM = setKey(10, true);
    Array<Audio> BM = setKey(11, true);

    Array<Audio> Cm = setKey(3, false);
    Array<Audio> CSm = setKey(4, false);
    Array<Audio> Dm = setKey(5, false);
    Array<Audio> DSm = setKey(6, false);
    Array<Audio> Em = setKey(7, false);
    Array<Audio> Fm = setKey(8, false);
    Array<Audio> FSm = setKey(9, false);
    Array<Audio> Gm = setKey(10, false);
    Array<Audio> GSm = setKey(11, false);
    Array<Audio> Am = setKey(0, false);
    Array<Audio> ASm = setKey(1, false);
    Array<Audio> Bm = setKey(2, false);

    String dispCM = setDispKey(0, true);
    String dispCSM = setDispKey(1, true);
    String dispDM = setDispKey(2, true);
    String dispDSM = setDispKey(3, true);
    String dispEM = setDispKey(4, true);
    String dispFM = setDispKey(5, true);
    String dispFSM = setDispKey(6, true);
    String dispGM = setDispKey(7, true);
    String dispGSM = setDispKey(8, true);
    String dispAM = setDispKey(9, true);
    String dispASM = setDispKey(10, true);
    String dispBM = setDispKey(11, true);

    String dispCm = setDispKey(3, false);
    String dispCSm = setDispKey(4, false);
    String dispDm = setDispKey(5, false);
    String dispDSm = setDispKey(6, false);
    String dispEm = setDispKey(7, false);
    String dispFm = setDispKey(8, false);
    String dispFSm = setDispKey(9, false);
    String dispGm = setDispKey(10, false);
    String dispGSm = setDispKey(11, false);
    String dispAm = setDispKey(0, false);
    String dispASm = setDispKey(1, false);
    String dispBm = setDispKey(2, false);

    Array<KeyList> inputKey = { KeyList::C, KeyList::CS, KeyList::D, KeyList::DS, KeyList::E, KeyList::F, KeyList::FS, KeyList::G, KeyList::GS, KeyList::A, KeyList::AS, KeyList::B,
                                KeyList::Cm, KeyList::CSm, KeyList::Dm, KeyList::DSm, KeyList::Em, KeyList::Fm, KeyList::FSm, KeyList::Gm, KeyList::GSm, KeyList::Am, KeyList::ASm, KeyList::Bm };
    Array<Array<Audio>> outputSound = { CM, CSM, DM, DSM, EM, FM, FSM, GM, GSM, AM, ASM, BM,
                            Cm, CSm, Dm, DSm, Em, Fm, FSm, Gm, GSm, Am, ASm, Bm };
    Array<String> outputDispSound = { dispCM, dispCSM, dispDM, dispDSM, dispEM, dispFM, dispFSM, dispGM, dispGSM, dispAM, dispASM, dispBM,
                            dispCm, dispCSm, dispDm, dispDSm, dispEm, dispFm, dispFSm, dispGm, dispGSm, dispAm, dispASm, dispBm };
    Array<String> outputKey = { U"C" ,U"C#" ,U"D", U"D#", U"E", U"F", U"F#", U"G", U"G#", U"A", U"A#", U"B",
                                U"Cm" ,U"C#m" ,U"Dm", U"D#m", U"Em", U"Fm", U"F#m", U"Gm", U"G#m", U"Am", U"A#m", U"Bm" };
        
    SoundSetting(SecondsF _tpc, int _beat) //発音時間[s]、発音間隔[msec]
    {
        tpc = _tpc;
        pC  = Audio(inst, PianoKey::C4, tpc);
        pCS = Audio(inst, PianoKey::CS4, tpc);
        pD  = Audio(inst, PianoKey::D4, tpc);
        pDS = Audio(inst, PianoKey::DS4, tpc);
        pE  = Audio(inst, PianoKey::E4, tpc);
        pF  = Audio(inst, PianoKey::F4, tpc);
        pFS = Audio(inst, PianoKey::FS4, tpc);
        pG  = Audio(inst, PianoKey::G4, tpc);
        pGS = Audio(inst, PianoKey::GS4, tpc);
        pA  = Audio(inst, PianoKey::A4, tpc);
        pAS = Audio(inst, PianoKey::AS4, tpc);
        pB  = Audio(inst, PianoKey::B4, tpc);
        beat = _beat;
        bar = beat * 16;
    }

    Array<int> ChordSetting(Array<KeyList> c) //コード進行
    {
        Array<int> r;
        for (int i = 0; i < c.size(); i++)
        {
            for (int j = 0; j < inputKey.size(); j++)
            {
                if (c[i] == inputKey[j])
                {
                    r << j;
                }
            }
        }
        return r;
    }
};



//チャンスオペレーションのクラス
class ChanceOperation {

    //方程式パラメータ
    double para[5] = { 1,1,1,1,1 };

    //閾値
    double thre[8] = { -14.0 / 7.0 , -10.0 / 7.0 , -6.0 / 7.0 , -2.0 / 7.0 , 2.0 / 7.0 , 6.0 / 7.0 , 10.0 / 7.0, 14.0 / 7.0 };

    //表示用
    Array<Vec2> pos = Array<Vec2>(10, Vec2(0.0, 0.0)); //現在位置
    LineString lineBack; //軌道表示
    Effect effect; //エフェクト用
    SoundSetting ss = SoundSetting::SoundSetting(0s, 0); //音声設定用
    Array<int> chordProg = {0};

    //処理用
    bool play = false; //再生用
    uint64 passedTimer = 0; //更新時間管理用
    uint64 beatTimer = 0; //発音時間管理用
    double t = 0.0; //経過時間[sec]
    bool on = false; //発音判定用
    int count = 0; //発音回数
    int repeat = 0; //繰り返しカウント用
    int currentChord = 0; //現在のコード進行
    int chordChange = 0; // コード変更フラグ
    int paraHist[2] = { 0, 0 }; //パラメータ保存用
    bool motif = true; //モチーフ有効


    double equationX(double t, double p[5], double period)
    {
        return (sin(p[0] * Math::TwoPi * t / (double)period) - pow(sin(p[1] * Math::TwoPi * t / (double)period), 3));
    }

    double equationY(double t, double p[5], double period)
    {
        return (cos(p[2] * Math::TwoPi * t / (double)period) - pow(cos(p[3] * Math::TwoPi * t / (double)period), 3)) * p[4];
    }

    LineString updateLine(Array<Vec2> m, Vec2 p, int ex)
    {
        LineString L;
        for (int i = 0; i < 10; i++)
        {
            L << Vec2(m[9 - i].x * ex + p.x, p.y - m[9 - i].y * ex);
        }
        return L;
    }

    void setPara(double p[5])
    {
        p[0] = pow(-1, RandomBool()) * Random(1, 10);
        p[1] = pow(-1, RandomBool()) * Random(1, 10);
        if ((int)p[0] == (int)p[1])
        {
            Array<int32> values = { -9,-8,-7,-6,-5,-4,-3,-2,-1,1,2,3,4,5,6,7,8,9 };
            values = values.removed((int32)((int)p[0]));
            p[1] = Sample(values);
        }
        p[2] = Random(1, 10);
        p[3] = Random(1, 10);
        if (p[2] == p[3])
        {
            p[3] = ((int)p[2] + Random(1, 9)) % 10;
            if (p[3] == 0)
            {
                p[3] = 10;
            }
        }
        p[4] = Sample({ 1,-1 });
    }

    LineString setBack(double para[5], Vec2 p, int ex)
    {
        LineString L;
        for (int i = 0; i < 1000; i++)
        {
            double x = equationX(i, para, 1000);
            double y = equationY(i, para, 1000);
            L << Vec2(x * ex + p.x, p.y - y * ex);
        }
        return L;
    }

    struct AttackEffect : IEffect
    {
        Rect pos;

        AttackEffect(const Rect& rec)
            : pos(rec.x, rec.y, rec.w, rec.h) {}

        bool update(double t) override
        {
            pos.draw(ColorF(Palette::Magenta, (0.2 - t) / 0.2));

            return t < 0.2;
        }
    };


public:
    const Font font = Font(15);
    double volumeMax = 0.4;
    double volumeMin = 0.15;
    int ex = 50; //拡大倍率
    Vec2 px = Vec2(225, 260); //表示ポジション
    Array<Audio> sound = ss.CM;

    ChanceOperation(SoundSetting _ss, Array<KeyList> _cprog)
    {
        ss = _ss;
        chordProg = ss.ChordSetting(_cprog);
        sound = ss.outputSound[chordProg[0]];
    }

    void KeyAction() //スタート・ストップのキーアクション用
    {
        if (!play) //起動
        {
            play = true;
            passedTimer = Time::GetMicrosecSinceEpoch();
            beatTimer = Time::GetMicrosecSinceEpoch();
            t = 0;
            on = false;
            count = 1;
            repeat = 0;
            currentChord = 0;
            chordChange = 0;
            setPara(para);
            lineBack.clear();
            lineBack = setBack(para, px, ex);
        }
        else //リセット
        {
            for (int i = 0; i < 10; i++) { pos[i] = Vec2(0.0, 0.0); }
            play = false;
        }
    }

    void Update() //実際の処理部分
    {
        if (play)
        {
            uint64 value = (Time::GetMicrosecSinceEpoch() - passedTimer); //更新時間間隔値[usec]
            passedTimer = Time::GetMicrosecSinceEpoch();
            t += (double)value * 0.001; //更新時間間隔[msec]
            
            if ((t < ss.bar / 2.0 && chordChange == 2) || (t > ss.bar / 2.0 && chordChange == 0))
            {
                chordChange += 1; //コード変更ON
            }
            if ((t < ss.bar / 2.0 && chordChange == 3) || (t > ss.bar/2.0 && chordChange == 1))
            {
                currentChord = (currentChord + 1) % chordProg.size();
                sound = ss.outputSound[chordProg[currentChord]];
                chordChange = (chordChange + 1) % 4; //コード変更OFF
            }

            if (t > ss.bar)
            {
                switch (repeat)
                {
                case 0:
                    if (motif)
                    {
                        paraHist[0] = para[0];
                        paraHist[1] = para[1];
                        para[0] += pow(-1, RandomBool()) * Random(3, 7) / 100;
                        para[1] += pow(-1, RandomBool()) * Random(3, 7) / 100;
                    }
                    repeat = 1;
                    break;
                case 1:
                    if (motif)
                    {
                        para[0] = paraHist[0];
                        para[1] = paraHist[1];
                    }
                    repeat = 2;
                    break;
                case 2:
                    para[2] += pow(-1, RandomBool()) * Random(2, 5) / 100;
                    para[3] += pow(-1, RandomBool()) * Random(2, 5) / 100;
                    repeat = 3;
                    break;
                case 3:
                    setPara(para);
                    lineBack.clear();
                    lineBack = setBack(para, px, ex);
                    repeat = 0;
                    break;
                default:
                    break;
                }
                t -= ss.bar;
            }
            pos.push_front(Vec2(equationX(t, para, ss.bar), equationY(t, para, ss.bar)));
            pos.pop_back();

            value = (Time::GetMicrosecSinceEpoch() - beatTimer); //再生時間値[usec]
            uint64 next = ss.beat * 1000 * count; //発音時間間隔判定[usec]
            if (!on)
            {
                on = true; //発音ON
            }
            if (on && value >= next)
            {
                double volume = volumeMax;
                if (pos[0].y < 0) { volume = volumeMin; }
                for (int i = 0; i < 7; i++)
                {
                    if (pos[0].x >= thre[i] && pos[0].x < thre[i + 1])
                    {
                        sound[i].playOneShot(volume);
                        effect.add<AttackEffect>(Rect(px.x + (thre[i + 1] - 4.0 / 7.0) * ex, px.y, (4.0 / 7.0) * ex, -100 * pos[i].y / abs(pos[i].y)));
                    }
                }
                count += 1;
                on = false; //発音OFF
            }
        }
    }

    void Show() //ディスプレイ表示用
    {
        if (play)
        {
            effect.update(); //エフェクト表示

            LineString lineHist = updateLine(pos, px, ex); //軌跡用
            lineBack.draw(1, Palette::Darkmagenta); //全軌道表示
            lineHist.draw(2, Palette::Pink); //軌跡表示
            Line(px.x - 100, px.y, px.x + 100, px.y).draw(1, Palette::White);
            Line(px.x, px.y - 100, px.x, px.y + 100).draw(1, Palette::White);
            for (int i = 1; i < 7; i++)
            {
                Line(px.x + (int)(thre[i] * ex), px.y - 100, px.x + (int)(thre[i] * ex), px.y + 100).draw(LineStyle::SquareDot, 1, Palette::White);
            }
            font(ss.outputDispSound[chordProg[currentChord]] + U"   key: " +  ss.outputKey[chordProg[currentChord]]).draw(px.x - 85, px.y + 105, Palette::Pink);
            font(U"x").draw(px.x + 104, px.y - 12);
            font(U"y").draw(px.x - 4, px.y - 125);
            font(U"x = sin(a*t) - sin^3(b*t),  y = e*(cos(c*t)-cos^3(d*t))").draw(30, 10);
            font(U"a=", para[0], U", b=", para[1], U", c=", para[2], U", d=", para[3], U", e=", para[4]).draw(30, 35);
            font(U"間隔=", ss.beat, U"[msec] , 周期=", ss.bar, U"[msec]").draw(30, 60);
            String txt = U"転調 ： ";
            for (int i = 0; i < chordProg.size(); i++)
            {
                txt += ss.outputKey[chordProg[i]] + U" → ";
            }
            txt += U"・・・";
            font(txt).draw(30, 85);
        }
    }

};




//ここからメイン
void Main()
{

    //ウィンドウサイズ
    Window::Resize(450, 500);

    SoundSetting ss(0.5s, 200); //音の設定（ここでは、0.5sの音の長さ、200msec間隔）
    ChanceOperation co(ss, { Am, F, G, C}); //コード進行をKeyListのArray方式で入力（ここでは、小室進行）

    while (System::Update())
    {
        if (KeySpace.down()) //スペースキーで起動
        {
            co.KeyAction();
        }
        co.Update();
        co.Show();
    }
}
```