見出し画像

生成AI Claude 3.5 sonnetでボイスチェンジャーwebアプリを作成する!

前書き

昨今、生成AIを利用したwebアプリ自動生成試行が多く行われています。今回は、Claude 3.5 sonnetを利用して、webブラウザで動作するヴォイスチェンジャーを自動生成してみます。
うまく生成できれば、youtube生配信時などで専用のアプリを入れなくてもこれで十分、となるかもしれません。

生成してもらった!

ということで、早速Claude と何ターンか対話して、30分ぐらいで以下機能を持つボイスチェンジャーができました!(HTMLコードはページの最後に)
・音量調整
・ミュート
・ピッチシフト(男性、女性の声ボタン付き)
・リバーブ
・スペクトルアナライザ

webアプリ画面

画面イメージは以下の通り。素晴らしい!きちんと動作します。

生成されたhtml

生成されたhtmlは次の通りです。HTMLにjavascript,cssも収めるよう指示したので、この1ファイルのみで動作します。(tone.jsという外部モジュールを組み込んでいます)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>リアルタイム音声エフェクトアプリ</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 20px;
            background-color: #f0f4f8;
            color: #333;
            max-width: 800px;
            margin: 0 auto;
        }
        h1 {
            color: #2c3e50;
            margin-bottom: 20px;
            font-size: 24px;
        }
        .control-panel {
            display: flex;
            flex-direction: column;
            width: 100%;
        }
        .slider-container {
            background-color: white;
            padding: 10px;
            border-radius: 10px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            margin-bottom: 10px;
        }
        input[type="range"] {
            width: 100%;
            margin-top: 5px;
        }
        button {
            padding: 8px 16px;
            font-size: 14px;
            background-color: #3498db;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: background-color 0.3s;
            margin: 5px;
        }
        button:hover {
            background-color: #2980b9;
        }
        .button-group {
            display: flex;
            justify-content: space-between;
            margin-bottom: 10px;
        }
        #spectrum {
            width: 100%;
            height: 150px;
            background-color: black;
            margin-top: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }
    </style>
</head>
<body>
    <h1>リアルタイム音声エフェクトアプリ</h1>
    <div class="control-panel">
        <div class="button-group">
            <button id="startButton">マイク入力開始</button>
            <button id="muteButton">ミュート</button>
        </div>
        <div class="slider-container">
            <label for="volumeSlider">音量: <span id="volumeValue">0</span> dB</label>
            <input type="range" id="volumeSlider" min="-60" max="0" step="1" value="0">
        </div>
        <div class="slider-container">
            <label for="pitchSlider">ピッチシフト: <span id="pitchValue">0</span> 半音</label>
            <input type="range" id="pitchSlider" min="-12" max="12" step="1" value="0">
        </div>
        <div class="button-group">
            <button id="maleVoice">男性の声</button>
            <button id="normalVoice">通常の声</button>
            <button id="femaleVoice">女性の声</button>
        </div>
        <div class="slider-container">
            <label for="reverbSlider">リバーブ: <span id="reverbValue">0.5</span></label>
            <input type="range" id="reverbSlider" min="0" max="1" step="0.1" value="0.5">
        </div>
    </div>
    <canvas id="spectrum"></canvas>

    <script>
        let mic, volume, reverb, pitchShift, analyser;
        let canvas, canvasContext;
        let isMuted = false;

        async function startAudio() {
            await Tone.start();
            console.log('Audio is ready');

            mic = new Tone.UserMedia();
            volume = new Tone.Volume(0);
            reverb = new Tone.Reverb({wet: 0.5});
            pitchShift = new Tone.PitchShift(0);
            analyser = new Tone.Analyser('fft', 1024);

            mic.chain(volume, pitchShift, reverb, analyser);
            analyser.toDestination();

            try {
                await mic.open();
                console.log('Mic is open');
                document.getElementById('startButton').textContent = 'マイク入力中';
                document.getElementById('startButton').disabled = true;
                drawSpectrum();
            } catch (e) {
                console.log('Could not open mic: ', e);
            }
        }

        function drawSpectrum() {
            requestAnimationFrame(drawSpectrum);
            const spectrum = analyser.getValue();
            canvasContext.clearRect(0, 0, canvas.width, canvas.height);
            for (let i = 0; i < spectrum.length; i++) {
                const x = i / spectrum.length * canvas.width;
                const height = (spectrum[i] + 140) * 2;
                const hue = i / spectrum.length * 360;
                canvasContext.fillStyle = `hsl(${hue}, 100%, 50%)`;
                canvasContext.fillRect(x, canvas.height - height, canvas.width / spectrum.length, height);
            }
        }

        document.getElementById('startButton').addEventListener('click', startAudio);

        document.getElementById('muteButton').addEventListener('click', () => {
            isMuted = !isMuted;
            volume.mute = isMuted;
            document.getElementById('muteButton').textContent = isMuted ? 'ミュート解除' : 'ミュート';
        });

        document.getElementById('volumeSlider').addEventListener('input', (e) => {
            volume.volume.value = parseFloat(e.target.value);
            document.getElementById('volumeValue').textContent = e.target.value;
        });

        document.getElementById('pitchSlider').addEventListener('input', (e) => {
            pitchShift.pitch = parseFloat(e.target.value);
            document.getElementById('pitchValue').textContent = e.target.value;
        });

        document.getElementById('reverbSlider').addEventListener('input', (e) => {
            reverb.wet.value = parseFloat(e.target.value);
            document.getElementById('reverbValue').textContent = e.target.value;
        });

        document.getElementById('maleVoice').addEventListener('click', () => {
            pitchShift.pitch = -5;
            document.getElementById('pitchSlider').value = -5;
            document.getElementById('pitchValue').textContent = -5;
        });

        document.getElementById('normalVoice').addEventListener('click', () => {
            pitchShift.pitch = 0;
            document.getElementById('pitchSlider').value = 0;
            document.getElementById('pitchValue').textContent = 0;
        });

        document.getElementById('femaleVoice').addEventListener('click', () => {
            pitchShift.pitch = 5;
            document.getElementById('pitchSlider').value = 5;
            document.getElementById('pitchValue').textContent = 5;
        });

        window.onload = () => {
            canvas = document.getElementById('spectrum');
            canvasContext = canvas.getContext('2d');
            canvas.width = 800;
            canvas.height = 150;
        };
    </script>
</body>
</html>

この記事が気に入ったらサポートをしてみませんか?