LoginSignup
5
3
新規開発や新技術の検証、導入にまつわる記事を投稿しよう!

C# で ChatGPT API: AI で作曲して MIDI ファイルを作成する

Last updated at Posted at 2023-06-18

C# で ChatGPT API: AI で作曲して MIDI ファイルを作成する

こんにちは、@studio_meowtoon です。今回は、WSL Ubuntu 22.04 の C# / .NET 環境にて、ChatGPT API で作曲を行い MIDI ファイルを作成する方法を紹介します。
chat-gpt_with_csharp.png

実現すること

ローカル環境の Ubuntu で、ChatGPT API を C# から使い、作曲をさせて MIDI ファイルとして出力します。

この記事では、従来のプログラムでは難しいとされるクリエイティブな作業である作曲において、AI がどのように役立つかを実際に試してみることを目的の一つとしています。

C# には本来厳格なコーディング規則がありますが、この記事では可読性のために、一部規則に沿わない表記方法を使用しています。ご注意ください。

開発環境

  • Windows 11 Home 22H2 を使用しています。

WSL の Ubuntu を操作していきますので macOS の方も参考にして頂けます。

WSL (Microsoft Store アプリ版) ※ こちらの関連記事からインストール方法をご確認いただけます

> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47

Ubuntu ※ こちらの関連記事からインストール方法をご確認いただけます

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04

.NET SDK ※ こちらの関連記事からインストール方法をご確認いただけます

$ dotnet --list-sdks
7.0.202 [/usr/share/dotnet/sdk]
$ dotnet --version
7.0.202

AI にどのようにアプローチするか?

ここでは AI に音楽理論のチャーチモードを説明し、そちらに基づいて AI にメロディの作成を依頼します。

音楽理論については、こちらを展開してご覧いただけます。

チャーチモード(教会旋法)

旋法 表記 スケール
リディアン Lydian 1, 2, 3, #4, 5, 6, 7
イオニアン Ionian 1, 2, 3, 4, 5, 6, 7
ミクソリディアン Mixolydian 1, 2, 3, 4, 5, 6, b7
ドリアン Dorian 1, 2, b3, 4, 5, 6, b7
エオリアン Aeolian 1, 2, b3, 4, 5, b6, b7
フリジアン Phrygian 1, b2, b3, 4, 5, b6, b7
ロクリアン Locrian 1, b2, b3, 4, b5, b6, b7

上記の音楽理論を用いて、AI がある程度秩序を保ったメロディを作成するように試みます。

AI で作曲する手順

OpenAI API のキーを取得します

こちらの以前の記事を参考にして頂けます。

UbuntuOPENAI_API_KEY 環境変数を作成します。

$ export OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

この環境変数が一時的なものであることに注意してください。

プロジェクトの作成

プロジェクトルートフォルダに移動します。
※ ~/tmp をプロジェクトルートフォルダとします。

$ cd ~/tmp

コンソールアプリを作成します。
※ GPTMIDIApp がアプリ名です。

$ dotnet new console -o GPTMIDIApp -f net7.0

一度コンソールアプリをビルド・実行します。

$ cd ~/tmp/GPTMIDIApp
$ dotnet run
Hello, World!

ここまでの手順で、C# / .NET コンソールアプリの雛形が作成できました😋

ライブラリの追加

ChatGPT API を使用する為、OpenAI のパッケージを NuGet で取得します。

$ dotnet add package OpenAI

また、ChatGPT API に渡すパラメータを JSON ファイルから読み込む為、Microsoft.Extensions.Configuration のパッケージを NuGet で取得します。

$ dotnet add package Microsoft.Extensions.Configuration
$ dotnet add package Microsoft.Extensions.Configuration.Json
$ dotnet add package Microsoft.Extensions.Configuration.Binder

パッケージを確認します。

$ dotnet list package
プロジェクト 'GPTMIDIApp' に次のパッケージ参照が含まれています
   [net7.0]:
   最上位レベル パッケージ                                     要求済み    解決済み
   > Microsoft.Extensions.Configuration             7.0.0   7.0.0
   > Microsoft.Extensions.Configuration.Binder      7.0.4   7.0.4
   > Microsoft.Extensions.Configuration.Json        7.0.0   7.0.0
   > OpenAI

ここまでの手順で、プロジェクトに OpenAIMicrosoft.Extensions.Configuration のパッケージを組み込むことができました😋

ChatGPT API に渡すプロンプトについて

ChatGPT API に関連して耳にするプロンプトとは何でしょうか?🤔

