ChatGPT 4oで演奏プログラムを作らせてみた

表題のテーマで、どこまで自動的にプログラムを作成できるかを調べてみる。

## 材料
Webアプリを作る。
obniz board と圧電スピーカーを使う。WebとIoTデバイスを連携させる。
ChatGPT 4o をインタラクティブに使う。

## アプリケーションの内容
独自フォーマットで定義された楽譜を読み込んで、obnizと圧電スピーカーを使って音楽を奏でさせるプログラムを作成を完全に自動生成させることを試みる。

## 最初のプロンプト
> 楽譜を読み込んで、その音楽を奏でるプログラムを作成する。
言語はJavaScript
音を出すspeakerには、obnizで利用できる圧電スピーカーを使う。
> 楽譜データには、テンポ、拍子、音階、音価、休符、タイなどが含まれており、一般的な分かりやすい形式で指定されている。
> 楽譜データはHTMLのテキストフィールドから取得し、そのデータを再生するためのプログラムを作成せよ。
> 
> 音階をA4,A#4,Bb4,B4,C4のように指定し、音価を
全音符: 1
二分音符: 1/2
四分音符: 1/4
八分音符: 1/8
十六分音符: 1/16
のように指定する。
曲のテンポの指定は、120BPMのように指定し、この場合は1分間に120拍で1拍は0.5秒（500ミリ秒）です。

## 追加したプロンプト
今回はWhite Christmasを曲として選んだが、このシンプルな曲でも３連符やタイが使われているので、それらを上手く処理できるようにした。
> タイ、３連符、音の強弱、繰り返し記号を表現できるようにして。

プロンプトに音の強弱を追加したのだが、実際には圧電スピーカーで音の強弱を表現する簡単な方法は無い。上記のプロンプトでどんなコードが出来上がるかを見てみた。

> 曲はホワイトクリスマスにしてください

## 出力されたコードの評価

そのまま実行は出来なかった。
エラーとしては、２点あり、一つはobnizのライブラリを実行するのに必要なファイルの読み込みが不足していたことと、もう一つは音の強弱を付ける処理に不備があった、というか、実行できない処理が追加されていた。

３回ほど問題点を指摘すると、そのたびに修正したコードが生成されて、最終的には動作した。

## 問題点
- やるたびに結果が違う。楽譜データとして、JSONを使ったり、コロン区切りのiniファイルみたいなものを考えたりする。
- JSONでは、1/4のような数値は使えないが、必ずここを間違う。
- ３連符など、具体的にこれに対応してほしいという指示をしないと処理を入れない。データ形式に追加しても、コードも修正するように指示する必要がある。
- obnizについてはある程度分かっているようだが、ライブラリのインクルードや、メソッドの使い方に間違いがあり、そのままでは動作しなかった。
具体的には、終了時に、obniz.disconnect()というメソッドを呼び出すソースになっていたが、そのようなメソッドは存在しない。
また、音の強弱を付けるように指示したが、圧電スピーカーでは音の強弱が付けられない。にもかかわらず、音の強弱を行うロジックを追加していた。これは実行してもエラーになる。
- ホワイトクリスマスは、全然似ても似つかぬ音楽になった。
- ChatGPTの問題だが、結構途中で止まったり、エラーになったりする。

## 良いところ
バグは多少あるが、問題は明らかで修正が容易。問題が出たのは、ほぼobnizとのインターフェース部分。
追加してほしいものは指示すればよい。
譜面データの仕様書などを正確に書いてくれる。

## 考察
ホワイトクリスマスの譜面を知らないというのは残念だった。自分でデータを起こすにしても、譜面から読み取って一つ一つ置いていく必要があるので、この点を自動化できないものか。
SmartScoreというアプリが、譜面を読み取って、MusicXMLという形式に変換してくれるようなので、これを使って見るのもありかも。
MusicXMLは譜面のコーディングとしてはどうやらデファクト標準らしい。
obnizの部分は、他システムとの連携になるが、連携方法の仕様はChatGPT任せにせずに、プロンプトとして設計しておいた方がいいかもしれない。またライブラリなどは最新情報が無いだろうから、それもこちらから指定すべきだろう。

