{"id":25349543,"url":"https://github.com/agehama/synthesizertutorial","last_synced_at":"2025-04-08T20:51:07.784Z","repository":{"id":187601936,"uuid":"579623361","full_name":"agehama/SynthesizerTutorial","owner":"agehama","description":"ソフトウェアシンセサイザーの作成チュートリアル","archived":false,"fork":false,"pushed_at":"2023-02-07T09:12:28.000Z","size":1111,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-04T14:46:05.933Z","etag":null,"topics":["audio-programming","cpp","digital-signal-processing","opensiv3d","synthesizer"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/agehama.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2022-12-18T10:20:50.000Z","updated_at":"2022-12-18T10:25:16.000Z","dependencies_parsed_at":"2023-08-11T11:13:54.533Z","dependency_job_id":"841dedfa-8a93-4c97-9594-4efd6b7c5f43","html_url":"https://github.com/agehama/SynthesizerTutorial","commit_stats":null,"previous_names":["agehama/synthesizertutorial"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agehama%2FSynthesizerTutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agehama%2FSynthesizerTutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agehama%2FSynthesizerTutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agehama%2FSynthesizerTutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/agehama","download_url":"https://codeload.github.com/agehama/SynthesizerTutorial/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247927305,"owners_count":21019506,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audio-programming","cpp","digital-signal-processing","opensiv3d","synthesizer"],"created_at":"2025-02-14T16:43:22.583Z","updated_at":"2025-04-08T20:51:07.766Z","avatar_url":"https://github.com/agehama.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SynthesizerTutorial\nソフトウェアシンセサイザーを作成するチュートリアルです\n\n記事：  \n１．https://qiita.com/agehama_/items/7da430491400e9a2b6a7  \n２．https://qiita.com/agehama_/items/863933459ca44ca5dbe0  \n３．https://qiita.com/agehama_/items/29175c24eae588c3e9f7  \n\n## ビルド環境\n- Visual Studio 2022\n- OpenSiv3D v0.6.6\n\n## その１：サイン波でMIDIを再生する\n### １.１ サイン波を再生する\n```diff\n@@ -0,0 +1,45 @@\n+﻿# include \u003cSiv3D.hpp\u003e // OpenSiv3D v0.6.6\n+\n+const auto SliderHeight = 36;\n+const auto SliderWidth = 400;\n+const auto LabelWidth = 200;\n+\n+Wave RenderWave(uint32 seconds, double amplitude, double frequency)\n+{\n+\tconst auto lengthOfSamples = seconds * Wave::DefaultSampleRate;\n+\n+\tWave wave(lengthOfSamples);\n+\n+\tfor (uint32 i = 0; i \u003c lengthOfSamples; ++i)\n+\t{\n+\t\tconst double sec = 1.0f * i / Wave::DefaultSampleRate;\n+\t\tconst double w = sin(Math::TwoPiF * frequency * sec) * amplitude;\n+\t\twave[i].left = wave[i].right = static_cast\u003cfloat\u003e(w);\n+\t}\n+\n+\treturn wave;\n+}\n+\n+void Main()\n+{\n+\tdouble amplitude = 0.2;\n+\tdouble frequency = 440.0;\n+\n+\tuint32 seconds = 3;\n+\n+\tAudio audio(RenderWave(seconds, amplitude, frequency));\n+\taudio.play();\n+\n+\twhile (System::Update())\n+\t{\n+\t\tVec2 pos(20, 20 - SliderHeight);\n+\t\tSimpleGUI::Slider(U\"amplitude : {:.2f}\"_fmt(amplitude), amplitude, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n+\t\tSimpleGUI::Slider(U\"frequency : {:.0f}\"_fmt(frequency), frequency, 100.0, 1000.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n+\n+\t\tif (SimpleGUI::Button(U\"波形を再生成\", Vec2{ pos.x, pos.y += SliderHeight }))\n+\t\t{\n+\t\t\taudio = Audio(RenderWave(seconds, amplitude, frequency));\n+\t\t\taudio.play();\n+\t\t}\n+\t}\n+}\n```\n### １.２ ADSR エンベロープを実装する\n```diff\n@@ -4,17 +4,134 @@ const auto SliderHeight = 36;\n const auto SliderWidth = 400;\n const auto LabelWidth = 200;\n \n-Wave RenderWave(uint32 seconds, double amplitude, double frequency)\n+struct ADSRConfig\n+{\n+\tdouble attackTime = 0.01;\n+\tdouble decayTime = 0.01;\n+\tdouble sustainLevel = 0.6;\n+\tdouble releaseTime = 0.4;\n+\n+\tvoid updateGUI(Vec2\u0026 pos)\n+\t{\n+\t\tSimpleGUI::Slider(U\"attack : {:.2f}\"_fmt(attackTime), attackTime, 0.0, 0.5, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n+\t\tSimpleGUI::Slider(U\"decay : {:.2f}\"_fmt(decayTime), decayTime, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n+\t\tSimpleGUI::Slider(U\"sustain : {:.2f}\"_fmt(sustainLevel), sustainLevel, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n+\t\tSimpleGUI::Slider(U\"release : {:.2f}\"_fmt(releaseTime), releaseTime, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n+\t}\n+};\n+\n+class EnvGenerator\n+{\n+public:\n+\n+\tenum class State\n+\t{\n+\t\tAttack, Decay, Sustain, Release\n+\t};\n+\n+\tvoid noteOff()\n+\t{\n+\t\tif (m_state != State::Release)\n+\t\t{\n+\t\t\tm_elapsed = 0;\n+\t\t\tm_state = State::Release;\n+\t\t}\n+\t}\n+\n+\tvoid reset(State state)\n+\t{\n+\t\tm_elapsed = 0;\n+\t\tm_state = state;\n+\t}\n+\n+\tvoid update(const ADSRConfig\u0026 adsr, double dt)\n+\t{\n+\t\tswitch (m_state)\n+\t\t{\n+\t\tcase State::Attack: // 0.0 から 1.0 まで attackTime かけて増幅する\n+\t\t\tif (m_elapsed \u003c adsr.attackTime)\n+\t\t\t{\n+\t\t\t\tm_currentLevel = m_elapsed / adsr.attackTime;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tm_elapsed -= adsr.attackTime;\n+\t\t\tm_state = State::Decay;\n+\t\t\t[[fallthrough]]; // Decay処理にそのまま続く\n+\n+\t\tcase State::Decay: // 1.0 から sustainLevel まで decayTime かけて減衰する\n+\t\t\tif (m_elapsed \u003c adsr.decayTime)\n+\t\t\t{\n+\t\t\t\tm_currentLevel = Math::Lerp(1.0, adsr.sustainLevel, m_elapsed / adsr.decayTime);\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tm_elapsed -= adsr.decayTime;\n+\t\t\tm_state = State::Sustain;\n+\t\t\t[[fallthrough]]; // Sustain処理にそのまま続く\n+\n+\n+\t\tcase State::Sustain: // ノートオンの間 sustainLevel を維持する\n+\t\t\tm_currentLevel = adsr.sustainLevel;\n+\t\t\tbreak;\n+\n+\t\tcase State::Release: // sustainLevel から 0.0 まで releaseTime かけて減衰する\n+\t\t\tm_currentLevel = m_elapsed \u003c adsr.releaseTime\n+\t\t\t\t? Math::Lerp(adsr.sustainLevel, 0.0, m_elapsed / adsr.releaseTime)\n+\t\t\t\t: 0.0;\n+\t\t\tbreak;\n+\n+\t\tdefault: break;\n+\t\t}\n+\n+\t\tm_elapsed += dt;\n+\t}\n+\n+\tbool isReleased(const ADSRConfig\u0026 adsr) const\n+\t{\n+\t\treturn m_state == State::Release \u0026\u0026 adsr.releaseTime \u003c= m_elapsed;\n+\t}\n+\n+\tdouble currentLevel() const\n+\t{\n+\t\treturn m_currentLevel;\n+\t}\n+\n+\tState state() const\n+\t{\n+\t\treturn m_state;\n+\t}\n+\n+private:\n+\n+\tState m_state = State::Attack;\n+\tdouble m_elapsed = 0; // ステート変更からの経過秒数\n+\tdouble m_currentLevel = 0; // 現在のレベル [0, 1]\n+};\n+\n+Wave RenderWave(uint32 seconds, double amplitude, double frequency, const ADSRConfig\u0026 adsr)\n {\n \tconst auto lengthOfSamples = seconds * Wave::DefaultSampleRate;\n \n \tWave wave(lengthOfSamples);\n \n+\t// 0サンプル目でノートオン\n+\tEnvGenerator envelope;\n+\n+\t// 半分経過したところでノートオフ\n+\tconst auto noteOffSample = lengthOfSamples / 2;\n+\n+\tconst float deltaT = 1.0f / Wave::DefaultSampleRate;\n+\tfloat time = 0;\n \tfor (uint32 i = 0; i \u003c lengthOfSamples; ++i)\n \t{\n-\t\tconst double sec = 1.0f * i / Wave::DefaultSampleRate;\n-\t\tconst double w = sin(Math::TwoPiF * frequency * sec) * amplitude;\n+\t\tif (i == noteOffSample)\n+\t\t{\n+\t\t\tenvelope.noteOff();\n+\t\t}\n+\t\tconst auto w = sin(Math::TwoPiF * frequency * time)\n+\t\t\t* amplitude * envelope.currentLevel();\n \t\twave[i].left = wave[i].right = static_cast\u003cfloat\u003e(w);\n+\t\ttime += deltaT;\n+\t\tenvelope.update(adsr, deltaT);\n \t}\n \n \treturn wave;\n@@ -27,7 +144,13 @@ void Main()\n \n \tuint32 seconds = 3;\n \n-\tAudio audio(RenderWave(seconds, amplitude, frequency));\n+\tADSRConfig adsr;\n+\tadsr.attackTime = 0.1;\n+\tadsr.decayTime = 0.1;\n+\tadsr.sustainLevel = 0.8;\n+\tadsr.releaseTime = 0.5;\n+\n+\tAudio audio(RenderWave(seconds, amplitude, frequency, adsr));\n \taudio.play();\n \n \twhile (System::Update())\n@@ -36,9 +159,11 @@ void Main()\n \t\tSimpleGUI::Slider(U\"amplitude : {:.2f}\"_fmt(amplitude), amplitude, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n \t\tSimpleGUI::Slider(U\"frequency : {:.0f}\"_fmt(frequency), frequency, 100.0, 1000.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n \n+\t\tadsr.updateGUI(pos);\n+\n \t\tif (SimpleGUI::Button(U\"波形を再生成\", Vec2{ pos.x, pos.y += SliderHeight }))\n \t\t{\n-\t\t\taudio = Audio(RenderWave(seconds, amplitude, frequency));\n+\t\t\taudio = Audio(RenderWave(seconds, amplitude, frequency, adsr));\n \t\t\taudio.play();\n \t\t}\n \t}\n```\n### １.３ 音階に対応させる\n```diff\n@@ -107,7 +107,12 @@ private:\n \tdouble m_currentLevel = 0; // 現在のレベル [0, 1]\n };\n \n-Wave RenderWave(uint32 seconds, double amplitude, double frequency, const ADSRConfig\u0026 adsr)\n+float NoteNumberToFrequency(int8_t d)\n+{\n+\treturn 440.0f * pow(2.0f, (d - 69) / 12.0f);\n+}\n+\n+Wave RenderWave(uint32 seconds, double amplitude, const Array\u003cint8_t\u003e\u0026 noteNumbers, const ADSRConfig\u0026 adsr)\n {\n \tconst auto lengthOfSamples = seconds * Wave::DefaultSampleRate;\n \n@@ -121,14 +126,23 @@ Wave RenderWave(uint32 seconds, double amplitude, double frequency, const ADSRCo\n \n \tconst float deltaT = 1.0f / Wave::DefaultSampleRate;\n \tfloat time = 0;\n+\n \tfor (uint32 i = 0; i \u003c lengthOfSamples; ++i)\n \t{\n \t\tif (i == noteOffSample)\n \t\t{\n \t\t\tenvelope.noteOff();\n \t\t}\n-\t\tconst auto w = sin(Math::TwoPiF * frequency * time)\n-\t\t\t* amplitude * envelope.currentLevel();\n+\n+\t\t// 和音の各波形を加算合成する\n+\t\tdouble w = 0;\n+\t\tfor (auto note : noteNumbers)\n+\t\t{\n+\t\t\tconst auto freq = NoteNumberToFrequency(note);\n+\t\t\tw += sin(Math::TwoPiF * freq * time)\n+\t\t\t\t* amplitude * envelope.currentLevel();\n+\t\t}\n+\n \t\twave[i].left = wave[i].right = static_cast\u003cfloat\u003e(w);\n \t\ttime += deltaT;\n \t\tenvelope.update(adsr, deltaT);\n@@ -140,7 +154,6 @@ Wave RenderWave(uint32 seconds, double amplitude, double frequency, const ADSRCo\n void Main()\n {\n \tdouble amplitude = 0.2;\n-\tdouble frequency = 440.0;\n \n \tuint32 seconds = 3;\n \n@@ -150,20 +163,26 @@ void Main()\n \tadsr.sustainLevel = 0.8;\n \tadsr.releaseTime = 0.5;\n \n-\tAudio audio(RenderWave(seconds, amplitude, frequency, adsr));\n+\tconst Array\u003cint8_t\u003e noteNumbers =\n+\t{\n+\t\t60, // C_4\n+\t\t64, // E_4\n+\t\t67, // G_4\n+\t};\n+\n+\tAudio audio(RenderWave(seconds, amplitude, noteNumbers, adsr));\n \taudio.play();\n \n \twhile (System::Update())\n \t{\n \t\tVec2 pos(20, 20 - SliderHeight);\n \t\tSimpleGUI::Slider(U\"amplitude : {:.2f}\"_fmt(amplitude), amplitude, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n-\t\tSimpleGUI::Slider(U\"frequency : {:.0f}\"_fmt(frequency), frequency, 100.0, 1000.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n \n \t\tadsr.updateGUI(pos);\n \n \t\tif (SimpleGUI::Button(U\"波形を再生成\", Vec2{ pos.x, pos.y += SliderHeight }))\n \t\t{\n-\t\t\taudio = Audio(RenderWave(seconds, amplitude, frequency, adsr));\n+\t\t\taudio = Audio(RenderWave(seconds, amplitude, noteNumbers, adsr));\n \t\t\taudio.play();\n \t\t}\n \t}\n```\n### １.４ シンセサイザーを定義する\n```diff\n@@ -112,40 +112,114 @@ float NoteNumberToFrequency(int8_t d)\n \treturn 440.0f * pow(2.0f, (d - 69) / 12.0f);\n }\n \n-Wave RenderWave(uint32 seconds, double amplitude, const Array\u003cint8_t\u003e\u0026 noteNumbers, const ADSRConfig\u0026 adsr)\n+struct NoteState\n+{\n+\tEnvGenerator m_envelope;\n+};\n+\n+class Synthesizer\n+{\n+public:\n+\n+\t// 1サンプル波形を生成して返す\n+\tWaveSample renderSample()\n+\t{\n+\t\tconst auto deltaT = 1.0 / Wave::DefaultSampleRate;\n+\n+\t\t// エンベロープの更新\n+\t\tfor (auto\u0026 [noteNumber, noteState] : m_noteState)\n+\t\t{\n+\t\t\tnoteState.m_envelope.update(m_adsr, deltaT);\n+\t\t}\n+\n+\t\t// リリースが終了したノートを削除する\n+\t\tstd::erase_if(m_noteState, [\u0026](const auto\u0026 noteState) { return noteState.second.m_envelope.isReleased(m_adsr); });\n+\n+\t\t// 入力中の波形を加算して書き込む\n+\t\tWaveSample sample(0, 0);\n+\t\tfor (auto\u0026 [noteNumber, noteState] : m_noteState)\n+\t\t{\n+\t\t\tconst auto envLevel = noteState.m_envelope.currentLevel();\n+\t\t\tconst auto frequency = NoteNumberToFrequency(noteNumber);\n+\n+\t\t\tconst auto w = static_cast\u003cfloat\u003e(sin(Math::TwoPiF * frequency * m_time) * envLevel);\n+\t\t\tsample.left += w;\n+\t\t\tsample.right += w;\n+\t\t}\n+\n+\t\tm_time += deltaT;\n+\n+\t\treturn sample * static_cast\u003cfloat\u003e(m_amplitude);\n+\t}\n+\n+\tvoid noteOn(int8_t noteNumber)\n+\t{\n+\t\tm_noteState.emplace(noteNumber, NoteState());\n+\t}\n+\n+\tvoid noteOff(int8_t noteNumber)\n+\t{\n+\t\tauto [beginIt, endIt] = m_noteState.equal_range(noteNumber);\n+\n+\t\tfor (auto it = beginIt; it != endIt; ++it)\n+\t\t{\n+\t\t\tauto\u0026 envelope = it-\u003esecond.m_envelope;\n+\n+\t\t\t// noteOnになっている最初の要素をnoteOffにする\n+\t\t\tif (envelope.state() != EnvGenerator::State::Release)\n+\t\t\t{\n+\t\t\t\tenvelope.noteOff();\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tvoid updateGUI(Vec2\u0026 pos)\n+\t{\n+\t\tSimpleGUI::Slider(U\"amplitude : {:.2f}\"_fmt(m_amplitude), m_amplitude, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n+\n+\t\tm_adsr.updateGUI(pos);\n+\t}\n+\n+\tvoid clear()\n+\t{\n+\t\tm_noteState.clear();\n+\t}\n+\n+private:\n+\n+\tstd::multimap\u003cint8_t, NoteState\u003e m_noteState;\n+\n+\tADSRConfig m_adsr;\n+\n+\tdouble m_amplitude = 0.2;\n+\n+\tdouble m_time = 0;\n+};\n+\n+Wave RenderWave(uint32 seconds, Synthesizer\u0026 synth)\n {\n \tconst auto lengthOfSamples = seconds * Wave::DefaultSampleRate;\n \n \tWave wave(lengthOfSamples);\n \n-\t// 0サンプル目でノートオン\n-\tEnvGenerator envelope;\n-\n \t// 半分経過したところでノートオフ\n \tconst auto noteOffSample = lengthOfSamples / 2;\n \n-\tconst float deltaT = 1.0f / Wave::DefaultSampleRate;\n-\tfloat time = 0;\n+\tsynth.noteOn(60); // C_4\n+\tsynth.noteOn(64); // E_4\n+\tsynth.noteOn(67); // G_4\n \n \tfor (uint32 i = 0; i \u003c lengthOfSamples; ++i)\n \t{\n \t\tif (i == noteOffSample)\n \t\t{\n-\t\t\tenvelope.noteOff();\n-\t\t}\n-\n-\t\t// 和音の各波形を加算合成する\n-\t\tdouble w = 0;\n-\t\tfor (auto note : noteNumbers)\n-\t\t{\n-\t\t\tconst auto freq = NoteNumberToFrequency(note);\n-\t\t\tw += sin(Math::TwoPiF * freq * time)\n-\t\t\t\t* amplitude * envelope.currentLevel();\n+\t\t\tsynth.noteOff(60);\n+\t\t\tsynth.noteOff(64);\n+\t\t\tsynth.noteOff(67);\n \t\t}\n \n-\t\twave[i].left = wave[i].right = static_cast\u003cfloat\u003e(w);\n-\t\ttime += deltaT;\n-\t\tenvelope.update(adsr, deltaT);\n+\t\twave[i] = synth.renderSample();\n \t}\n \n \treturn wave;\n@@ -153,36 +227,24 @@ Wave RenderWave(uint32 seconds, double amplitude, const Array\u003cint8_t\u003e\u0026 noteNumbe\n \n void Main()\n {\n-\tdouble amplitude = 0.2;\n-\n \tuint32 seconds = 3;\n \n-\tADSRConfig adsr;\n-\tadsr.attackTime = 0.1;\n-\tadsr.decayTime = 0.1;\n-\tadsr.sustainLevel = 0.8;\n-\tadsr.releaseTime = 0.5;\n+\tSynthesizer synth;\n \n-\tconst Array\u003cint8_t\u003e noteNumbers =\n-\t{\n-\t\t60, // C_4\n-\t\t64, // E_4\n-\t\t67, // G_4\n-\t};\n-\n-\tAudio audio(RenderWave(seconds, amplitude, noteNumbers, adsr));\n+\tAudio audio(RenderWave(seconds, synth));\n \taudio.play();\n \n \twhile (System::Update())\n \t{\n \t\tVec2 pos(20, 20 - SliderHeight);\n-\t\tSimpleGUI::Slider(U\"amplitude : {:.2f}\"_fmt(amplitude), amplitude, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n \n-\t\tadsr.updateGUI(pos);\n+\t\tsynth.updateGUI(pos);\n \n \t\tif (SimpleGUI::Button(U\"波形を再生成\", Vec2{ pos.x, pos.y += SliderHeight }))\n \t\t{\n-\t\t\taudio = Audio(RenderWave(seconds, amplitude, noteNumbers, adsr));\n+\t\t\tsynth.clear();\n+\n+\t\t\taudio = Audio(RenderWave(seconds, synth));\n \t\t\taudio.play();\n \t\t}\n \t}\n```\n### １.５ 譜面を再生する\n```diff\n@@ -1,5 +1,7 @@\n ﻿# include \u003cSiv3D.hpp\u003e // OpenSiv3D v0.6.6\n \n+#include \"SoundTools.hpp\"\n+\n const auto SliderHeight = 36;\n const auto SliderWidth = 400;\n const auto LabelWidth = 200;\n@@ -114,6 +116,7 @@ float NoteNumberToFrequency(int8_t d)\n \n struct NoteState\n {\n+\tfloat m_velocity = 1.f;\n \tEnvGenerator m_envelope;\n };\n \n@@ -139,7 +142,7 @@ public:\n \t\tWaveSample sample(0, 0);\n \t\tfor (auto\u0026 [noteNumber, noteState] : m_noteState)\n \t\t{\n-\t\t\tconst auto envLevel = noteState.m_envelope.currentLevel();\n+\t\t\tconst auto envLevel = noteState.m_envelope.currentLevel() * noteState.m_velocity;\n \t\t\tconst auto frequency = NoteNumberToFrequency(noteNumber);\n \n \t\t\tconst auto w = static_cast\u003cfloat\u003e(sin(Math::TwoPiF * frequency * m_time) * envLevel);\n@@ -152,9 +155,11 @@ public:\n \t\treturn sample * static_cast\u003cfloat\u003e(m_amplitude);\n \t}\n \n-\tvoid noteOn(int8_t noteNumber)\n+\tvoid noteOn(int8_t noteNumber, int8_t velocity)\n \t{\n-\t\tm_noteState.emplace(noteNumber, NoteState());\n+\t\tNoteState noteState;\n+\t\tnoteState.m_velocity = velocity / 127.0f;\n+\t\tm_noteState.emplace(noteNumber, noteState);\n \t}\n \n \tvoid noteOff(int8_t noteNumber)\n@@ -192,33 +197,52 @@ private:\n \n \tADSRConfig m_adsr;\n \n-\tdouble m_amplitude = 0.2;\n+\tdouble m_amplitude = 0.1;\n \n \tdouble m_time = 0;\n };\n \n-Wave RenderWave(uint32 seconds, Synthesizer\u0026 synth)\n+Wave RenderWave(Synthesizer\u0026 synth, const MidiData\u0026 midiData)\n {\n-\tconst auto lengthOfSamples = seconds * Wave::DefaultSampleRate;\n+\tconst auto lengthOfSamples = static_cast\u003cint64\u003e(ceil(midiData.lengthOfTime() * Wave::DefaultSampleRate));\n \n \tWave wave(lengthOfSamples);\n \n-\t// 半分経過したところでノートオフ\n-\tconst auto noteOffSample = lengthOfSamples / 2;\n+\tfor (int64 i = 0; i \u003c lengthOfSamples; ++i)\n+\t{\n+\t\tconst auto currentTime = 1.0 * i / wave.sampleRate();\n+\t\tconst auto nextTime = 1.0 * (i + 1) / wave.sampleRate();\n \n-\tsynth.noteOn(60); // C_4\n-\tsynth.noteOn(64); // E_4\n-\tsynth.noteOn(67); // G_4\n+\t\tconst auto currentTick = midiData.secondsToTicks(currentTime);\n+\t\tconst auto nextTick = midiData.secondsToTicks(nextTime);\n \n-\tfor (uint32 i = 0; i \u003c lengthOfSamples; ++i)\n-\t{\n-\t\tif (i == noteOffSample)\n+\t\t// tick が進んだら MIDI イベントの処理を更新する\n+\t\tif (currentTick != nextTick)\n \t\t{\n-\t\t\tsynth.noteOff(60);\n-\t\t\tsynth.noteOff(64);\n-\t\t\tsynth.noteOff(67);\n+\t\t\tfor (const auto\u0026 track : midiData.tracks())\n+\t\t\t{\n+\t\t\t\tif (track.isPercussionTrack())\n+\t\t\t\t{\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n+\n+\t\t\t\t// 発生したノートオフイベントをシンセに登録\n+\t\t\t\tconst auto noteOffEvents = track.getMIDIEvent\u003cNoteOffEvent\u003e(currentTick, nextTick);\n+\t\t\t\tfor (auto\u0026 [tick, noteOff] : noteOffEvents)\n+\t\t\t\t{\n+\t\t\t\t\tsynth.noteOff(noteOff.note_number);\n+\t\t\t\t}\n+\n+\t\t\t\t// 発生したノートオンイベントをシンセに登録\n+\t\t\t\tconst auto noteOnEvents = track.getMIDIEvent\u003cNoteOnEvent\u003e(currentTick, nextTick);\n+\t\t\t\tfor (auto\u0026 [tick, noteOn] : noteOnEvents)\n+\t\t\t\t{\n+\t\t\t\t\tsynth.noteOn(noteOn.note_number, noteOn.velocity);\n+\t\t\t\t}\n+\t\t\t}\n \t\t}\n \n+\t\t// シンセを1サンプル更新して波形を書き込む\n \t\twave[i] = synth.renderSample();\n \t}\n \n@@ -227,11 +251,18 @@ Wave RenderWave(uint32 seconds, Synthesizer\u0026 synth)\n \n void Main()\n {\n-\tuint32 seconds = 3;\n+\tauto midiDataOpt = LoadMidi(U\"short_loop.mid\");\n+\tif (!midiDataOpt)\n+\t{\n+\t\t// ファイルが見つからない or 読み込みエラー\n+\t\treturn;\n+\t}\n+\n+\tconst MidiData\u0026 midiData = midiDataOpt.value();\n \n \tSynthesizer synth;\n \n-\tAudio audio(RenderWave(seconds, synth));\n+\tAudio audio(RenderWave(synth, midiData));\n \taudio.play();\n \n \twhile (System::Update())\n@@ -242,9 +273,10 @@ void Main()\n \n \t\tif (SimpleGUI::Button(U\"波形を再生成\", Vec2{ pos.x, pos.y += SliderHeight }))\n \t\t{\n+\t\t\taudio.stop();\n \t\t\tsynth.clear();\n \n-\t\t\taudio = Audio(RenderWave(seconds, synth));\n+\t\t\taudio = Audio(RenderWave(synth, midiData));\n \t\t\taudio.play();\n \t\t}\n \t}\n```\n## その２：基本波形とリアルタイム再生\n### ２.１ 基本波形を追加する\n```diff\n@@ -2,6 +2,76 @@\n \n #include \"SoundTools.hpp\"\n \n+double WaveSaw(double t, int n)\n+{\n+\tdouble sum = 0;\n+\tfor (int k = 1; k \u003c= n; ++k)\n+\t{\n+\t\tconst double a = (k % 2 == 0 ? 1.0 : -1.0) / k;\n+\t\tsum += a * sin(k * t);\n+\t}\n+\n+\treturn -2.0 * sum / Math::Pi;\n+}\n+\n+double WaveSquare(double t, int n)\n+{\n+\tdouble sum = 0;\n+\tfor (int k = 1; k \u003c= n; ++k)\n+\t{\n+\t\tconst double a = 2.0 * k - 1.0;\n+\t\tsum += sin(a * t) / a;\n+\t}\n+\n+\treturn 4.0 * sum / Math::Pi;\n+}\n+\n+double WavePulse(double t, int n, double d)\n+{\n+\tdouble sum = 0;\n+\tfor (int k = 1; k \u003c= n; ++k)\n+\t{\n+\t\tconst double a = sin(k * d * Math::Pi) / k;\n+\t\tsum += a * cos(k * (t - d * Math::Pi));\n+\t}\n+\n+\treturn 2.0 * d - 1.0 + 4.0 * sum / Math::Pi;\n+}\n+\n+double WaveNoise()\n+{\n+\treturn Random(-1.0, 1.0);\n+}\n+\n+enum class WaveForm\n+{\n+\tSaw, Sin, Square, Noise,\n+};\n+\n+static constexpr uint32 SamplingFreq = Wave::DefaultSampleRate;\n+static constexpr uint32 MaxFreq = SamplingFreq / 2;\n+\n+class Oscillator\n+{\n+public:\n+\n+\tdouble get(double t, double freq, WaveForm waveForm) const\n+\t{\n+\t\tswitch (waveForm)\n+\t\t{\n+\t\tcase WaveForm::Saw:\n+\t\t\treturn WaveSaw(freq * t * 2_pi, static_cast\u003cint\u003e(MaxFreq / freq));\n+\t\tcase WaveForm::Sin:\n+\t\t\treturn sin(freq * t * 2_pi);\n+\t\tcase WaveForm::Square:\n+\t\t\treturn WaveSquare(freq * t * 2_pi, static_cast\u003cint\u003e((MaxFreq + freq) / (freq * 2.0)));\n+\t\tcase WaveForm::Noise:\n+\t\t\treturn WaveNoise();\n+\t\tdefault: return 0;\n+\t\t}\n+\t}\n+};\n+\n const auto SliderHeight = 36;\n const auto SliderWidth = 400;\n const auto LabelWidth = 200;\n@@ -22,6 +92,15 @@ struct ADSRConfig\n \t}\n };\n \n+bool SliderInt(const String\u0026 label, int\u0026 value, double min, double max, const Vec2\u0026 pos, double labelWidth = 80.0, double sliderWidth = 120.0, bool enabled = true)\n+{\n+\tstatic std::unordered_map\u003cint*, double\u003e val;\n+\tval[\u0026value] = value;\n+\tconst bool result = SimpleGUI::Slider(label, val[\u0026value], min, max, pos, labelWidth, sliderWidth, enabled);\n+\tvalue = static_cast\u003cint\u003e(Math::Round(val[\u0026value]));\n+\treturn result;\n+}\n+\n class EnvGenerator\n {\n public:\n@@ -145,7 +224,8 @@ public:\n \t\t\tconst auto envLevel = noteState.m_envelope.currentLevel() * noteState.m_velocity;\n \t\t\tconst auto frequency = NoteNumberToFrequency(noteNumber);\n \n-\t\t\tconst auto w = static_cast\u003cfloat\u003e(sin(Math::TwoPiF * frequency * m_time) * envLevel);\n+\t\t\tconst auto osc = m_oscillator.get(m_time, frequency, static_cast\u003cWaveForm\u003e(m_oscIndex));\n+\t\t\tconst auto w = static_cast\u003cfloat\u003e(osc * envLevel);\n \t\t\tsample.left += w;\n \t\t\tsample.right += w;\n \t\t}\n@@ -182,6 +262,7 @@ public:\n \tvoid updateGUI(Vec2\u0026 pos)\n \t{\n \t\tSimpleGUI::Slider(U\"amplitude : {:.2f}\"_fmt(m_amplitude), m_amplitude, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n+\t\tSliderInt(U\"oscillator : {}\"_fmt(m_oscIndex), m_oscIndex, 0, 3, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n \n \t\tm_adsr.updateGUI(pos);\n \t}\n@@ -198,6 +279,8 @@ private:\n \tADSRConfig m_adsr;\n \n \tdouble m_amplitude = 0.1;\n+\tOscillator m_oscillator;\n+\tint m_oscIndex = 0;\n \n \tdouble m_time = 0;\n };\n@@ -251,6 +334,7 @@ Wave RenderWave(Synthesizer\u0026 synth, const MidiData\u0026 midiData)\n \n void Main()\n {\n+\t// 注意：波形生成に数秒かかります。example/midi/test.mid を渡すと長すぎて返って来ないので注意\n \tauto midiDataOpt = LoadMidi(U\"short_loop.mid\");\n \tif (!midiDataOpt)\n \t{\n```\n### ２.２ オシレータをウェーブテーブル化する\n```diff\n@@ -51,25 +51,62 @@ enum class WaveForm\n static constexpr uint32 SamplingFreq = Wave::DefaultSampleRate;\n static constexpr uint32 MaxFreq = SamplingFreq / 2;\n \n-class Oscillator\n+class OscillatorWavetable\n {\n public:\n \n-\tdouble get(double t, double freq, WaveForm waveForm) const\n+\tOscillatorWavetable() = default;\n+\n+\tOscillatorWavetable(size_t resolution, double frequency, WaveForm waveType) :\n+\t\tm_wave(resolution)\n \t{\n-\t\tswitch (waveForm)\n+\t\tconst int mSaw = static_cast\u003cint\u003e(MaxFreq / frequency);\n+\t\tconst int mSquare = static_cast\u003cint\u003e((MaxFreq + frequency) / (frequency * 2.0));\n+\n+\t\tfor (size_t i = 0; i \u003c resolution; ++i)\n \t\t{\n-\t\tcase WaveForm::Saw:\n-\t\t\treturn WaveSaw(freq * t * 2_pi, static_cast\u003cint\u003e(MaxFreq / freq));\n-\t\tcase WaveForm::Sin:\n-\t\t\treturn sin(freq * t * 2_pi);\n-\t\tcase WaveForm::Square:\n-\t\t\treturn WaveSquare(freq * t * 2_pi, static_cast\u003cint\u003e((MaxFreq + freq) / (freq * 2.0)));\n-\t\tcase WaveForm::Noise:\n-\t\t\treturn WaveNoise();\n-\t\tdefault: return 0;\n+\t\t\tconst double angle = 2_pi * i / resolution;\n+\n+\t\t\tswitch (waveType)\n+\t\t\t{\n+\t\t\tcase WaveForm::Saw:\n+\t\t\t\tm_wave[i] = static_cast\u003cfloat\u003e(WaveSaw(angle, mSaw));\n+\t\t\t\tbreak;\n+\t\t\tcase WaveForm::Sin:\n+\t\t\t\tm_wave[i] = static_cast\u003cfloat\u003e(sin(angle));\n+\t\t\t\tbreak;\n+\t\t\tcase WaveForm::Square:\n+\t\t\t\tm_wave[i] = static_cast\u003cfloat\u003e(WaveSquare(angle, mSquare));\n+\t\t\t\tbreak;\n+\t\t\tcase WaveForm::Noise:\n+\t\t\t\tm_wave[i] = static_cast\u003cfloat\u003e(WaveNoise());\n+\t\t\t\tbreak;\n+\t\t\tdefault: break;\n+\t\t\t}\n \t\t}\n \t}\n+\n+\tdouble get(double x) const\n+\t{\n+\t\tconst size_t resolution = m_wave.size();\n+\t\tconst double indexFloat = fmod(x * resolution / 2_pi, resolution);\n+\t\tconst int indexInt = static_cast\u003cint\u003e(indexFloat);\n+\t\tconst double rate = indexFloat - indexInt;\n+\t\treturn Math::Lerp(m_wave[indexInt], m_wave[(indexInt + 1) % resolution], rate);\n+\t}\n+\n+private:\n+\n+\tArray\u003cfloat\u003e m_wave;\n+};\n+\n+// とりあえず440Hzで生成\n+static Array\u003cOscillatorWavetable\u003e OscWaveTables =\n+{\n+\tOscillatorWavetable(2048, 440, WaveForm::Saw),\n+\tOscillatorWavetable(2048, 440, WaveForm::Sin),\n+\tOscillatorWavetable(2048, 440, WaveForm::Square),\n+\tOscillatorWavetable(SamplingFreq, 440, WaveForm::Noise),\n };\n \n const auto SliderHeight = 36;\n@@ -206,7 +243,7 @@ public:\n \t// 1サンプル波形を生成して返す\n \tWaveSample renderSample()\n \t{\n-\t\tconst auto deltaT = 1.0 / Wave::DefaultSampleRate;\n+\t\tconst auto deltaT = 1.0 / SamplingFreq;\n \n \t\t// エンベロープの更新\n \t\tfor (auto\u0026 [noteNumber, noteState] : m_noteState)\n@@ -224,7 +261,7 @@ public:\n \t\t\tconst auto envLevel = noteState.m_envelope.currentLevel() * noteState.m_velocity;\n \t\t\tconst auto frequency = NoteNumberToFrequency(noteNumber);\n \n-\t\t\tconst auto osc = m_oscillator.get(m_time, frequency, static_cast\u003cWaveForm\u003e(m_oscIndex));\n+\t\t\tconst auto osc = OscWaveTables[m_oscIndex].get(m_time * frequency * 2_pi);\n \t\t\tconst auto w = static_cast\u003cfloat\u003e(osc * envLevel);\n \t\t\tsample.left += w;\n \t\t\tsample.right += w;\n@@ -279,7 +316,6 @@ private:\n \tADSRConfig m_adsr;\n \n \tdouble m_amplitude = 0.1;\n-\tOscillator m_oscillator;\n \tint m_oscIndex = 0;\n \n \tdouble m_time = 0;\n@@ -334,7 +370,6 @@ Wave RenderWave(Synthesizer\u0026 synth, const MidiData\u0026 midiData)\n \n void Main()\n {\n-\t// 注意：波形生成に数秒かかります。example/midi/test.mid を渡すと長すぎて返って来ないので注意\n \tauto midiDataOpt = LoadMidi(U\"short_loop.mid\");\n \tif (!midiDataOpt)\n \t{\n```\n### ２.３ 帯域制限付きウェーブテーブルを実装する\n```diff\n@@ -49,6 +49,7 @@ enum class WaveForm\n };\n \n static constexpr uint32 SamplingFreq = Wave::DefaultSampleRate;\n+static constexpr uint32 MinFreq = 20;\n static constexpr uint32 MaxFreq = SamplingFreq / 2;\n \n class OscillatorWavetable\n@@ -100,13 +101,59 @@ private:\n \tArray\u003cfloat\u003e m_wave;\n };\n \n-// とりあえず440Hzで生成\n-static Array\u003cOscillatorWavetable\u003e OscWaveTables =\n+class BandLimitedWaveTables\n {\n-\tOscillatorWavetable(2048, 440, WaveForm::Saw),\n-\tOscillatorWavetable(2048, 440, WaveForm::Sin),\n-\tOscillatorWavetable(2048, 440, WaveForm::Square),\n-\tOscillatorWavetable(SamplingFreq, 440, WaveForm::Noise),\n+public:\n+\n+\tBandLimitedWaveTables() = default;\n+\n+\tBandLimitedWaveTables(size_t tableCount, size_t waveResolution, WaveForm waveType)\n+\t{\n+\t\tm_waveTables.reserve(tableCount);\n+\t\tm_tableFreqs.reserve(tableCount);\n+\n+\t\tfor (size_t i = 0; i \u003c tableCount; ++i)\n+\t\t{\n+\t\t\tconst double rate = 1.0 * i / tableCount;\n+\t\t\tconst double freq = pow(2, Math::Lerp(m_minFreqLog, m_maxFreqLog, rate));\n+\n+\t\t\tm_waveTables.emplace_back(waveResolution, freq, waveType);\n+\t\t\tm_tableFreqs.push_back(static_cast\u003cfloat\u003e(freq));\n+\t\t}\n+\t}\n+\n+\tdouble get(double x, double freq) const\n+\t{\n+\t\tconst auto nextIt = std::upper_bound(m_tableFreqs.begin(), m_tableFreqs.end(), freq);\n+\t\tconst auto nextIndex = std::distance(m_tableFreqs.begin(), nextIt);\n+\t\tif (nextIndex == 0)\n+\t\t{\n+\t\t\treturn m_waveTables.front().get(x);\n+\t\t}\n+\t\tif (static_cast\u003csize_t\u003e(nextIndex) == m_tableFreqs.size())\n+\t\t{\n+\t\t\treturn m_waveTables.back().get(x);\n+\t\t}\n+\n+\t\tconst auto prevIndex = nextIndex - 1;\n+\t\tconst auto rate = Math::InvLerp(m_tableFreqs[prevIndex], m_tableFreqs[nextIndex], freq);\n+\t\treturn Math::Lerp(m_waveTables[prevIndex].get(x), m_waveTables[nextIndex].get(x), rate);\n+\t}\n+\n+private:\n+\n+\tdouble m_minFreqLog = log2(MinFreq);\n+\tdouble m_maxFreqLog = log2(MaxFreq);\n+\tArray\u003cOscillatorWavetable\u003e m_waveTables;\n+\tArray\u003cfloat\u003e m_tableFreqs;\n+};\n+\n+static Array\u003cBandLimitedWaveTables\u003e OscWaveTables =\n+{\n+\tBandLimitedWaveTables(80, 2048, WaveForm::Saw),\n+\tBandLimitedWaveTables(1, 2048, WaveForm::Sin),\n+\tBandLimitedWaveTables(80, 2048, WaveForm::Square),\n+\tBandLimitedWaveTables(1, SamplingFreq, WaveForm::Noise),\n };\n \n const auto SliderHeight = 36;\n@@ -261,7 +308,7 @@ public:\n \t\t\tconst auto envLevel = noteState.m_envelope.currentLevel() * noteState.m_velocity;\n \t\t\tconst auto frequency = NoteNumberToFrequency(noteNumber);\n \n-\t\t\tconst auto osc = OscWaveTables[m_oscIndex].get(m_time * frequency * 2_pi);\n+\t\t\tconst auto osc = OscWaveTables[m_oscIndex].get(m_time * frequency * 2_pi, frequency);\n \t\t\tconst auto w = static_cast\u003cfloat\u003e(osc * envLevel);\n \t\t\tsample.left += w;\n \t\t\tsample.right += w;\n@@ -309,6 +356,11 @@ public:\n \t\tm_noteState.clear();\n \t}\n \n+\tADSRConfig\u0026 adsr()\n+\t{\n+\t\treturn m_adsr;\n+\t}\n+\n private:\n \n \tstd::multimap\u003cint8_t, NoteState\u003e m_noteState;\n@@ -370,7 +422,8 @@ Wave RenderWave(Synthesizer\u0026 synth, const MidiData\u0026 midiData)\n \n void Main()\n {\n-\tauto midiDataOpt = LoadMidi(U\"short_loop.mid\");\n+\tWindow::Resize(1280, 720);\n+\tauto midiDataOpt = LoadMidi(U\"C1_B8.mid\");\n \tif (!midiDataOpt)\n \t{\n \t\t// ファイルが見つからない or 読み込みエラー\n@@ -380,17 +433,20 @@ void Main()\n \tconst MidiData\u0026 midiData = midiDataOpt.value();\n \n \tSynthesizer synth;\n+\tauto\u0026 adsr = synth.adsr();\n+\tadsr.attackTime = 0.02;\n+\tadsr.releaseTime = 0.02;\n \n \tAudio audio(RenderWave(synth, midiData));\n \taudio.play();\n \n+\tAudioVisualizer visualizer(Scene::Rect().stretched(-50), AudioVisualizer::Spectrogram, AudioVisualizer::LogScale);\n+\tvisualizer.setFreqRange(100, 20000); // [100, 20000] Hz\n+\tvisualizer.setSplRange(-120, -60); // [-120, -60] dB\n+\n \twhile (System::Update())\n \t{\n-\t\tVec2 pos(20, 20 - SliderHeight);\n-\n-\t\tsynth.updateGUI(pos);\n-\n-\t\tif (SimpleGUI::Button(U\"波形を再生成\", Vec2{ pos.x, pos.y += SliderHeight }))\n+\t\tif (KeySpace.down())\n \t\t{\n \t\t\taudio.stop();\n \t\t\tsynth.clear();\n@@ -398,5 +454,9 @@ void Main()\n \t\t\taudio = Audio(RenderWave(synth, midiData));\n \t\t\taudio.play();\n \t\t}\n+\n+\t\tvisualizer.setInputWave(audio);\n+\t\tvisualizer.updateFFT();\n+\t\tvisualizer.draw();\n \t}\n }\n```\n### ２.４ 波形生成をリアルタイムに行う\n```diff\n@@ -373,90 +373,94 @@ private:\n \tdouble m_time = 0;\n };\n \n-Wave RenderWave(Synthesizer\u0026 synth, const MidiData\u0026 midiData)\n+class AudioRenderer : public IAudioStream\n {\n-\tconst auto lengthOfSamples = static_cast\u003cint64\u003e(ceil(midiData.lengthOfTime() * Wave::DefaultSampleRate));\n+public:\n \n-\tWave wave(lengthOfSamples);\n+\tvoid setMidiData(const MidiData\u0026 midiData)\n+\t{\n+\t\tm_midiData = midiData;\n+\t}\n \n-\tfor (int64 i = 0; i \u003c lengthOfSamples; ++i)\n+\tvoid updateGUI(Vec2\u0026 pos)\n \t{\n-\t\tconst auto currentTime = 1.0 * i / wave.sampleRate();\n-\t\tconst auto nextTime = 1.0 * (i + 1) / wave.sampleRate();\n+\t\tm_synth.updateGUI(pos);\n+\t}\n \n-\t\tconst auto currentTick = midiData.secondsToTicks(currentTime);\n-\t\tconst auto nextTick = midiData.secondsToTicks(nextTime);\n+private:\n \n-\t\t// tick が進んだら MIDI イベントの処理を更新する\n-\t\tif (currentTick != nextTick)\n+\tvoid getAudio(float* left, float* right, const size_t samplesToWrite) override\n+\t{\n+\t\tfor (size_t i = 0; i \u003c samplesToWrite; ++i)\n \t\t{\n-\t\t\tfor (const auto\u0026 track : midiData.tracks())\n-\t\t\t{\n-\t\t\t\tif (track.isPercussionTrack())\n-\t\t\t\t{\n-\t\t\t\t\tcontinue;\n-\t\t\t\t}\n+\t\t\tconst double currentTime = 1.0 * m_readMIDIPos / SamplingFreq;\n+\t\t\tconst double nextTime = 1.0 * (m_readMIDIPos + 1) / SamplingFreq;\n \n-\t\t\t\t// 発生したノートオフイベントをシンセに登録\n-\t\t\t\tconst auto noteOffEvents = track.getMIDIEvent\u003cNoteOffEvent\u003e(currentTick, nextTick);\n-\t\t\t\tfor (auto\u0026 [tick, noteOff] : noteOffEvents)\n-\t\t\t\t{\n-\t\t\t\t\tsynth.noteOff(noteOff.note_number);\n-\t\t\t\t}\n+\t\t\tconst auto currentTick = m_midiData.secondsToTicks(currentTime);\n+\t\t\tconst auto nextTick = m_midiData.secondsToTicks(nextTime);\n \n-\t\t\t\t// 発生したノートオンイベントをシンセに登録\n-\t\t\t\tconst auto noteOnEvents = track.getMIDIEvent\u003cNoteOnEvent\u003e(currentTick, nextTick);\n-\t\t\t\tfor (auto\u0026 [tick, noteOn] : noteOnEvents)\n+\t\t\t// tick が進んだら MIDI イベントの処理を更新する\n+\t\t\tif (currentTick != nextTick)\n+\t\t\t{\n+\t\t\t\tfor (const auto\u0026 track : m_midiData.tracks())\n \t\t\t\t{\n-\t\t\t\t\tsynth.noteOn(noteOn.note_number, noteOn.velocity);\n+\t\t\t\t\tif (track.isPercussionTrack())\n+\t\t\t\t\t{\n+\t\t\t\t\t\tcontinue;\n+\t\t\t\t\t}\n+\n+\t\t\t\t\t// 発生したノートオフイベントをシンセに登録\n+\t\t\t\t\tconst auto noteOffEvents = track.getMIDIEvent\u003cNoteOffEvent\u003e(currentTick, nextTick);\n+\t\t\t\t\tfor (auto\u0026 [tick, noteOff] : noteOffEvents)\n+\t\t\t\t\t{\n+\t\t\t\t\t\tm_synth.noteOff(noteOff.note_number);\n+\t\t\t\t\t}\n+\n+\t\t\t\t\t// 発生したノートオンイベントをシンセに登録\n+\t\t\t\t\tconst auto noteOnEvents = track.getMIDIEvent\u003cNoteOnEvent\u003e(currentTick, nextTick);\n+\t\t\t\t\tfor (auto\u0026 [tick, noteOn] : noteOnEvents)\n+\t\t\t\t\t{\n+\t\t\t\t\t\tm_synth.noteOn(noteOn.note_number, noteOn.velocity);\n+\t\t\t\t\t}\n \t\t\t\t}\n \t\t\t}\n-\t\t}\n \n-\t\t// シンセを1サンプル更新して波形を書き込む\n-\t\twave[i] = synth.renderSample();\n+\t\t\tconst auto waveSample = m_synth.renderSample();\n+\n+\t\t\t*left++ = waveSample.left;\n+\t\t\t*right++ = waveSample.right;\n+\n+\t\t\t++m_readMIDIPos;\n+\t\t}\n \t}\n \n-\treturn wave;\n-}\n+\tbool hasEnded() override { return false; }\n+\tvoid rewind() override {}\n+\n+\tSynthesizer m_synth;\n+\tMidiData m_midiData;\n+\tsize_t m_readMIDIPos = 0;\n+};\n \n void Main()\n {\n-\tWindow::Resize(1280, 720);\n-\tauto midiDataOpt = LoadMidi(U\"C1_B8.mid\");\n+\tauto midiDataOpt = LoadMidi(U\"example/midi/test.mid\");\n \tif (!midiDataOpt)\n \t{\n \t\t// ファイルが見つからない or 読み込みエラー\n \t\treturn;\n \t}\n \n-\tconst MidiData\u0026 midiData = midiDataOpt.value();\n-\n-\tSynthesizer synth;\n-\tauto\u0026 adsr = synth.adsr();\n-\tadsr.attackTime = 0.02;\n-\tadsr.releaseTime = 0.02;\n+\tstd::shared_ptr\u003cAudioRenderer\u003e audioStream = std::make_shared\u003cAudioRenderer\u003e();\n+\taudioStream-\u003esetMidiData(midiDataOpt.value());\n \n-\tAudio audio(RenderWave(synth, midiData));\n+\tAudio audio(audioStream);\n \taudio.play();\n \n-\tAudioVisualizer visualizer(Scene::Rect().stretched(-50), AudioVisualizer::Spectrogram, AudioVisualizer::LogScale);\n-\tvisualizer.setFreqRange(100, 20000); // [100, 20000] Hz\n-\tvisualizer.setSplRange(-120, -60); // [-120, -60] dB\n-\n \twhile (System::Update())\n \t{\n-\t\tif (KeySpace.down())\n-\t\t{\n-\t\t\taudio.stop();\n-\t\t\tsynth.clear();\n-\n-\t\t\taudio = Audio(RenderWave(synth, midiData));\n-\t\t\taudio.play();\n-\t\t}\n+\t\tVec2 pos(20, 20 - SliderHeight);\n \n-\t\tvisualizer.setInputWave(audio);\n-\t\tvisualizer.updateFFT();\n-\t\tvisualizer.draw();\n+\t\taudioStream-\u003eupdateGUI(pos);\n \t}\n }\n```\n### ２.５ パフォーマンスをもう少し最適化する\n```diff\n@@ -59,7 +59,8 @@ public:\n \tOscillatorWavetable() = default;\n \n \tOscillatorWavetable(size_t resolution, double frequency, WaveForm waveType) :\n-\t\tm_wave(resolution)\n+\t\tm_wave(resolution),\n+\t\tm_xToIndex(resolution / 2_pi)\n \t{\n \t\tconst int mSaw = static_cast\u003cint\u003e(MaxFreq / frequency);\n \t\tconst int mSquare = static_cast\u003cint\u003e((MaxFreq + frequency) / (frequency * 2.0));\n@@ -89,16 +90,26 @@ public:\n \n \tdouble get(double x) const\n \t{\n-\t\tconst size_t resolution = m_wave.size();\n-\t\tconst double indexFloat = fmod(x * resolution / 2_pi, resolution);\n-\t\tconst int indexInt = static_cast\u003cint\u003e(indexFloat);\n-\t\tconst double rate = indexFloat - indexInt;\n-\t\treturn Math::Lerp(m_wave[indexInt], m_wave[(indexInt + 1) % resolution], rate);\n+\t\tauto indexFloat = x * m_xToIndex;\n+\t\tauto prevIndex = static_cast\u003csize_t\u003e(indexFloat);\n+\t\tif (m_wave.size() == prevIndex)\n+\t\t{\n+\t\t\tprevIndex -= m_wave.size();\n+\t\t\tindexFloat -= m_wave.size();\n+\t\t}\n+\t\tauto nextIndex = prevIndex + 1;\n+\t\tif (m_wave.size() == nextIndex)\n+\t\t{\n+\t\t\tnextIndex = 0;\n+\t\t}\n+\t\tconst auto x01 = indexFloat - prevIndex;\n+\t\treturn Math::Lerp(m_wave[prevIndex], m_wave[nextIndex], x01);\n \t}\n \n private:\n \n \tArray\u003cfloat\u003e m_wave;\n+\tdouble m_xToIndex = 0;\n };\n \n class BandLimitedWaveTables\n@@ -120,12 +131,22 @@ public:\n \t\t\tm_waveTables.emplace_back(waveResolution, freq, waveType);\n \t\t\tm_tableFreqs.push_back(static_cast\u003cfloat\u003e(freq));\n \t\t}\n+\n+\t\t{\n+\t\t\tm_indices.resize(2048);\n+\t\t\tm_freqToIndex = m_indices.size() / (1.0 * MaxFreq);\n+\t\t\tfor (int i = 0; i \u003c m_indices.size(); ++i)\n+\t\t\t{\n+\t\t\t\tconst float freq = static_cast\u003cfloat\u003e(i / m_freqToIndex);\n+\t\t\t\tconst auto nextIt = std::upper_bound(m_tableFreqs.begin(), m_tableFreqs.end(), freq);\n+\t\t\t\tm_indices[i] = static_cast\u003cuint32\u003e(nextIt - m_tableFreqs.begin());\n+\t\t\t}\n+\t\t}\n \t}\n \n \tdouble get(double x, double freq) const\n \t{\n-\t\tconst auto nextIt = std::upper_bound(m_tableFreqs.begin(), m_tableFreqs.end(), freq);\n-\t\tconst auto nextIndex = std::distance(m_tableFreqs.begin(), nextIt);\n+\t\tconst auto nextIndex = m_indices[static_cast\u003cint\u003e(freq * m_freqToIndex)];\n \t\tif (nextIndex == 0)\n \t\t{\n \t\t\treturn m_waveTables.front().get(x);\n@@ -146,6 +167,9 @@ private:\n \tdouble m_maxFreqLog = log2(MaxFreq);\n \tArray\u003cOscillatorWavetable\u003e m_waveTables;\n \tArray\u003cfloat\u003e m_tableFreqs;\n+\n+\tArray\u003cuint32\u003e m_indices;\n+\tdouble m_freqToIndex = 0;\n };\n \n static Array\u003cBandLimitedWaveTables\u003e OscWaveTables =\n@@ -279,6 +303,7 @@ float NoteNumberToFrequency(int8_t d)\n \n struct NoteState\n {\n+\tdouble m_phase = 0;\n \tfloat m_velocity = 1.f;\n \tEnvGenerator m_envelope;\n };\n@@ -308,14 +333,18 @@ public:\n \t\t\tconst auto envLevel = noteState.m_envelope.currentLevel() * noteState.m_velocity;\n \t\t\tconst auto frequency = NoteNumberToFrequency(noteNumber);\n \n-\t\t\tconst auto osc = OscWaveTables[m_oscIndex].get(m_time * frequency * 2_pi, frequency);\n+\t\t\tconst auto osc = OscWaveTables[m_oscIndex].get(noteState.m_phase, frequency);\n+\t\t\tnoteState.m_phase += deltaT * frequency * 2_pi;\n+\t\t\tif (Math::TwoPi \u003c noteState.m_phase)\n+\t\t\t{\n+\t\t\t\tnoteState.m_phase -= Math::TwoPi;\n+\t\t\t}\n+\n \t\t\tconst auto w = static_cast\u003cfloat\u003e(osc * envLevel);\n \t\t\tsample.left += w;\n \t\t\tsample.right += w;\n \t\t}\n \n-\t\tm_time += deltaT;\n-\n \t\treturn sample * static_cast\u003cfloat\u003e(m_amplitude);\n \t}\n \n@@ -369,69 +398,89 @@ private:\n \n \tdouble m_amplitude = 0.1;\n \tint m_oscIndex = 0;\n-\n-\tdouble m_time = 0;\n };\n \n class AudioRenderer : public IAudioStream\n {\n public:\n \n+\tAudioRenderer()\n+\t{\n+\t\t// 100ms分のバッファを確保する\n+\t\tconst size_t bufferSize = SamplingFreq / 10;\n+\t\tm_buffer.resize(bufferSize);\n+\t}\n+\n \tvoid setMidiData(const MidiData\u0026 midiData)\n \t{\n \t\tm_midiData = midiData;\n \t}\n \n-\tvoid updateGUI(Vec2\u0026 pos)\n+\tvoid bufferSample()\n \t{\n-\t\tm_synth.updateGUI(pos);\n-\t}\n+\t\tconst double currentTime = 1.0 * m_readMIDIPos / SamplingFreq;\n+\t\tconst double nextTime = 1.0 * (m_readMIDIPos + 1) / SamplingFreq;\n \n-private:\n+\t\tconst auto currentTick = m_midiData.secondsToTicks(currentTime);\n+\t\tconst auto nextTick = m_midiData.secondsToTicks(nextTime);\n \n-\tvoid getAudio(float* left, float* right, const size_t samplesToWrite) override\n-\t{\n-\t\tfor (size_t i = 0; i \u003c samplesToWrite; ++i)\n+\t\t// tick が進んだら MIDI イベントの処理を更新する\n+\t\tif (currentTick != nextTick)\n \t\t{\n-\t\t\tconst double currentTime = 1.0 * m_readMIDIPos / SamplingFreq;\n-\t\t\tconst double nextTime = 1.0 * (m_readMIDIPos + 1) / SamplingFreq;\n+\t\t\tfor (const auto\u0026 track : m_midiData.tracks())\n+\t\t\t{\n+\t\t\t\tif (track.isPercussionTrack())\n+\t\t\t\t{\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n \n-\t\t\tconst auto currentTick = m_midiData.secondsToTicks(currentTime);\n-\t\t\tconst auto nextTick = m_midiData.secondsToTicks(nextTime);\n+\t\t\t\t// 発生したノートオフイベントをシンセに登録\n+\t\t\t\tconst auto noteOffEvents = track.getMIDIEvent\u003cNoteOffEvent\u003e(currentTick, nextTick);\n+\t\t\t\tfor (auto\u0026 [tick, noteOff] : noteOffEvents)\n+\t\t\t\t{\n+\t\t\t\t\tm_synth.noteOff(noteOff.note_number);\n+\t\t\t\t}\n \n-\t\t\t// tick が進んだら MIDI イベントの処理を更新する\n-\t\t\tif (currentTick != nextTick)\n-\t\t\t{\n-\t\t\t\tfor (const auto\u0026 track : m_midiData.tracks())\n+\t\t\t\t// 発生したノートオンイベントをシンセに登録\n+\t\t\t\tconst auto noteOnEvents = track.getMIDIEvent\u003cNoteOnEvent\u003e(currentTick, nextTick);\n+\t\t\t\tfor (auto\u0026 [tick, noteOn] : noteOnEvents)\n \t\t\t\t{\n-\t\t\t\t\tif (track.isPercussionTrack())\n-\t\t\t\t\t{\n-\t\t\t\t\t\tcontinue;\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t// 発生したノートオフイベントをシンセに登録\n-\t\t\t\t\tconst auto noteOffEvents = track.getMIDIEvent\u003cNoteOffEvent\u003e(currentTick, nextTick);\n-\t\t\t\t\tfor (auto\u0026 [tick, noteOff] : noteOffEvents)\n-\t\t\t\t\t{\n-\t\t\t\t\t\tm_synth.noteOff(noteOff.note_number);\n-\t\t\t\t\t}\n-\n-\t\t\t\t\t// 発生したノートオンイベントをシンセに登録\n-\t\t\t\t\tconst auto noteOnEvents = track.getMIDIEvent\u003cNoteOnEvent\u003e(currentTick, nextTick);\n-\t\t\t\t\tfor (auto\u0026 [tick, noteOn] : noteOnEvents)\n-\t\t\t\t\t{\n-\t\t\t\t\t\tm_synth.noteOn(noteOn.note_number, noteOn.velocity);\n-\t\t\t\t\t}\n+\t\t\t\t\tm_synth.noteOn(noteOn.note_number, noteOn.velocity);\n \t\t\t\t}\n \t\t\t}\n+\t\t}\n+\n+\t\tconst size_t writeIndex = m_bufferWritePos % m_buffer.size();\n+\n+\t\tm_buffer[writeIndex] = m_synth.renderSample();\n+\n+\t\t++m_bufferWritePos;\n+\t\t++m_readMIDIPos;\n+\t}\n \n-\t\t\tconst auto waveSample = m_synth.renderSample();\n+\tbool bufferCompleted() const\n+\t{\n+\t\treturn m_bufferReadPos + m_buffer.size() - 1 \u003c m_bufferWritePos;\n+\t}\n \n-\t\t\t*left++ = waveSample.left;\n-\t\t\t*right++ = waveSample.right;\n+\tvoid updateGUI(Vec2\u0026 pos)\n+\t{\n+\t\tm_synth.updateGUI(pos);\n+\t}\n+\n+private:\n+\n+\tvoid getAudio(float* left, float* right, const size_t samplesToWrite) override\n+\t{\n+\t\tfor (size_t i = 0; i \u003c samplesToWrite; ++i)\n+\t\t{\n+\t\t\tconst auto\u0026 readSample = m_buffer[(m_bufferReadPos + i) % m_buffer.size()];\n \n-\t\t\t++m_readMIDIPos;\n+\t\t\t*left++ = readSample.left;\n+\t\t\t*right++ = readSample.right;\n \t\t}\n+\n+\t\tm_bufferReadPos += samplesToWrite;\n \t}\n \n \tbool hasEnded() override { return false; }\n@@ -439,7 +488,10 @@ private:\n \n \tSynthesizer m_synth;\n \tMidiData m_midiData;\n+\tArray\u003cWaveSample\u003e m_buffer;\n \tsize_t m_readMIDIPos = 0;\n+\tsize_t m_bufferReadPos = 0;\n+\tsize_t m_bufferWritePos = 0;\n };\n \n void Main()\n@@ -454,6 +506,23 @@ void Main()\n \tstd::shared_ptr\u003cAudioRenderer\u003e audioStream = std::make_shared\u003cAudioRenderer\u003e();\n \taudioStream-\u003esetMidiData(midiDataOpt.value());\n \n+\tbool isRunning = true;\n+\n+\tauto renderUpdate = [\u0026]()\n+\t{\n+\t\twhile (isRunning)\n+\t\t{\n+\t\t\twhile (!audioStream-\u003ebufferCompleted())\n+\t\t\t{\n+\t\t\t\taudioStream-\u003ebufferSample();\n+\t\t\t}\n+\n+\t\t\tstd::this_thread::sleep_for(std::chrono::milliseconds(1));\n+\t\t}\n+\t};\n+\n+\tstd::thread audioRenderThread(renderUpdate);\n+\n \tAudio audio(audioStream);\n \taudio.play();\n \n@@ -463,4 +532,7 @@ void Main()\n \n \t\taudioStream-\u003eupdateGUI(pos);\n \t}\n+\n+\tisRunning = false;\n+\taudioRenderThread.join();\n }\n```\n## その３：波形を合成して音を作る\n### ３.０ 可視化機能を実装する\n```diff\n@@ -390,6 +390,24 @@ public:\n \t\treturn m_adsr;\n \t}\n \n+\tint oscIndex() const\n+\t{\n+\t\treturn m_oscIndex;\n+\t}\n+\tvoid setOscIndex(int oscIndex)\n+\t{\n+\t\tm_oscIndex = oscIndex;\n+\t}\n+\n+\tdouble amplitude() const\n+\t{\n+\t\treturn m_amplitude;\n+\t}\n+\tvoid setAmplitude(double amplitude)\n+\t{\n+\t\tm_amplitude = amplitude;\n+\t}\n+\n private:\n \n \tstd::multimap\u003cint8_t, NoteState\u003e m_noteState;\n@@ -468,6 +486,26 @@ public:\n \t\tm_synth.updateGUI(pos);\n \t}\n \n+\tconst Array\u003cWaveSample\u003e\u0026 buffer() const\n+\t{\n+\t\treturn m_buffer;\n+\t}\n+\n+\tsize_t bufferReadPos() const\n+\t{\n+\t\treturn m_bufferReadPos;\n+\t}\n+\n+\tsize_t playingMIDIPos() const\n+\t{\n+\t\treturn m_readMIDIPos - (m_bufferWritePos - m_bufferReadPos);\n+\t}\n+\n+\tSynthesizer\u0026 synth()\n+\t{\n+\t\treturn m_synth;\n+\t}\n+\n private:\n \n \tvoid getAudio(float* left, float* right, const size_t samplesToWrite) override\n@@ -496,6 +534,8 @@ private:\n \n void Main()\n {\n+\tWindow::Resize(1600, 900);\n+\n \tauto midiDataOpt = LoadMidi(U\"example/midi/test.mid\");\n \tif (!midiDataOpt)\n \t{\n@@ -503,9 +543,24 @@ void Main()\n \t\treturn;\n \t}\n \n+\tAudioVisualizer visualizer;\n+\tvisualizer.setSplRange(-60, -30);\n+\tvisualizer.setWindowType(AudioVisualizer::Hamming);\n+\tvisualizer.setDrawScore(NoteNumber::C_2, NoteNumber::B_7);\n+\tvisualizer.setDrawArea(Scene::Rect());\n+\n \tstd::shared_ptr\u003cAudioRenderer\u003e audioStream = std::make_shared\u003cAudioRenderer\u003e();\n \taudioStream-\u003esetMidiData(midiDataOpt.value());\n \n+\tauto\u0026 synth = audioStream-\u003esynth();\n+\tsynth.setOscIndex(static_cast\u003cint\u003e(WaveForm::Sin));\n+\n+\tauto\u0026 adsr = synth.adsr();\n+\tadsr.attackTime = 0.01;\n+\tadsr.decayTime = 0.0;\n+\tadsr.sustainLevel = 1.0;\n+\tadsr.releaseTime = 0.01;\n+\n \tbool isRunning = true;\n \n \tauto renderUpdate = [\u0026]()\n@@ -526,11 +581,43 @@ void Main()\n \tAudio audio(audioStream);\n \taudio.play();\n \n+\tbool showGUI = true;\n \twhile (System::Update())\n \t{\n \t\tVec2 pos(20, 20 - SliderHeight);\n \n-\t\taudioStream-\u003eupdateGUI(pos);\n+\t\t// visualizerの更新\n+\t\t{\n+\t\t\tauto\u0026 visualizeBuffer = visualizer.inputWave();\n+\t\t\tvisualizeBuffer.fill(0);\n+\n+\t\t\tconst auto\u0026 streamBuffer = audioStream-\u003ebuffer();\n+\t\t\tconst auto readStartPos = audioStream-\u003ebufferReadPos();\n+\n+\t\t\tconst auto fftInputSize = Min(visualizeBuffer.size(), streamBuffer.size());\n+\n+\t\t\tfor (size_t i = 0; i \u003c fftInputSize; ++i)\n+\t\t\t{\n+\t\t\t\tconst auto inputIndex = (readStartPos + i) % streamBuffer.size();\n+\t\t\t\tconst auto\u0026 sample = streamBuffer[inputIndex];\n+\t\t\t\tvisualizeBuffer[i] = (sample.left + sample.right) * 0.5f;\n+\t\t\t}\n+\n+\t\t\tvisualizer.updateFFT(fftInputSize);\n+\n+\t\t\tconst auto currentTime = 1.0 * audioStream-\u003eplayingMIDIPos() / SamplingFreq;\n+\t\t\tvisualizer.drawScore(midiDataOpt.value(), currentTime);\n+\t\t}\n+\n+\t\tif (KeyG.down())\n+\t\t{\n+\t\t\tshowGUI = !showGUI;\n+\t\t}\n+\n+\t\tif (showGUI)\n+\t\t{\n+\t\t\taudioStream-\u003eupdateGUI(pos);\n+\t\t}\n \t}\n \n \tisRunning = false;\n```\n### ３.１ パンとピッチシフト\n```diff\n@@ -326,12 +326,14 @@ public:\n \t\t// リリースが終了したノートを削除する\n \t\tstd::erase_if(m_noteState, [\u0026](const auto\u0026 noteState) { return noteState.second.m_envelope.isReleased(m_adsr); });\n \n+\t\tconst auto pitch = pow(2.0, m_pitchShift / 12.0);\n+\n \t\t// 入力中の波形を加算して書き込む\n \t\tWaveSample sample(0, 0);\n \t\tfor (auto\u0026 [noteNumber, noteState] : m_noteState)\n \t\t{\n \t\t\tconst auto envLevel = noteState.m_envelope.currentLevel() * noteState.m_velocity;\n-\t\t\tconst auto frequency = NoteNumberToFrequency(noteNumber);\n+\t\t\tconst auto frequency = NoteNumberToFrequency(noteNumber) * pitch;\n \n \t\t\tconst auto osc = OscWaveTables[m_oscIndex].get(noteState.m_phase, frequency);\n \t\t\tnoteState.m_phase += deltaT * frequency * 2_pi;\n@@ -345,6 +347,9 @@ public:\n \t\t\tsample.right += w;\n \t\t}\n \n+\t\tsample.left *= static_cast\u003cfloat\u003e(cos(Math::HalfPi * m_pan));\n+\t\tsample.right *= static_cast\u003cfloat\u003e(sin(Math::HalfPi * m_pan));\n+\n \t\treturn sample * static_cast\u003cfloat\u003e(m_amplitude);\n \t}\n \n@@ -375,8 +380,15 @@ public:\n \tvoid updateGUI(Vec2\u0026 pos)\n \t{\n \t\tSimpleGUI::Slider(U\"amplitude : {:.2f}\"_fmt(m_amplitude), m_amplitude, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n+\t\tSimpleGUI::Slider(U\"pan : {:.2f}\"_fmt(m_pan), m_pan, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n \t\tSliderInt(U\"oscillator : {}\"_fmt(m_oscIndex), m_oscIndex, 0, 3, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n \n+\t\tif (SimpleGUI::Slider(U\"pitchShift : {:.2f}\"_fmt(m_pitchShift), m_pitchShift, -24.0, 24.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth)\n+\t\t\t \u0026\u0026 KeyControl.pressed())\n+\t\t{\n+\t\t\tm_pitchShift = Math::Round(m_pitchShift);\n+\t\t}\n+\n \t\tm_adsr.updateGUI(pos);\n \t}\n \n@@ -408,6 +420,24 @@ public:\n \t\tm_amplitude = amplitude;\n \t}\n \n+\tdouble pan() const\n+\t{\n+\t\treturn m_pan;\n+\t}\n+\tvoid setPan(double pan)\n+\t{\n+\t\tm_pan = pan;\n+\t}\n+\n+\tdouble pitchShift() const\n+\t{\n+\t\treturn m_pitchShift;\n+\t}\n+\tvoid setPitchShift(double pitchShift)\n+\t{\n+\t\tm_pitchShift = pitchShift;\n+\t}\n+\n private:\n \n \tstd::multimap\u003cint8_t, NoteState\u003e m_noteState;\n@@ -415,6 +445,8 @@ private:\n \tADSRConfig m_adsr;\n \n \tdouble m_amplitude = 0.1;\n+\tdouble m_pan = 0.5;\n+\tdouble m_pitchShift = 0.0;\n \tint m_oscIndex = 0;\n };\n \n```\n### ３.２ ユニゾン\n```diff\n@@ -301,9 +301,22 @@ float NoteNumberToFrequency(int8_t d)\n \treturn 440.0f * pow(2.0f, (d - 69) / 12.0f);\n }\n \n+static constexpr uint32 MaxUnisonSize = 16;\n+static const double Semitone = pow(2.0, 1.0 / 12.0) - 1.0;\n+\n struct NoteState\n {\n-\tdouble m_phase = 0;\n+\tNoteState()\n+\t{\n+\t\tfor (auto\u0026 initialPhase : m_phase)\n+\t\t{\n+\t\t\t// 初期位相をランダムに設定する\n+\t\t\tinitialPhase = Random(0.0, 2_pi);\n+\t\t}\n+\t}\n+\n+\t// ユニゾン波形ごとに進む周波数が異なるので、別々に位相を管理する\n+\tstd::array\u003cdouble, MaxUnisonSize\u003e m_phase = {};\n \tfloat m_velocity = 1.f;\n \tEnvGenerator m_envelope;\n };\n@@ -312,6 +325,12 @@ class Synthesizer\n {\n public:\n \n+\tSynthesizer()\n+\t{\n+\t\tm_detunePitch.fill(1);\n+\t\tm_unisonPan.fill(Float2::One().normalize());\n+\t}\n+\n \t// 1サンプル波形を生成して返す\n \tWaveSample renderSample()\n \t{\n@@ -330,27 +349,34 @@ public:\n \n \t\t// 入力中の波形を加算して書き込む\n \t\tWaveSample sample(0, 0);\n+\n \t\tfor (auto\u0026 [noteNumber, noteState] : m_noteState)\n \t\t{\n \t\t\tconst auto envLevel = noteState.m_envelope.currentLevel() * noteState.m_velocity;\n \t\t\tconst auto frequency = NoteNumberToFrequency(noteNumber) * pitch;\n \n-\t\t\tconst auto osc = OscWaveTables[m_oscIndex].get(noteState.m_phase, frequency);\n-\t\t\tnoteState.m_phase += deltaT * frequency * 2_pi;\n-\t\t\tif (Math::TwoPi \u003c noteState.m_phase)\n+\t\t\tfor (int d = 0; d \u003c m_unisonCount; ++d)\n \t\t\t{\n-\t\t\t\tnoteState.m_phase -= Math::TwoPi;\n-\t\t\t}\n+\t\t\t\tconst auto detuneFrequency = frequency * m_detunePitch[d];\n+\t\t\t\tauto\u0026 phase = noteState.m_phase[d];\n \n-\t\t\tconst auto w = static_cast\u003cfloat\u003e(osc * envLevel);\n-\t\t\tsample.left += w;\n-\t\t\tsample.right += w;\n+\t\t\t\tconst auto osc = OscWaveTables[m_oscIndex].get(phase, detuneFrequency);\n+\t\t\t\tphase += deltaT * detuneFrequency * Math::TwoPiF;\n+\t\t\t\tif (Math::TwoPi \u003c phase)\n+\t\t\t\t{\n+\t\t\t\t\tphase -= Math::TwoPi;\n+\t\t\t\t}\n+\n+\t\t\t\tconst auto w = static_cast\u003cfloat\u003e(osc * envLevel);\n+\t\t\t\tsample.left += w * m_unisonPan[d].x;\n+\t\t\t\tsample.right += w * m_unisonPan[d].y;\n+\t\t\t}\n \t\t}\n \n \t\tsample.left *= static_cast\u003cfloat\u003e(cos(Math::HalfPi * m_pan));\n \t\tsample.right *= static_cast\u003cfloat\u003e(sin(Math::HalfPi * m_pan));\n \n-\t\treturn sample * static_cast\u003cfloat\u003e(m_amplitude);\n+\t\treturn sample * static_cast\u003cfloat\u003e(m_amplitude / sqrt(m_unisonCount));\n \t}\n \n \tvoid noteOn(int8_t noteNumber, int8_t velocity)\n@@ -389,6 +415,16 @@ public:\n \t\t\tm_pitchShift = Math::Round(m_pitchShift);\n \t\t}\n \n+\t\tbool unisonUpdated = false;\n+\t\tunisonUpdated = SliderInt(U\"unisonCount : {}\"_fmt(m_unisonCount), m_unisonCount, 1, 16, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth) || unisonUpdated;\n+\t\tunisonUpdated = SimpleGUI::Slider(U\"detune : {:.2f}\"_fmt(m_detune), m_detune, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth) || unisonUpdated;\n+\t\tunisonUpdated = SimpleGUI::Slider(U\"spread : {:.2f}\"_fmt(m_spread), m_spread, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth) || unisonUpdated;\n+\n+\t\tif (unisonUpdated)\n+\t\t{\n+\t\t\tupdateUnisonParam();\n+\t\t}\n+\n \t\tm_adsr.updateGUI(pos);\n \t}\n \n@@ -438,8 +474,63 @@ public:\n \t\tm_pitchShift = pitchShift;\n \t}\n \n+\tint unisonCount() const\n+\t{\n+\t\treturn m_unisonCount;\n+\t}\n+\tvoid setUnisonCount(int unisonCount)\n+\t{\n+\t\tm_unisonCount = unisonCount;\n+\t\tupdateUnisonParam();\n+\t}\n+\n+\tdouble detune() const\n+\t{\n+\t\treturn m_detune;\n+\t}\n+\tvoid setDetune(double detune)\n+\t{\n+\t\tm_detune = detune;\n+\t\tupdateUnisonParam();\n+\t}\n+\n+\tdouble spread() const\n+\t{\n+\t\treturn m_spread;\n+\t}\n+\tvoid setSpread(double spread)\n+\t{\n+\t\tm_spread = spread;\n+\t\tupdateUnisonParam();\n+\t}\n+\n private:\n \n+\tvoid updateUnisonParam()\n+\t{\n+\t\t// ユニゾンなし\n+\t\tif (m_unisonCount == 1)\n+\t\t{\n+\t\t\tm_detunePitch.fill(1);\n+\t\t\tm_unisonPan.fill(Float2::One().normalize());\n+\t\t\treturn;\n+\t\t}\n+\n+\t\t// ユニゾンあり\n+\t\tfor (int d = 0; d \u003c m_unisonCount; ++d)\n+\t\t{\n+\t\t\t// 各波形の位置を[-1, 1]で計算する\n+\t\t\tconst auto detunePos = Math::Lerp(-1.0, 1.0, 1.0 * d / (m_unisonCount - 1));\n+\n+\t\t\t// 現在の周波数から最大で Semitone * m_detune だけピッチシフトする\n+\t\t\tm_detunePitch[d] = static_cast\u003cfloat\u003e(1.0 + Semitone * m_detune * detunePos);\n+\n+\t\t\t// Math::QuarterPi が中央\n+\t\t\tconst auto unisonAngle = Math::QuarterPi * (1.0 + detunePos * m_spread);\n+\t\t\tm_unisonPan[d] = Float2(cos(unisonAngle), sin(unisonAngle));\n+\t\t}\n+\t}\n+\n \tstd::multimap\u003cint8_t, NoteState\u003e m_noteState;\n \n \tADSRConfig m_adsr;\n@@ -448,6 +539,13 @@ private:\n \tdouble m_pan = 0.5;\n \tdouble m_pitchShift = 0.0;\n \tint m_oscIndex = 0;\n+\n+\tint m_unisonCount = 1;\n+\tdouble m_detune = 0;\n+\tdouble m_spread = 1.0;\n+\n+\tstd::array\u003cfloat, MaxUnisonSize\u003e m_detunePitch;\n+\tstd::array\u003cFloat2, MaxUnisonSize\u003e m_unisonPan;\n };\n \n class AudioRenderer : public IAudioStream\n@@ -568,7 +666,7 @@ void Main()\n {\n \tWindow::Resize(1600, 900);\n \n-\tauto midiDataOpt = LoadMidi(U\"example/midi/test.mid\");\n+\tauto midiDataOpt = LoadMidi(U\"C5_B8.mid\");\n \tif (!midiDataOpt)\n \t{\n \t\t// ファイルが見つからない or 読み込みエラー\n@@ -578,14 +676,17 @@ void Main()\n \tAudioVisualizer visualizer;\n \tvisualizer.setSplRange(-60, -30);\n \tvisualizer.setWindowType(AudioVisualizer::Hamming);\n-\tvisualizer.setDrawScore(NoteNumber::C_2, NoteNumber::B_7);\n-\tvisualizer.setDrawArea(Scene::Rect());\n+\tvisualizer.setFreqRange(300, 10000);\n+\tvisualizer.setDrawArea(Scene::Rect().stretched(-50));\n \n \tstd::shared_ptr\u003cAudioRenderer\u003e audioStream = std::make_shared\u003cAudioRenderer\u003e();\n \taudioStream-\u003esetMidiData(midiDataOpt.value());\n \n \tauto\u0026 synth = audioStream-\u003esynth();\n-\tsynth.setOscIndex(static_cast\u003cint\u003e(WaveForm::Sin));\n+\tsynth.setOscIndex(static_cast\u003cint\u003e(WaveForm::Saw));\n+\tsynth.setUnisonCount(4);\n+\tsynth.setDetune(0.5);\n+\tsynth.setSpread(0.0);\n \n \tauto\u0026 adsr = synth.adsr();\n \tadsr.attackTime = 0.01;\n@@ -637,8 +738,7 @@ void Main()\n \n \t\t\tvisualizer.updateFFT(fftInputSize);\n \n-\t\t\tconst auto currentTime = 1.0 * audioStream-\u003eplayingMIDIPos() / SamplingFreq;\n-\t\t\tvisualizer.drawScore(midiDataOpt.value(), currentTime);\n+\t\t\tvisualizer.draw();\n \t\t}\n \n \t\tif (KeyG.down())\n```\n### ３.３ モノフォニックとポリフォニック\n```diff\n@@ -189,6 +189,7 @@ struct ADSRConfig\n \tdouble attackTime = 0.01;\n \tdouble decayTime = 0.01;\n \tdouble sustainLevel = 0.6;\n+\tdouble sustainResetTime = 0.05;\n \tdouble releaseTime = 0.4;\n \n \tvoid updateGUI(Vec2\u0026 pos)\n@@ -222,6 +223,7 @@ public:\n \t{\n \t\tif (m_state != State::Release)\n \t\t{\n+\t\t\tm_prevStateLevel = m_currentLevel;\n \t\t\tm_elapsed = 0;\n \t\t\tm_state = State::Release;\n \t\t}\n@@ -229,6 +231,7 @@ public:\n \n \tvoid reset(State state)\n \t{\n+\t\tm_prevStateLevel = m_currentLevel;\n \t\tm_elapsed = 0;\n \t\tm_state = state;\n \t}\n@@ -240,9 +243,10 @@ public:\n \t\tcase State::Attack: // 0.0 から 1.0 まで attackTime かけて増幅する\n \t\t\tif (m_elapsed \u003c adsr.attackTime)\n \t\t\t{\n-\t\t\t\tm_currentLevel = m_elapsed / adsr.attackTime;\n+\t\t\t\tm_currentLevel = Math::Lerp(m_prevStateLevel, 1.0, m_elapsed / adsr.attackTime);\n \t\t\t\tbreak;\n \t\t\t}\n+\t\t\tm_prevStateLevel = m_currentLevel;\n \t\t\tm_elapsed -= adsr.attackTime;\n \t\t\tm_state = State::Decay;\n \t\t\t[[fallthrough]]; // Decay処理にそのまま続く\n@@ -250,21 +254,28 @@ public:\n \t\tcase State::Decay: // 1.0 から sustainLevel まで decayTime かけて減衰する\n \t\t\tif (m_elapsed \u003c adsr.decayTime)\n \t\t\t{\n-\t\t\t\tm_currentLevel = Math::Lerp(1.0, adsr.sustainLevel, m_elapsed / adsr.decayTime);\n+\t\t\t\tm_currentLevel = Math::Lerp(m_prevStateLevel, adsr.sustainLevel, m_elapsed / adsr.decayTime);\n \t\t\t\tbreak;\n \t\t\t}\n+\t\t\tm_prevStateLevel = m_currentLevel;\n \t\t\tm_elapsed -= adsr.decayTime;\n \t\t\tm_state = State::Sustain;\n \t\t\t[[fallthrough]]; // Sustain処理にそのまま続く\n \n-\n \t\tcase State::Sustain: // ノートオンの間 sustainLevel を維持する\n-\t\t\tm_currentLevel = adsr.sustainLevel;\n+\t\t\tif (m_elapsed \u003c adsr.sustainResetTime)\n+\t\t\t{\n+\t\t\t\tm_currentLevel = Math::Lerp(m_prevStateLevel, adsr.sustainLevel, m_elapsed / adsr.sustainResetTime);\n+\t\t\t}\n+\t\t\telse\n+\t\t\t{\n+\t\t\t\tm_currentLevel = adsr.sustainLevel;\n+\t\t\t}\n \t\t\tbreak;\n \n \t\tcase State::Release: // sustainLevel から 0.0 まで releaseTime かけて減衰する\n \t\t\tm_currentLevel = m_elapsed \u003c adsr.releaseTime\n-\t\t\t\t? Math::Lerp(adsr.sustainLevel, 0.0, m_elapsed / adsr.releaseTime)\n+\t\t\t\t? Math::Lerp(m_prevStateLevel, 0.0, m_elapsed / adsr.releaseTime)\n \t\t\t\t: 0.0;\n \t\t\tbreak;\n \n@@ -294,6 +305,7 @@ private:\n \tState m_state = State::Attack;\n \tdouble m_elapsed = 0; // ステート変更からの経過秒数\n \tdouble m_currentLevel = 0; // 現在のレベル [0, 1]\n+\tdouble m_prevStateLevel = 0; // ステート変更前のレベル [0, 1]\n };\n \n float NoteNumberToFrequency(int8_t d)\n@@ -381,9 +393,24 @@ public:\n \n \tvoid noteOn(int8_t noteNumber, int8_t velocity)\n \t{\n-\t\tNoteState noteState;\n-\t\tnoteState.m_velocity = velocity / 127.0f;\n-\t\tm_noteState.emplace(noteNumber, noteState);\n+\t\tif (!m_mono || m_noteState.empty())\n+\t\t{\n+\t\t\tNoteState noteState;\n+\t\t\tnoteState.m_velocity = velocity / 127.0f;\n+\t\t\tm_noteState.emplace(noteNumber, noteState);\n+\t\t}\n+\t\telse\n+\t\t{\n+\t\t\tauto [key, oldState] = *m_noteState.begin();\n+\n+\t\t\t// ノート番号が同じとは限らないので一回消して作り直す\n+\t\t\tm_noteState.clear();\n+\n+\t\t\tNoteState noteState = oldState;\n+\t\t\tnoteState.m_velocity = velocity / 127.0f;\n+\t\t\tnoteState.m_envelope.reset(m_legato ? EnvGenerator::State::Sustain : EnvGenerator::State::Attack);\n+\t\t\tm_noteState.emplace(noteNumber, noteState);\n+\t\t}\n \t}\n \n \tvoid noteOff(int8_t noteNumber)\n@@ -426,6 +453,20 @@ public:\n \t\t}\n \n \t\tm_adsr.updateGUI(pos);\n+\n+\t\tconst int marginWidth = 32;\n+\n+\t\t{\n+\t\t\tpos.y += SliderHeight;\n+\t\t\tRectF(pos, LabelWidth + SliderWidth, SliderHeight * (m_mono ? 2 : 1)).draw();\n+\t\t\tSimpleGUI::CheckBox(m_mono, U\"mono\", pos);\n+\t\t\tif (m_mono)\n+\t\t\t{\n+\t\t\t\tpos.x += marginWidth;\n+\t\t\t\tSimpleGUI::CheckBox(m_legato, U\"legato\", Vec2(pos.x, pos.y += SliderHeight));\n+\t\t\t\tpos.x -= marginWidth;\n+\t\t\t}\n+\t\t}\n \t}\n \n \tvoid clear()\n@@ -504,6 +545,24 @@ public:\n \t\tupdateUnisonParam();\n \t}\n \n+\tbool mono() const\n+\t{\n+\t\treturn m_mono;\n+\t}\n+\tvoid setMono(bool mono)\n+\t{\n+\t\tm_mono = mono;\n+\t}\n+\n+\tbool legato() const\n+\t{\n+\t\treturn m_legato;\n+\t}\n+\tvoid setLegato(bool legato)\n+\t{\n+\t\tm_legato = legato;\n+\t}\n+\n private:\n \n \tvoid updateUnisonParam()\n@@ -544,6 +603,9 @@ private:\n \tdouble m_detune = 0;\n \tdouble m_spread = 1.0;\n \n+\tbool m_mono = false;\n+\tbool m_legato = false;\n+\n \tstd::array\u003cfloat, MaxUnisonSize\u003e m_detunePitch;\n \tstd::array\u003cFloat2, MaxUnisonSize\u003e m_unisonPan;\n };\n@@ -666,7 +728,7 @@ void Main()\n {\n \tWindow::Resize(1600, 900);\n \n-\tauto midiDataOpt = LoadMidi(U\"C5_B8.mid\");\n+\tauto midiDataOpt = LoadMidi(U\"glide_test.mid\");\n \tif (!midiDataOpt)\n \t{\n \t\t// ファイルが見つからない or 読み込みエラー\n@@ -676,22 +738,23 @@ void Main()\n \tAudioVisualizer visualizer;\n \tvisualizer.setSplRange(-60, -30);\n \tvisualizer.setWindowType(AudioVisualizer::Hamming);\n-\tvisualizer.setFreqRange(300, 10000);\n-\tvisualizer.setDrawArea(Scene::Rect().stretched(-50));\n+\tvisualizer.setShowPastNotes(false);\n+\tvisualizer.setDrawScore(NoteNumber::C_2, NoteNumber::B_7);\n+\tvisualizer.setDrawArea(Scene::Rect());\n \n \tstd::shared_ptr\u003cAudioRenderer\u003e audioStream = std::make_shared\u003cAudioRenderer\u003e();\n \taudioStream-\u003esetMidiData(midiDataOpt.value());\n \n \tauto\u0026 synth = audioStream-\u003esynth();\n-\tsynth.setOscIndex(static_cast\u003cint\u003e(WaveForm::Saw));\n-\tsynth.setUnisonCount(4);\n-\tsynth.setDetune(0.5);\n-\tsynth.setSpread(0.0);\n+\tsynth.setOscIndex(static_cast\u003cint\u003e(WaveForm::Sin));\n+\tsynth.setMono(true);\n+\tsynth.setLegato(false);\n+\tsynth.setAmplitude(0.4);\n \n \tauto\u0026 adsr = synth.adsr();\n \tadsr.attackTime = 0.01;\n-\tadsr.decayTime = 0.0;\n-\tadsr.sustainLevel = 1.0;\n+\tadsr.decayTime = 0.1;\n+\tadsr.sustainLevel = 0.2;\n \tadsr.releaseTime = 0.01;\n \n \tbool isRunning = true;\n@@ -738,7 +801,8 @@ void Main()\n \n \t\t\tvisualizer.updateFFT(fftInputSize);\n \n-\t\t\tvisualizer.draw();\n+\t\t\tconst auto currentTime = 1.0 * audioStream-\u003eplayingMIDIPos() / SamplingFreq;\n+\t\t\tvisualizer.drawScore(midiDataOpt.value(), currentTime);\n \t\t}\n \n \t\tif (KeyG.down())\n```\n### ３.４ グライド（ポルタメント）\n```diff\n@@ -364,8 +364,22 @@ public:\n \n \t\tfor (auto\u0026 [noteNumber, noteState] : m_noteState)\n \t\t{\n+\t\t\tconst auto targetFreq = NoteNumberToFrequency(noteNumber);\n+\n+\t\t\tif (m_mono \u0026\u0026 m_glide)\n+\t\t\t{\n+\t\t\t\tconst double targetScale = targetFreq / m_startGlideFreq;\n+\t\t\t\tconst double rate = Saturate(m_glideElapsed / m_glideTime);\n+\t\t\t\tm_currentFreq = m_startGlideFreq * pow(targetScale, rate);\n+\t\t\t\tm_glideElapsed += deltaT;\n+\t\t\t}\n+\t\t\telse\n+\t\t\t{\n+\t\t\t\tm_currentFreq = targetFreq;\n+\t\t\t}\n+\n \t\t\tconst auto envLevel = noteState.m_envelope.currentLevel() * noteState.m_velocity;\n-\t\t\tconst auto frequency = NoteNumberToFrequency(noteNumber) * pitch;\n+\t\t\tconst auto frequency = m_currentFreq * pitch;\n \n \t\t\tfor (int d = 0; d \u003c m_unisonCount; ++d)\n \t\t\t{\n@@ -411,6 +425,12 @@ public:\n \t\t\tnoteState.m_envelope.reset(m_legato ? EnvGenerator::State::Sustain : EnvGenerator::State::Attack);\n \t\t\tm_noteState.emplace(noteNumber, noteState);\n \t\t}\n+\n+\t\tif (m_mono \u0026\u0026 m_glide)\n+\t\t{\n+\t\t\tm_startGlideFreq = m_currentFreq;\n+\t\t\tm_glideElapsed = 0;\n+\t\t}\n \t}\n \n \tvoid noteOff(int8_t noteNumber)\n@@ -458,12 +478,15 @@ public:\n \n \t\t{\n \t\t\tpos.y += SliderHeight;\n-\t\t\tRectF(pos, LabelWidth + SliderWidth, SliderHeight * (m_mono ? 2 : 1)).draw();\n+\t\t\tRectF(pos, LabelWidth + SliderWidth, SliderHeight * (m_mono ? 3 : 1)).draw();\n \t\t\tSimpleGUI::CheckBox(m_mono, U\"mono\", pos);\n \t\t\tif (m_mono)\n \t\t\t{\n+\t\t\t\tconst auto legatoWidth = SimpleGUI::CheckBoxRegion(U\"legato\", {}).w;\n \t\t\t\tpos.x += marginWidth;\n \t\t\t\tSimpleGUI::CheckBox(m_legato, U\"legato\", Vec2(pos.x, pos.y += SliderHeight));\n+\t\t\t\tSimpleGUI::CheckBox(m_glide, U\"glide\", Vec2(pos.x + legatoWidth, pos.y));\n+\t\t\t\tSimpleGUI::Slider(U\"glideTime : {:.2f}\"_fmt(m_glideTime), m_glideTime, 0.001, 0.5, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth - marginWidth, SliderWidth);\n \t\t\t\tpos.x -= marginWidth;\n \t\t\t}\n \t\t}\n@@ -563,6 +586,24 @@ public:\n \t\tm_legato = legato;\n \t}\n \n+\tbool glide() const\n+\t{\n+\t\treturn m_glide;\n+\t}\n+\tvoid setGlide(bool glide)\n+\t{\n+\t\tm_glide = glide;\n+\t}\n+\n+\tdouble glideTime() const\n+\t{\n+\t\treturn m_glideTime;\n+\t}\n+\tvoid setGlideTime(double glideTime)\n+\t{\n+\t\tm_glideTime = glideTime;\n+\t}\n+\n private:\n \n \tvoid updateUnisonParam()\n@@ -605,9 +646,15 @@ private:\n \n \tbool m_mono = false;\n \tbool m_legato = false;\n+\tbool m_glide = false;\n+\tdouble m_glideTime = 0.001;\n \n \tstd::array\u003cfloat, MaxUnisonSize\u003e m_detunePitch;\n \tstd::array\u003cFloat2, MaxUnisonSize\u003e m_unisonPan;\n+\n+\tdouble m_currentFreq = 440; //現在の周波数を常に保存しておく\n+\tdouble m_startGlideFreq = 440; // グライド開始時の周波数\n+\tdouble m_glideElapsed = 0.0; // グライド開始から経過した秒数\n };\n \n class AudioRenderer : public IAudioStream\n@@ -626,6 +673,12 @@ public:\n \t\tm_midiData = midiData;\n \t}\n \n+\tvoid restart()\n+\t{\n+\t\tm_synth.clear();\n+\t\tm_readMIDIPos = 0;\n+\t}\n+\n \tvoid bufferSample()\n \t{\n \t\tconst double currentTime = 1.0 * m_readMIDIPos / SamplingFreq;\n@@ -736,10 +789,10 @@ void Main()\n \t}\n \n \tAudioVisualizer visualizer;\n-\tvisualizer.setSplRange(-60, -30);\n+\tvisualizer.setThresholdFromPeak(-20);\n+\tvisualizer.setLerpStrength(0.5);\n \tvisualizer.setWindowType(AudioVisualizer::Hamming);\n-\tvisualizer.setShowPastNotes(false);\n-\tvisualizer.setDrawScore(NoteNumber::C_2, NoteNumber::B_7);\n+\tvisualizer.setDrawScore(NoteNumber::C_3, NoteNumber::B_6);\n \tvisualizer.setDrawArea(Scene::Rect());\n \n \tstd::shared_ptr\u003cAudioRenderer\u003e audioStream = std::make_shared\u003cAudioRenderer\u003e();\n@@ -748,8 +801,10 @@ void Main()\n \tauto\u0026 synth = audioStream-\u003esynth();\n \tsynth.setOscIndex(static_cast\u003cint\u003e(WaveForm::Sin));\n \tsynth.setMono(true);\n-\tsynth.setLegato(false);\n-\tsynth.setAmplitude(0.4);\n+\tsynth.setLegato(true);\n+\tsynth.setGlide(true);\n+\tsynth.setGlideTime(0.1);\n+\tsynth.setAmplitude(0.2);\n \n \tauto\u0026 adsr = synth.adsr();\n \tadsr.attackTime = 0.01;\n@@ -758,11 +813,18 @@ void Main()\n \tadsr.releaseTime = 0.01;\n \n \tbool isRunning = true;\n+\tbool requestRestart = false;\n \n \tauto renderUpdate = [\u0026]()\n \t{\n \t\twhile (isRunning)\n \t\t{\n+\t\t\tif (requestRestart)\n+\t\t\t{\n+\t\t\t\taudioStream-\u003erestart();\n+\t\t\t\trequestRestart = false;\n+\t\t\t}\n+\n \t\t\twhile (!audioStream-\u003ebufferCompleted())\n \t\t\t{\n \t\t\t\taudioStream-\u003ebufferSample();\n@@ -814,6 +876,11 @@ void Main()\n \t\t{\n \t\t\taudioStream-\u003eupdateGUI(pos);\n \t\t}\n+\n+\t\tif (KeySpace.down())\n+\t\t{\n+\t\t\trequestRestart = true;\n+\t\t}\n \t}\n \n \tisRunning = false;\n```\n### ３.５ LFO による変調\n```diff\n@@ -308,6 +308,134 @@ private:\n \tdouble m_prevStateLevel = 0; // ステート変更前のレベル [0, 1]\n };\n \n+class LFO\n+{\n+public:\n+\n+\t// 位相をリセットする\n+\tvoid reset()\n+\t{\n+\t\tm_phase = 0;\n+\t}\n+\n+\tvoid update(double dt)\n+\t{\n+\t\tif (m_lfoFunction)\n+\t\t{\n+\t\t\t// 音符の長さで周期を設定する場合は、ここでBPMを受け取って時間に変換する\n+\t\t\tconst double cycleTime = m_seconds;\n+\t\t\tconst double deltaPhase = Math::TwoPi * dt / cycleTime;\n+\n+\t\t\tm_currentLevel = m_lfoFunction(m_phase);\n+\t\t\tm_phase += deltaPhase;\n+\n+\t\t\tif (Math::TwoPi \u003c m_phase)\n+\t\t\t{\n+\t\t\t\tif (m_loop)\n+\t\t\t\t{\n+\t\t\t\t\tm_phase -= Math::TwoPi;\n+\t\t\t\t}\n+\t\t\t\telse\n+\t\t\t\t{\n+\t\t\t\t\tm_phase = Math::TwoPi;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\t// 現在の入力値: [0, 2pi]\n+\tdouble phase() const\n+\t{\n+\t\treturn m_phase;\n+\t}\n+\n+\t// 現在の出力値: [-1.0, 1.0]\n+\tdouble currentLevel() const\n+\t{\n+\t\treturn m_currentLevel;\n+\t}\n+\n+\t// 周期を設定する\n+\tvoid setSeconds(double seconds)\n+\t{\n+\t\tm_seconds = seconds;\n+\t}\n+\n+\t// カーブを設定する\n+\tvoid setFunction(std::function\u003cdouble(double)\u003e func)\n+\t{\n+\t\tm_lfoFunction = func;\n+\t}\n+\n+\tbool isLoop() const\n+\t{\n+\t\treturn m_loop;\n+\t}\n+\n+\t// ループを有効にする\n+\tvoid setLoop(bool isLoop)\n+\t{\n+\t\tm_loop = isLoop;\n+\t}\n+\n+private:\n+\n+\tdouble m_seconds = 1;\n+\tbool m_loop = true;\n+\tstd::function\u003cdouble(double)\u003e m_lfoFunction;\n+\n+\tdouble m_phase = 0;\n+\tdouble m_currentLevel = 0;\n+};\n+\n+class ModParameter\n+{\n+public:\n+\n+\tModParameter(double value) : value(value) {}\n+\n+\t// 値が書き換わったら true を返す\n+\tbool fetch(const Array\u003cLFO\u003e\u0026 lfoTable)\n+\t{\n+\t\tif (m_modIndex)\n+\t\t{\n+\t\t\tconst double x = lfoTable[m_modIndex.value()].currentLevel();\n+\t\t\tconst double newValue = Math::Lerp(m_low, m_high, x * 0.5 + 0.5);\n+\t\t\tif (value != newValue)\n+\t\t\t{\n+\t\t\t\tvalue = newValue;\n+\t\t\t\treturn true;\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn false;\n+\t}\n+\n+\tvoid setRange(double lowValue, double highValue)\n+\t{\n+\t\tm_low = lowValue;\n+\t\tm_high = highValue;\n+\t}\n+\n+\tvoid setModIndex(int index)\n+\t{\n+\t\tm_modIndex = index;\n+\t}\n+\n+\tvoid unsetModIndex()\n+\t{\n+\t\tm_modIndex = none;\n+\t}\n+\n+\tdouble value = 0;\n+\n+private:\n+\n+\tdouble m_low = 0;\n+\tdouble m_high = 1;\n+\tOptional\u003cint\u003e m_modIndex;\n+};\n+\n float NoteNumberToFrequency(int8_t d)\n {\n \treturn 440.0f * pow(2.0f, (d - 69) / 12.0f);\n@@ -354,10 +482,20 @@ public:\n \t\t\tnoteState.m_envelope.update(m_adsr, deltaT);\n \t\t}\n \n+\t\t// 再生中のノートがあれば LFO を更新する\n+\t\tif (!m_noteState.empty())\n+\t\t{\n+\t\t\tfor (auto\u0026 lfoState : m_lfoStates)\n+\t\t\t{\n+\t\t\t\tlfoState.update(deltaT);\n+\t\t\t}\n+\t\t}\n+\n \t\t// リリースが終了したノートを削除する\n \t\tstd::erase_if(m_noteState, [\u0026](const auto\u0026 noteState) { return noteState.second.m_envelope.isReleased(m_adsr); });\n \n-\t\tconst auto pitch = pow(2.0, m_pitchShift / 12.0);\n+\t\tm_pitchShift.fetch(m_lfoStates);\n+\t\tconst auto pitch = pow(2.0, m_pitchShift.value / 12.0);\n \n \t\t// 入力中の波形を加算して書き込む\n \t\tWaveSample sample(0, 0);\n@@ -399,10 +537,12 @@ public:\n \t\t\t}\n \t\t}\n \n-\t\tsample.left *= static_cast\u003cfloat\u003e(cos(Math::HalfPi * m_pan));\n-\t\tsample.right *= static_cast\u003cfloat\u003e(sin(Math::HalfPi * m_pan));\n+\t\tm_pan.fetch(m_lfoStates);\n+\t\tsample.left *= static_cast\u003cfloat\u003e(cos(Math::HalfPi * m_pan.value));\n+\t\tsample.right *= static_cast\u003cfloat\u003e(sin(Math::HalfPi * m_pan.value));\n \n-\t\treturn sample * static_cast\u003cfloat\u003e(m_amplitude / sqrt(m_unisonCount));\n+\t\tm_amplitude.fetch(m_lfoStates);\n+\t\treturn sample * static_cast\u003cfloat\u003e(m_amplitude.value / sqrt(m_unisonCount));\n \t}\n \n \tvoid noteOn(int8_t noteNumber, int8_t velocity)\n@@ -431,6 +571,15 @@ public:\n \t\t\tm_startGlideFreq = m_currentFreq;\n \t\t\tm_glideElapsed = 0;\n \t\t}\n+\n+\t\tif (!m_mono)\n+\t\t{\n+\t\t\t// LFO の再生状態をリセットする\n+\t\t\tfor (auto\u0026 lfoState : m_lfoStates)\n+\t\t\t{\n+\t\t\t\tlfoState.reset();\n+\t\t\t}\n+\t\t}\n \t}\n \n \tvoid noteOff(int8_t noteNumber)\n@@ -452,14 +601,14 @@ public:\n \n \tvoid updateGUI(Vec2\u0026 pos)\n \t{\n-\t\tSimpleGUI::Slider(U\"amplitude : {:.2f}\"_fmt(m_amplitude), m_amplitude, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n-\t\tSimpleGUI::Slider(U\"pan : {:.2f}\"_fmt(m_pan), m_pan, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n+\t\tSimpleGUI::Slider(U\"amplitude : {:.2f}\"_fmt(m_amplitude.value), m_amplitude.value, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n+\t\tSimpleGUI::Slider(U\"pan : {:.2f}\"_fmt(m_pan.value), m_pan.value, 0.0, 1.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n \t\tSliderInt(U\"oscillator : {}\"_fmt(m_oscIndex), m_oscIndex, 0, 3, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth);\n \n-\t\tif (SimpleGUI::Slider(U\"pitchShift : {:.2f}\"_fmt(m_pitchShift), m_pitchShift, -24.0, 24.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth)\n+\t\tif (SimpleGUI::Slider(U\"pitchShift : {:.2f}\"_fmt(m_pitchShift.value), m_pitchShift.value, -24.0, 24.0, Vec2{ pos.x, pos.y += SliderHeight }, LabelWidth, SliderWidth)\n \t\t\t \u0026\u0026 KeyControl.pressed())\n \t\t{\n-\t\t\tm_pitchShift = Math::Round(m_pitchShift);\n+\t\t\tm_pitchShift.value = Math::Round(m_pitchShift.value);\n \t\t}\n \n \t\tbool unisonUpdated = false;\n@@ -502,6 +651,11 @@ public:\n \t\treturn m_adsr;\n \t}\n \n+\tArray\u003cLFO\u003e\u0026 lfoStates()\n+\t{\n+\t\treturn m_lfoStates;\n+\t}\n+\n \tint oscIndex() const\n \t{\n \t\treturn m_oscIndex;\n@@ -511,31 +665,31 @@ public:\n \t\tm_oscIndex = oscIndex;\n \t}\n \n-\tdouble amplitude() const\n+\tconst ModParameter\u0026 amplitude() const\n \t{\n \t\treturn m_amplitude;\n \t}\n-\tvoid setAmplitude(double amplitude)\n+\tModParameter\u0026 amplitude()\n \t{\n-\t\tm_amplitude = amplitude;\n+\t\treturn m_amplitude;\n \t}\n \n-\tdouble pan() const\n+\tconst ModParameter\u0026 pan() const\n \t{\n \t\treturn m_pan;\n \t}\n-\tvoid setPan(double pan)\n+\tModParameter\u0026 pan()\n \t{\n-\t\tm_pan = pan;\n+\t\treturn m_pan;\n \t}\n \n-\tdouble pitchShift() const\n+\tconst ModParameter\u0026 pitchShift() const\n \t{\n \t\treturn m_pitchShift;\n \t}\n-\tvoid setPitchShift(double pitchShift)\n+\tModParameter\u0026 pitchShift()\n \t{\n-\t\tm_pitchShift = pitchShift;\n+\t\treturn m_pitchShift;\n \t}\n \n \tint unisonCount() const\n@@ -635,9 +789,11 @@ private:\n \n \tADSRConfig m_adsr;\n \n-\tdouble m_amplitude = 0.1;\n-\tdouble m_pan = 0.5;\n-\tdouble m_pitchShift = 0.0;\n+\tArray\u003cLFO\u003e m_lfoStates;\n+\n+\tModParameter m_amplitude = 0.1;\n+\tModParameter m_pan = 0.5;\n+\tModParameter m_pitchShift = 0.0;\n \tint m_oscIndex = 0;\n \n \tint m_unisonCount = 1;\n@@ -804,7 +960,7 @@ void Main()\n \tsynth.setLegato(true);\n \tsynth.setGlide(true);\n \tsynth.setGlideTime(0.1);\n-\tsynth.setAmplitude(0.2);\n+\tsynth.amplitude().value = 0.2;\n \n \tauto\u0026 adsr = synth.adsr();\n \tadsr.attackTime = 0.01;\n@@ -812,6 +968,16 @@ void Main()\n \tadsr.sustainLevel = 0.2;\n \tadsr.releaseTime = 0.01;\n \n+\tauto\u0026 lfoStates = synth.lfoStates();\n+\tlfoStates.resize(1);\n+\tlfoStates[0].setFunction(Sin);\n+\tlfoStates[0].setSeconds(0.1);\n+\tlfoStates[0].setLoop(true);\n+\n+\tauto\u0026 pitchShift = synth.pitchShift();\n+\tpitchShift.setModIndex(0);\n+\tpitchShift.setRange(-0.5, 0.5);\n+\n \tbool isRunning = true;\n \tbool requestRestart = false;\n \n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagehama%2Fsynthesizertutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fagehama%2Fsynthesizertutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagehama%2Fsynthesizertutorial/lists"}