OpenAI の Web サイトにこのように記されています。

The inputs to GPTs are also referred to as "prompts". Designing a prompt is essentially how you “program” a GPT model, usually by providing instructions or some examples of how to successfully complete a task.

GPT への入力はプロンプトとも呼ばれます。プロンプトの設計は基本的に、GPT モデルをプログラムする方法であり、通常はタスクを正常に完了するための手順や例を提供します。

プロンプトメッセージの構成

役割 内容
システム アシスタントの動作を設定するのに役立ちます。たとえば、アシスタントの性格を設定したり、会話全体でアシスタントがどのように動作するかについて具体的な指示を設定します。
ユーザー アシスタントが応答するためのリクエストまたはコメントを提供します。
アシスタント 通常、以前のアシスタントの応答が保存されています。望ましい動作の例を示すためにユーザーが作成することもできます。

このように、GhatGPT に対してシステムプロンプトで手順を、また、アシスタントプロンプトでを提供することが可能となっています😋

ChatGPT API に渡すプロンプトの作成

実際にプログラムを開発する前に、Playgroundシステムアシスタントプロンプトの内容を検討した方がスムーズに開発が進みます😐

ChatGPT API Playground

今回の記事では以下のようなプロンプトを作成しました。

この記事では音の音程を後述するMIDI ノート番号で表しています。

こちらを展開してご覧いただけます。
システム
数値群から数値を選択してメロディとして連続32個出力せよ。

数値群:
A: {60,62,64,66,67,69,71,72}
B: {60,62,64,65,67,69,71,72}
C: {60,62,64,65,67,69,70,72}
D: {60,62,63,65,67,69,70,72}
E: {60,62,63,65,67,68,70,72}
F: {60,61,63,65,67,68,70,72}
G: {60,61,63,65,66,68,70,72}

特別な数値:
A: {60,66}
B: {65,71}
C: {64,70}
D: {63,69}
E: {62,68}
F: {61,67}
G: {60,66}

以下の条件で数値を連続32個並べる。

・数値群が選ばれる確率は対等である
・数値は必ず一つの数値群から選ぶ
・数値は重複することが出来る
・数値は連続することが出来る
・数値群の特別な音は出現する頻度が極端に低い
・数値の選択頻度はかなりバラツキがある
・数値は音楽のメロディのように並ぶ

どの数値群から選んだか出力する。
数値はそれぞれどれくらいの頻度で使用したか出力する。

以上の条件で連続32個並べた数値を以下のフォーマットで出力する。

数値群: X

頻度:
60:5
62:3
64:4
...

結果:
{ n,n,n,n... }
アシスタント
これが提示された条件に基づいて作成した、具体的な実行例です。

数値群: E

頻度:
60: 2
62: 5
63: 3
65: 6
67: 5
68: 3
70: 5
72: 3

結果:
{62, 65, 70, 65, 62, 63, 65, 68, 70, 65, 67, 68, 67, 63, 68, 67, 65, 72, 67, 60, 65, 70, 67, 62, 70, 67, 68, 65, 70, 68, 72, 72}
ChatGPT API のレスポンスの一例
注:この問題は、ランダム性が含まれているため、異なる回答が得られる場合があります。以下の回答は、一つの例です。

数値群: C

頻度:
60: 6
62: 2
64: 3
65: 4
67: 5
69: 3
70: 4
72: 5

結果:
{65, 67, 64, 60, 67, 70, 69, 70, 60, 69, 67, 72, 67, 60, 72, 67, 69, 60, 65, 70, 65, 72, 70, 64, 72, 67, 60, 65, 70, 67, 69, 67}

ここまでの手順で、ChatGPT API Playground から音楽の旋律を得ることができました😋

プロンプトの組み方によりさらに音楽的な旋律を自動生成させることが可能だと思われます。今回の例ではこのような単純な例で紹介しました。

コードの修正

プロジェクトフォルダに移動します。

$ cd ~/tmp/GPTMIDIApp

設定ファイルを読み込む AppSettings.cs を作成します。

$ vim AppSettings.cs

ファイルの内容

AppSettings.cs
namespace GPTMIDIApp {
    public class AppSettings {
        public string? System { get; set; }
        public string? StartupAssistant { get; set; }
    }
}

appsettings.json を作成します。

$ vim appsettings.json

ファイルの内容