## 最終版のコード
```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>simpleMusicFormat Player</title>
</head>
<body>
    <h1>simpleMusicFormat Player</h1>
    <textarea id="simpleMusicFormat" rows="10" cols="50">
    Tempo: 90
    TimeSignature: 4/4
    Notes: G4 1/4 mf, A4 1/4 mf, B4 1/4 mf, G4 1/4 mf, E4 1/4 mf, E4 1/4 mf, D4 1/4 mf, C4 1/4 mf, B4 1/4 mf, C5 1/4 mf, G4 1/4 mf, E4 1/4 mf, F#4 1/4 mf, G4 1/4 mf, A4 1/4 mf, G4 1/4 mf, F#4 1/4 mf, E4 1/4 mf, D4 1/2 mf
    </textarea>
    <button id="playButton">再生</button>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://unpkg.com/obniz@3.19.0/obniz.js"></script>
    <script src="main.js"></script>
</body>
</html>

```
```javascript
// main.js

$(document).ready(function() {
    $('#playButton').click(async function() {
        const simpleMusicFormat = $('#simpleMusicFormat').val();
        const musicData = parseSimpleMusicFormat(simpleMusicFormat);
        await playMusic(musicData);
    });
});

function parseSimpleMusicFormat(data) {
    const lines = data.split('\n');
    let tempo = 90;
    let timeSignature = '4/4';
    const notes = [];
    let repeatStartIndex = -1;
    let inTriplet = false;

    lines.forEach(line => {
        line = line.trim();
        if (line.startsWith('Tempo:')) {
            tempo = parseInt(line.split(':')[1].trim(), 10);
        } else if (line.startsWith('TimeSignature:')) {
            timeSignature = line.split(':')[1].trim();
        } else if (line.startsWith('Notes:')) {
            const notesStr = line.split(':')[1].trim();
            const noteParts = notesStr.split(',');
            noteParts.forEach(noteStr => {
                noteStr = noteStr.trim();
                if (noteStr === 'RepeatStart') {
                    repeatStartIndex = notes.length;
                } else if (noteStr === 'RepeatEnd') {
                    if (repeatStartIndex !== -1) {
                        const repeatSection = notes.slice(repeatStartIndex);
                        notes.push(...repeatSection);
                    }
                } else if (noteStr === 'Tie') {
                    if (notes.length > 0) {
                        notes[notes.length - 1].tie = true;
                    }
                } else if (noteStr === 'Triplet') {
                    inTriplet = true;
                } else if (inTriplet && noteStr === 'EndTriplet') {
                    inTriplet = false;
                } else {
                    const [note, duration, volume] = noteStr.split(' ');
                    notes.push({
                        note,
                        duration: inTriplet ? parseFloat(duration) / 3 : parseFloat(duration),
                        volume: volume || 'mf'
                    });
                }
            });
        }
    });

    return { tempo, timeSignature, notes };
}

async function playMusic(musicData) {
    const { tempo, notes } = musicData;
    const noteDurations = {
        '1': 1,
        '1/2': 1/2,
        '1/4': 1/4,
        '1/8': 1/8,
        '1/16': 1/16
    };

    const bpmToMs = (bpm) => (60 / bpm) * 1000;
    const beatDurationMs = bpmToMs(tempo);

    const obniz = new Obniz("YOUR_OBNIZ_ID");
    await obniz.connectWait();
    const speaker = obniz.wired("Speaker", { signal: 0, gnd: 1 });

    for (let i = 0; i < notes.length; i++) {
        const noteData = notes[i];
        const durationMs = noteDurations[noteData.duration] * beatDurationMs;
        if (noteData.note.toLowerCase() === 'rest') {
            await new Promise(resolve => setTimeout(resolve, durationMs));
        } else {
            const frequency = noteToFrequency(noteData.note);
            speaker.play(frequency);
            adjustVolume(noteData.volume, obniz);
            await new Promise(resolve => setTimeout(resolve, durationMs));
            speaker.stop();
        }

        if (noteData.tie && i < notes.length - 1 && notes[i + 1].note === noteData.note) {
            notes[i + 1].duration += noteData.duration;
        }
    }

    obniz.close();
}

// この関数は動作しない。サポートしないobniz+スピーカの機能を使っている。
function adjustVolume(volume, obniz) {
    return; 
    const volumeLevels = {
        'pp': 0.2,
        'p': 0.4,
        'mp': 0.6,
        'mf': 0.8,
        'f': 1.0,
        'ff': 1.2
    };

    // Adjust volume by changing PWM duty
    if (volumeLevels[volume]) {
        obniz.pwm0.start({ duty: volumeLevels[volume] });
    } else {
        obniz.pwm0.start({ duty: 0.8 });  // Default to 'mf' if volume is not specified
    }
}

function noteToFrequency(note) {
    const notes = {
        'C4': 261.63,
        'C#4': 277.18,
        'Db4': 277.18,
        'D4': 293.66,
        'D#4': 311.13,
        'Eb4': 311.13,
        'E4': 329.63,
        'F4': 349.23,
        'F#4': 369.99,
        'Gb4': 369.99,
        'G4': 392.00,
        'G#4': 415.30,
        'Ab4': 415.30,
        'A4': 440.00,
        'A#4': 466.16,
        'Bb4': 466.16,
        'B4': 493.88,
        'C5': 523.25,
        'C#5': 554.37,
        'Db5': 554.37,
        'D5': 587.33,
        'D#5': 622.25,
        'Eb5': 622.25,
        'E5': 659.25,
        'F5': 698.46,
        'F#5': 739.99,
        'Gb5': 739.99,
        'G5': 783.99,
        'G#5': 830.61,
        'Ab5': 830.61,
        'A5': 880.00,
        'A#5': 932.33,
        'Bb5': 932.33,
        'B5': 987.77,
    };

    return notes[note];
}

```