appsettings.json
{
  "System": "数値群から数値を選択してメロディとして連続32個出力せよ。\n\n数値群:\nA: {60,62,64,66,67,69,71,72}\nB: {60,62,64,65,67,69,71,72}\nC: {60,62,64,65,67,69,70,72}\nD: {60,62,63,65,67,69,70,72}\nE: {60,62,63,65,67,68,70,72}\nF: {60,61,63,65,67,68,70,72}\nG: {60,61,63,65,66,68,70,72}\n\n特別な数値:\nA: {60,66}\nB: {65,71}\nC: {64,70}\nD: {63,69}\nE: {62,68}\nF: {61,67}\nG: {60,66}\n\n以下の条件で数値を連続32個並べる。\n\n・数値群が選ばれる確率は対等である\n・数値は必ず一つの数値群から選ぶ\n・数値は重複することが出来る\n・数値は連続することが出来る\n・数値群の特別な音は出現する頻度が極端に低い\n・数値の選択頻度はかなりバラツキがある\n・数値は音楽のメロディのように並ぶ\n\nどの数値群から選んだか出力する。\n数値はそれぞれどれくらいの頻度で使用したか出力する。\n\n以上の条件で連続32個並べた数値を以下のフォーマットで出力する。\n\n数値群: X\n\n頻度:\n60:5\n62:3\n64:4\n...\n\n結果:\n{ n,n,n,n... }",
  "StartupAssistant": "これが提示された条件に基づいて作成した、具体的な実行例です。\n\n数値群: E\n\n頻度:\n60: 2\n62: 5\n63: 3\n65: 6\n67: 5\n68: 3\n70: 5\n72: 3\n\n結果:\n{62, 65, 70, 65, 62, 63, 65, 68, 70, 65, 67, 68, 67, 63, 68, 67, 65, 72, 67, 60, 65, 70, 67, 62, 70, 67, 68, 65, 70, 68, 72, 72}"
}

appsettings.json をビルドの成果物に含める為、GPTMIDIApp.csproj を修正します。

$ vim GPTMIDIApp.csproj

ファイルの内容 (※以下の内容を追記します)

GPTMIDIApp.csproj の一部
<Project Sdk="Microsoft.NET.Sdk">
  <!-- 省略 -->
  <ItemGroup>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>

ここまでの手順で、ChatGPT API に入力するプロンプトを含んだ appsettings.json の内容を AppSettings オブジェクトに読み込む設定ができました😋

Program.cs を修正します。

$ vim Program.cs

ファイルの内容

Program.cs
using static System.Console;
using static System.ConsoleColor;
using static System.Environment;

using Microsoft.Extensions.Configuration;
using OpenAI_API;
using OpenAI_API.Chat;
using OpenAI_API.Models;

namespace GPTMIDIApp {
    class Program {
        static async Task Main(string[] args) {
            // ChatGPT API から問い合わせの結果を取得
            string reply = await getAssistant();
        }
        // ChatGPT API に問い合わせます
        static async Task<String> getAssistant() {
            // AppSettings オブジェクトを取得
            IConfiguration configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build();
            AppSettings? app_settings = configuration.Get<AppSettings>();
            // プロンプトを取得
            string? system = app_settings?.System;
            string? startup_assistant = app_settings?.StartupAssistant;
#if DEBUG
            ForegroundColor = Red;
            WriteLine($"SYSTEM:\n{system}\n");
            WriteLine($"STARTUP ASSISTANT:\n{startup_assistant}\n");
            ResetColor();
#endif
            // OpenAI の API キーを取得
            string? api_key = GetEnvironmentVariable("OPENAI_API_KEY");
            // OpenAIAPI オブジェクトを生成
            OpenAIAPI api = new(api_key);
            // ChatMessage オブジェクトのリストを作成
            List<ChatMessage> chat_message_list = new();
            // system プロンプトを設定
            chat_message_list.Add(new ChatMessage(ChatMessageRole.System, system));
            // assistant プロンプトを設定
            chat_message_list.Add(new ChatMessage(ChatMessageRole.Assistant, startup_assistant));
            // user プロンプトを設定
            chat_message_list.Add(new ChatMessage(ChatMessageRole.User, "次の実行例を表示する。"));
            // ChatGPT API にリクエストして結果を取得
            ChatResult result = await api.Chat.CreateChatCompletionAsync(new ChatRequest() {
                Model = Model.ChatGPTTurbo0301,
                Messages = chat_message_list
            });
#if DEBUG
            ForegroundColor = Green;
            string? reply = result.Choices[0].Message.Content.Trim();
            WriteLine($"ASSISTANT:\n{reply}\n");
            ResetColor();
#endif
            WriteLine($"get note completed.");
            return reply;
        }
    }
}