## 楽譜データフォーマットの仕様書
以下の仕様書はChatGPTが生成したものである。

### 概要
この仕様書では、楽譜データフォーマットの詳細を定義します。このフォーマットは、JavaScriptプログラムを用いてobnizデバイス上で圧電スピーカーにより音楽を再生するためのものです。MusicXMLの要素を簡略化して使用します。(違うと思う)

### フォーマット
simpleMusicFormat は以下の要素で構成されます：

- Tempo: テンポ（BPM、例: 120）
- TimeSignature: 拍子（例: 4/4）
- Notes: 音符のリスト

#### テンポ
テンポは1分間に何拍あるかを示すBPM（Beats Per Minute）で指定します。指定方法は以下の通りです：
```
Tempo: 120
```
#### 拍子
拍子は「分子/分母」の形式で指定します。指定方法は以下の通りです：
```
TimeSignature: 4/4
```
#### 音符
音符のリストは「音名 音価 音量」の形式で指定します。音符はカンマで区切ります。音名、音価、音量の詳細は以下の通りです。

##### 音名
音名は以下の形式で指定します：

```
A4, A#4, Bb4, B4, C4, ...
```
##### 音価
音価は以下の形式で指定します：

```
全音符: 1
二分音符: 1/2
四分音符: 1/4
八分音符: 1/8
十六分音符: 1/16
```
##### 休符
休符は「Rest 音価」の形式で指定します。例：
```
Rest 1/4
```
##### タイ
タイは音符を結びつけるために使用します。「Tie」で指定します。例：
```
A4 1/4, Tie, A4 1/4
```
##### 3連符
3連符は「Triplet」のキーワードを使用して指定します。3連符の開始と終了を明示的に示します。例：
```
Triplet, C4 1/8, D4 1/8, E4 1/8, EndTriplet
```
##### 繰り返し記号
繰り返し記号は「RepeatStart」と「RepeatEnd」で指定します。例：
```
RepeatStart, A4 1/4, B4 1/4, RepeatEnd
```

##### 音量
音量は以下の形式で指定します：

```
pp, p, mp, mf, f
```

##### サンプルデータ
```
Tempo: 90
TimeSignature: 4/4
Notes: G4 1/4, A4 1/4, B4 1/4, G4 1/4, E4 1/4, E4 1/4, D4 1/4, C4 1/4, B4 1/4, C5 1/4, G4 1/4, E4 1/4, F#4 1/4, G4 1/4, A4 1/4, G4 1/4, F#4 1/4, E4 1/4, D4 1/2, RepeatStart, G4 1/4, A4 1/4, RepeatEnd, Triplet, C4 1/8, D4 1/8, E4 1/8, EndTriplet, F4 1/4, G4 1/4
```
この例では、テンポが90 BPM、拍子が4/4であり、各音符の音名、音価が指定されています。また、タイ、3連符、繰り返し記号も含まれています。

#### 注意事項
- 繰り返し記号は、`RepeatStart`と`RepeatEnd`で囲まれた部分を繰り返します。
- `Tie`は前の音符と次の音符を結びつけ、持続時間を合計します。
- `Triplet`は3連符の音符を指定します。指定された音価を3で割った時間で再生します。

このフォーマットを使用することで、様々な音楽データを効率的に記述し、再生することが可能です。