コンソールアプリを実行します。

$ dotnet run

image.png

ここまでの手順で、ChatGPT API を使い、システムアシスタントプロンプトに基づいた音楽の旋律を得ることができました😋

MIDI ファイルを出力する手順

コードの修正

ChatGPT API から取得した文字列から MIDI ノート番号などを抽出する処理を作成します。

Program.cs を修正します。

$ vim Program.cs

ファイルの内容(※一部)

Program.cs の一部
// MIDI ノート番号とモード名を取得します
static (string mode, int[] notes) getMIDINotes(string content) {
    Dictionary<string, string> type_to_mode = new() {
        { "A", "Lydian" }, { "B", "Ionian" }, { "C", "Mixolydian" },
        { "D", "Dorian" }, { "E", "Aeolian" }, { "F", "Phrygian" }, { "G", "Locrian" }
    };
    // MIDI ノート番号の配列切り出し
    string[] split_array1 = content.Split(new string[] { "結果:" }, StringSplitOptions.None);
    string notes_string = split_array1[1].Trim();
    int[] notes = notes_string.TrimStart('{').TrimEnd('}').Split(',').Select(int.Parse).ToArray();
    // 数値群のタイプ切り出し
    string[] split_array2 = content.Split(new string[] { "数値群:" }, StringSplitOptions.None);
    string type = split_array2[1].Trim().Substring(0, 1);
    // タプルで返す
    return (type_to_mode[type], notes);
}

ここまでの手順で、ChatGPT API から取得した結果を、後段のプログラムから利用できる状態加工することができました😋

また、取り出した MIDI ノート番号の配列を MIDI ファイルとして出力する具体的な内容については、こちらの以前の記事を参照頂けます🙇‍♂️

コードの全体を表示する
Program.cs
using static System.Console;
using static System.ConsoleColor;
using static System.Environment;

using Microsoft.Extensions.Configuration;
using OpenAI_API;
using OpenAI_API.Chat;
using OpenAI_API.Models;
using Sanford.Multimedia.Midi;
using static Sanford.Multimedia.Midi.ChannelCommand;
using static Sanford.Multimedia.Midi.MetaType;

using static GPTMIDIApp.Converter;

namespace GPTMIDIApp {
    class Program {
        static async Task Main(string[] args) {
            // ChatGPT API から問い合わせの結果を取得
            string reply = await getAssistant();
            // MIDI ノート番号とモード名を取得する
            (string mode, int[] notes) = getMIDINotes(reply);
            // MIDI ファイルを保存
            writeMIDIFile(mode, notes);
        }
        // ChatGPT API に問い合わせます
        static async Task<String> getAssistant() {
            // AppSettings オブジェクトを取得
            IConfiguration configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build();
            AppSettings? app_settings = configuration.Get<AppSettings>();
            // プロンプトを取得
            string? system = app_settings?.System;
            string? startup_assistant = app_settings?.StartupAssistant;
#if DEBUG
            ForegroundColor = Red;
            WriteLine($"SYSTEM:\n{system}\n");
            WriteLine($"STARTUP ASSISTANT:\n{startup_assistant}\n");
            ResetColor();
#endif
            // OpenAI の API キーを取得
            string? api_key = GetEnvironmentVariable("OPENAI_API_KEY");
            // OpenAIAPI オブジェクトを生成
            OpenAIAPI api = new(api_key);
            // ChatMessage オブジェクトのリストを作成
            List<ChatMessage> chat_message_list = new();
            // system プロンプトを設定
            chat_message_list.Add(new ChatMessage(ChatMessageRole.System, system));
            // assistant プロンプトを設定
            chat_message_list.Add(new ChatMessage(ChatMessageRole.Assistant, startup_assistant));
            // user プロンプトを設定
            chat_message_list.Add(new ChatMessage(ChatMessageRole.User, "次の実行例を表示する。"));
            // ChatGPT API にリクエストして結果を取得
            ChatResult result = await api.Chat.CreateChatCompletionAsync(new ChatRequest() {
                Model = Model.ChatGPTTurbo0301,
                Messages = chat_message_list
            });
#if DEBUG
            ForegroundColor = Green;
            string? reply = result.Choices[0].Message.Content.Trim();
            WriteLine($"ASSISTANT:\n{reply}\n");
            ResetColor();
#endif
            WriteLine($"get note completed.");
            return reply;
        }
        // MIDI ノート番号とモード名を取得します
        static (string mode, int[] notes) getMIDINotes(string content) {
            Dictionary<string, string> type_to_mode = new() {
                { "A", "Lydian" }, { "B", "Ionian" }, { "C", "Mixolydian" },
                { "D", "Dorian" }, { "E", "Aeolian" }, { "F", "Phrygian" }, { "G", "Locrian" }
            };
            // MIDI ノート番号の配列切り出し
            string[] split_array1 = content.Split(new string[] { "結果:" }, StringSplitOptions.None);
            string notes_string = split_array1[1].Trim();
            int[] notes = notes_string.TrimStart('{').TrimEnd('}').Split(',').Select(int.Parse).ToArray();
            // 数値群のタイプ切り出し
            string[] split_array2 = content.Split(new string[] { "数値群:" }, StringSplitOptions.None);
            string type = split_array2[1].Trim().Substring(0, 1);
            // タプルで返す
            return (type_to_mode[type], notes);
        }
        // MIDI ファイルを出力します
        static void writeMIDIFile(string mode, int[] notes) {
            // シーケンスの作成
            Sequence sequence = new();
            sequence.Format = 1;

            int channel = 0; // MIDI チャンネル番号
            int velocity = 112; // ノートオン時のベロシティ
            int bpm = 120; // テンポ
            int eighth_note_length = 6000 / (bpm * 4); // 8分音符の長さ(ミリ秒)
            string song_name = $"{mode} Song {DateTime.Now.ToString("yyyy-MM-dd_HHmmss")}"; // 曲名
            string copyright = "STUDIO Awesome"; // 著作権

            // ノートの長さを 8分音符に設定
            int duration = eighth_note_length;

            // コンダクタートラックの設定
            Track conductor_track = new();
            conductor_track.Insert(position: 0, new MetaMessage(Tempo, ToByteTempo(bpm)));
            conductor_track.Insert(position: 0, new MetaMessage(TrackName, ToByteArray(song_name)));
            conductor_track.Insert(position: 0, new MetaMessage(Copyright, ToByteArray(copyright)));
            sequence.Add(item: conductor_track);

            // シーケンストラックの設定
            Track sequence_track = new();
            sequence_track.Insert(position: 0, new MetaMessage(TrackName, ToByteArray("Melody")));
            sequence.Add(item: sequence_track);

            // 音階の設定
            int tick = 0; // 時間のトラッキング用変数
            foreach (int note_number in notes) {
                // ノートオンイベントの作成
                sequence_track.Insert(position: tick, new ChannelMessage(NoteOn, channel, note_number, velocity));
                // 次のノートまでの時間を加算
                tick += duration;
                // ノートオフイベントの作成
                sequence_track.Insert(position: tick, new ChannelMessage(NoteOff, channel, note_number, 0));
            }

            // Bass 音の設定
            tick = 0;
            foreach (int note_number in new int[] { 36, 36, 36, 36 }) { // キーを C に指定
                sequence_track.Insert(position: tick, new ChannelMessage(NoteOn, channel, note_number, velocity));
                tick += (duration * 8); // 全音符
                sequence_track.Insert(position: (tick - 1), new ChannelMessage(NoteOff, channel, note_number, 0));
            }

            // MIDI ファイルの保存
            sequence.Save($"{song_name.Replace(" ", "_")}.mid");
            WriteLine("write midi file completed.");
        }
    }
}

CahtGPT に作曲させてみる

コンソールアプリを実行します。

$ export OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$ cd ~/tmp/GPTMIDIApp
$ dotnet run

プロジェクトフォルダに MIDI ファイルが出力されました。

Dorian_Song_2023-06-17_231957.mid

🎵実際に ChatGPT が作曲した音楽を聴いてみる😋

モード mp3 ファイル
イオニアン 🔊Ionian_Song_2023-06-17_223247.mp3
ドリアン 🔊Dorian_Song_2023-06-17_232103.mp3

※ ChatGPT が作曲したメロディにプログラムからベース音を足しています。非常に粗削りではありますが、AI 作曲への可能性を感じて頂けたらと思います。

まとめ

ローカル環境の Ubuntu C# / .NET 環境にて、ChatGPT に作曲をさせて MIDI ファイルとして出力することができました。

どうでしたか? Window 11 の WSL Ubuntu に、C# / .NET の開発環境を手軽に構築することができます、ぜひお試しください。今後も .NET / OpenAI の技術トピックなどを紹介していきますので、ぜひお楽しみにしてください。

推奨コンテンツ

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3