ルーパー向けトーンジェネレーターの開発

## はじめに
　この記事では、ループステーションデバイスの1つであるBOSS RC-505mkIIとの連携に特化し、声の代わりに一定周波数の音をデバイスに入力するためのWebサイト「ルーパー向けトーンジェネレータ」の開発記録についてまとめます。


https://looper-tone.replit.app/

## プロジェクトの目標
　このプロジェクトの主な目的は、ループステーションのプレイヤー(以下、ルーパー)の練習時に、OSC BOTなどのエフェクトを使用するために「あーーー」といった声を出す必要がある問題を解決することです。特に、大きな声を出すことによる近隣への配慮が求められる状況を想定しています。
　その解決策として、ルーパーの声の代わりにパソコンから直接デバイスへ音を入力するアプローチを取りました。これにより、声を出さずに済むため、周囲を気にすることなく音作りに集中できるようになります。

## 技術スタック
　このアプリケーションは、以下の技術スタックで構築・運用されています。開発とホスティングには、オンライン統合開発環境である**Replit**を利用しました。

*   **HTML (HyperText Markup Language)**: ウェブページの構造を記述します。
*   **CSS (Cascading Style Sheets)**: ウェブページのデザインを整えます。
*   **JavaScript**: ウェブページの動的な機能を実装します。
*   **Web Audio API**: JavaScriptの機能の一部で、ブラウザ上で高度な音声処理（今回の場合は音の生成や制御）を可能にします。

## ソースコードの解説

　このトーンジェネレータは、以下の3つのファイルで構成されています。ウェブサイト制作の基本となる組み合わせですね。

*   `index.html` : ウェブページの構造。「何をどこに表示するか」を定義。
*   `style.css` : ウェブページの装飾、つまり「どのように見えるか」を指定。
*   `script.js` : ウェブページの動作、つまり「ルーパーの操作にどう反応するか」を記述。

　**この記事の最後に、上記3つのソースコードをすべて記述しています。**


　それぞれのファイルが具体的にどう連携しているのか解説していきます。

**1**\. **index.html - ウェブページの骨組み**

　HTMLファイルは、ウェブページに表示されるテキスト、ボタン、選択肢などの要素を配置し、それらの構造を定義する設計図のようなものです。このトーンジェネレータの `index.html` は、ユーザーが音の設定をしたり、RC-505mkIIとの接続方法を確認したりするためのインターフェースを提供します。

**主な構成要素:**

 *   **コントロールパネル (`<div class="controls">`)**:
 　音を生成し制御するための操作に関するインターフェースが集まるセクションです。
　
* **音階とオクターブ選択**



``` html
<div class="control-group">
    <label>音階とオクターブ:</label>
    <div class="note-select">
        <select id="note">
            <option value="C">C</option>
            <option value="D♭">D♭</option>
            <option value="D">D</option>
            <option value="E♭">E♭</option>
            <option value="E">E</option>
            <option value="F">F</option>
            <option value="F♯">F♯</option>
            <option value="G">G</option>
            <option value="A♭">A♭</option>
            <option value="A" selected>A</option>
            <option value="B♭">B♭</option>
            <option value="B">B</option>
        </select>
        <select id="octave">
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
            <option value="4" selected>4</option>
            <option value="5">5</option>
            <option value="6">6</option>
            <option value="7">7</option>
            <option value="8">8</option>
            <option value="9">9</option>
        </select>
    </div>
</div>
```
　`<select>` タグはドロップダウンリストを作成します。ここでユーザーは音階（C, D♭, Dなど）とオクターブ（1〜9）を選べます。`id="note"` や `id="octave"` というのは、要素に固有の名札のようなものです。後でJavaScriptが「`note`という名札の要素の値を取得してこい！」というように、特定の要素を簡単に見つけて操作するために使います。

 * **周波数入力**
```html
<input type="number" id="frequency" min="16.35" max="7902.13" value="440" step="0.01">
```
数値を直接入力して周波数を指定できる欄です。`min`（最小値）、`max`（最大値）、`value`（初期値）、`step`（増減の刻み幅）属性で入力の振る舞いを制御しています。
　
*   **波形選択** 
```html
<div class="control-group">
    <label for="waveform">波形:</label>
    <select id="waveform">
        <option value="sine">正弦波</option>
        <option value="square">方形波</option>
        <option value="sawtooth">のこぎり波</option>
        <option value="triangle">三角波</option>
    </select>
</div>
```
　音の波形（正弦波、方形波、のこぎり波、三角波）を選択。
　
*   **音量調整**
```html
<div class="control-group">
    <label for="volume">音量:</label>
    <div class="volume-control">
        <div class="volume-buttons">
            <button id="volumeDown">-</button>
            <button id="volumeUp">+</button>
        </div>
        <input type="range" id="volume" min="0" max="100" value="50">
        <span id="volumeValue">50%</span>
    </div>
</div>
```
　スライダー (`<input type="range">`) とプラスボタンとマイナスボタンで直感的に音量を調整する。現在の音量パーセンテージを `<span id="volumeValue">` で表示する。
　
*   **再生モード選択**
```html
<div class="control-group">
    <label for="mode">再生モード:</label>
    <select id="mode">
        <option value="hold">ホールド (押している間)</option>
        <option value="toggle">トグル (切り替え)</option>
    </select>
</div>
```
　音の再生方法を「ホールド（ボタンやキーを押している間だけ鳴る）」か「トグル（ボタンやキーを押すたびに再生/停止が切り替わる）」を選択できる。
　
* **再生ボタン**
```html
<button id="playButton" class="play">再生</button>
``` 
　実際に音を再生・停止するための主要なボタンを表示している。`class="play"` は初期状態の見た目をCSSで指定するために使用する。
　
*   **ステータス表示**
```html
<div id="status">停止中</div>
```
現在の再生状態（再生中/停止中）を表示する。
　
*   **操作説明**
```html
<p class="play-instruction">音を再生するには、<strong>スペースキー</strong>または<strong>再生ボタン</strong>を押してください。</p>
```
　スペースキーでも操作できることをユーザーに案内しています。
　
*   **説明書セクション (`<div class="instructions">`)**:
　RC-505mkIIとこのトーンジェネレータを連携させるための具体的な設定手順や、トラブルシューティングの情報が記載されています。こちらは主にテキストとリストで構成され、ユーザーをサポートします。
　
*   **JavaScriptファイルの読み込み**:
    ```html
    <script src="script.js"></script>
    </body>
    ```
　`<body>` タグの閉じタグの直前にこの行を記述するのが一般的です。これにより、HTMLの要素がすべてページに読み込まれ、準備が整った状態で `script.js` が実行されるため、JavaScriptがHTML要素を確実に見つけて操作できるようになります。

**2**\. **style.css - 見た目を整える**

　CSSファイルは、HTMLで作成された要素が「どのように見えるか」を指定します。色、フォント、サイズ、配置などを調整し、ページ全体を美しく、使いやすくデザインする役割を担います。

**主なスタイルのポイント:**

*   **基本スタイル (`body`)**
　ページ全体のデフォルトフォント、背景色、余白などを設定し、一貫性のあるデザインの基礎を作ります。
 　
*   **レイアウト (`.container`, `.controls`, `.instructions`)**
    ```css
    .container {
      display: flex; /* 要素を横並びにするための強力なレイアウトモード */
      width: 100%;
      max-width: 1200px; /* 画面が広すぎてもコンテンツが間延びしないように最大幅を設定 */
      /* ... */
    }
    .controls {
      flex: 1; /* .container 内での幅の割合 (この場合は1) */
      /* ... */
    }
    .instructions {
      flex: 2; /* .container 内での幅の割合 (この場合は2、controlsより広く) */
      /* ... */
    }
    ```
    　`display: flex;` は、近年のWebサイトのレイアウト設計で非常によく使われる機能です。これにより、コントロールパネルと説明書セクションを柔軟に横並びに配置し、それぞれの幅の比率も簡単に指定できます。
　
*   **コントロール要素のスタイル**
　ラベル (`<label>`)、入力欄 (`<input>`, `<select>`)、ボタン (`<button>`) などが、見やすく操作しやすいように、個別にスタイルが設定されています。
　
*   **再生ボタンの視覚的フィードバック**
    ```css
    button#playButton.play { /* 再生ボタンが "play" クラスを持つとき (再生可能な状態) */
      background-color: #4CAF50; /* 緑色 */
      color: white;
    }
    button#playButton.stop { /* 再生ボタンが "stop" クラスを持つとき (停止可能な状態) */
      background-color: #f44336; /* 赤色 */
      color: white;
    }
    ```
　再生ボタンの状態（再生可能か、停止可能か）に応じて背景色が変わるように、CSSクラス (`.play`, `.stop`) を利用しています。これらのクラスは、後述するJavaScriptによって動的に付け替えられます。これにより、ボタンの現在の機能を直感的に理解できるようになります。
*   **アニメーション効果**
    ```css
    .controls.playing { /* .controls要素に "playing" クラスが付いているとき */
      animation: pulse 2s infinite; /* "pulse"という名前のアニメーションを2秒間隔で無限に繰り返す */
    }
    @keyframes pulse { /* "pulse" アニメーションの定義 */
      0%   { background-color: #fafafa; } /* 開始時 */
      50%  { background-color: #e6f3e6; } /* 中間地点で薄い緑色に変化 */
      100% { background-color: #fafafa; } /* 終了時 (元の色に戻る) */
    }
    ```
　音声再生中にコントロールパネルの背景がゆっくりと点滅するようなアニメーション（`pulse`）や、ステータス表示 (`#status`) が点滅するアニメーション（`blink`）を加えています。これらは視覚的なフィードバックとして、ユーザーに「今、音が鳴っているな」と伝えるのに役立ちます。
*   **レスポンシブデザイン**
    ```css
    @media (max-width: 900px) { /* 画面の幅が900ピクセル以下の場合に適用 */
      .container {
        flex-direction: column; /* 要素を縦並びにする */
      }
      /* ... 他の調整 ... */
    }
    ```
　`@media` クエリは、特定の条件（ここでは画面幅）に応じて異なるスタイルを適用するためのCSSの機能です。これにより、PCのような広い画面ではコントロールパネルと説明書が横並びになり、スマートフォンなどの狭い画面では縦並びになるようにレイアウトが自動的に変更されます。どんなデバイスからアクセスしても快適に利用できるようにするための重要な配慮です。

**3**\. **script.js - 動きと音の制御**

　JavaScriptファイルは、ウェブページに動作を組み込む役割を担います。ユーザーの操作（ボタンクリックなど）に反応したり、HTMLの表示内容を動的に変更したり、そしてこのトーンジェネレータの核心である「音を生成し制御する」処理を実行します。

**主な機能とコードのポイント**

*   **Web Audio APIの初期設定**
    ```javascript
    const ctx = new (window.AudioContext || window.webkitAudioContext)();
    let oscillator = null; // 音波を実際に発生させる装置 (オシレーター)
    let gainNode = null;   // 音量を調節する装置 (ゲインノード)
    let isPlaying = false; // 現在、音が再生中かどうかを記憶する変数 (フラグ)
    ```
    *   `AudioContext`: Web Audio API を使うための「作業場」のようなものです。これを通じて音に関する様々な操作を行います。
    `window.AudioContext || window.webkitAudioContext` は、古いブラウザにも対応するための記述です。
    *   `oscillator`: 特定の周波数・波形の音波を周期的に生成するノード（部品）です。これが「音源」となります。
    *   `gainNode`: 音量を制御するためのノードです。オシレーターで作られた音を大きくしたり小さくしたりします。
    *   `isPlaying`: 音が現在再生されているかどうかの状態を管理するための変数です。`true`なら再生中、`false`なら停止中を表します。
　
*   **HTML要素の取得**
    ```javascript
    const playButton = document.getElementById('playButton');
    const frequencyInput = document.getElementById('frequency');
    // ... 他の多くのコントロール要素も同様に取得 ...
    ```
　`document.getElementById('ID名')` を使って、HTMLファイル内で `id` 属性を使って名付けられた各UI要素（ボタン、入力欄など）を参照し、その値を取得します。この参照を通じて、JavaScriptから要素の値を読み取ったり、見た目を変えたり、クリックなどのイベントを監視したりできるようになります。

*   **音階とオクターブから周波数を計算 (`calculateFrequency`)**
    ```javascript
    // 音階名と基準(A4)からの半音差を対応付けるオブジェクト
    const noteOffsets = { 'C': 0, /* ... */ 'A': 9, /* ... */ };
    const A4_FREQ = 440; // A4 (ラの音) の周波数は440Hz
    const A4_OCTAVE = 4; // A4 のオクターブ
    const A4_NOTE_OFFSET = noteOffsets['A']; // Aのオフセット値

    function calculateFrequency(note, octave) {
      const noteOffset = noteOffsets[note];
      // 基準となるA4からどれだけ半音ズレているかを計算
      const totalOffset = (octave - A4_OCTAVE) * 12 + (noteOffset - A4_NOTE_OFFSET);
      // 12乗根2 を使って周波数を計算 (1オクターブ上がると周波数は2倍になる)
      return A4_FREQ * Math.pow(2, totalOffset / 12);
    }
    ```
　ユーザーがドロップダウンで選択した音階とオクターブの値を基に、その音の正確な周波数（Hz）を計算します。まず、基準となる音（ここではA4=440Hz）があり、他の音の高さ（周波数）はそれとの相対的な関係で決まります。この関数はその計算を行っています。

*   **音声再生処理 (`startTone`)**:
    ```javascript
    function startTone() {
      if (oscillator) { // もし既に音源(oscillator)が存在すれば、一度停止・破棄する
        oscillator.stop();
      }
      oscillator = ctx.createOscillator(); // AudioContextから新しいオシレーターを作成
      gainNode = ctx.createGain();       // AudioContextから新しいゲインノードを作成

      // ユーザーがUIで設定した値をオシレーターとゲインノードに適用
      oscillator.type = waveformSelect.value; // 波形の種類 (sine, squareなど)
      oscillator.frequency.setValueAtTime(parseFloat(frequencyInput.value), ctx.currentTime); // 周波数
      gainNode.gain.setValueAtTime(volumeInput.value / 100, ctx.currentTime); // 音量 (0.0 〜 1.0の範囲)

      // 音の信号が流れる経路を構築
      oscillator.connect(gainNode); // オシレーターの出力をゲインノードの入力に接続
      gainNode.connect(ctx.destination); // ゲインノードの出力を最終的な出力先(スピーカーなど)に接続

      oscillator.start(); // オシレーターの再生を開始
      isPlaying = true;   // 再生状態フラグをtrueに

      // UI（ボタンのテキスト、ステータス表示など）を「再生中」の状態に更新
      status.textContent = '再生中';
      status.classList.add('playing'); // CSSクラスを追加して見た目も変更
      controls.classList.add('playing');
      playButton.textContent = '停止';
      playButton.classList.remove('play');
      playButton.classList.add('stop');
    }
    ```
　この関数が呼ばれると、以下のステップで音が鳴り始めます。
**1**\.　`AudioContext` に新しい `OscillatorNode` (音源) と `GainNode` (音量調節器) を作ってもらいます。
**2**\.　ユーザーがUIで設定した波形、周波数、音量を、作成したオシレーターとゲインノードにそれぞれ設定します。`ctx.currentTime` は、音声処理の正確なタイミングを指定するために使われます。
**3**\.　`connect()` メソッドを使って、音の部品を繋ぎ合わせます。
　`oscillator.connect(gainNode)` は「オシレーターで作った音を、ゲインノードに通せや」という指示です。次に `gainNode.connect(ctx.destination)` で「ゲインノードで調整した音を、最終的にスピーカー（またはRC-505mkIIへのUSBオーディオ出力）から出せや」と指示します。
　このように音の信号が流れる経路を構築するのがWeb Audio APIの基本的な考え方です。
 **4**\.　`oscillator.start()` で実際に音の再生を開始します。
 **5**\.　`isPlaying` フラグを `true` にし、ボタンの表示などを「再生中」の状態に更新して、ユーザーにフィードバックします。

*   **音声停止処理 (`stopTone`)**
    ```javascript
    function stopTone() {
      if (oscillator) { // オシレーターが存在すれば（＝再生中なら）
        oscillator.stop(); // 音の再生を停止
        oscillator = null; // 使用済みのオシレーターを破棄 (メモリ解放のため)
        gainNode = null;   // 同様にゲインノードも破棄
        isPlaying = false; // 再生状態フラグをfalseに

        // UIを「停止中」の状態に更新
        status.textContent = '停止中';
        status.classList.remove('playing');
        controls.classList.remove('playing');
        playButton.textContent = '再生';
        playButton.classList.remove('stop');
        playButton.classList.add('play');
      }
    }
    ```
    再生中の音を停止し、使用していたオシレーターとゲインノードを破棄（クリーンアップ）します。UIも停止状態に戻します。
　
*   **イベントリスナーの設定**:
    ```javascript
    // 再生ボタンがクリックされたときの処理を登録
    playButton.addEventListener('click', () => {
      // 再生モードが 'toggle' (切り替え) の場合
      if (modeSelect.value === 'toggle') {
        if (isPlaying) { // もし再生中なら停止
          stopTone();
        } else { // もし停止中なら再生開始
          startTone();
        }
      }
      // 'hold' (押している間) モードのクリック処理は mousedown/mouseup で別途定義
    });

    // 音階セレクトボックスの値が変更されたら周波数を更新する処理を登録
    noteSelect.addEventListener('change', updateFrequency);
    octaveSelect.addEventListener('change', updateFrequency);

    // 音量スライダーが操作されたら音量を更新する処理を登録
    volumeInput.addEventListener('input', () => {
      updateVolumeDisplay(); // 画面上の音量表示を更新
      if (isPlaying && gainNode) { // もし再生中でゲインノードが存在すれば
        // 実際の音量もリアルタイムに変更
        gainNode.gain.setValueAtTime(volumeInput.value / 100, ctx.currentTime);
      }
    });
    ```
　`addEventListener` とは、ウェブページ上の要素に対して「この操作（ボタンクリックなど）を検知したら、この処理を実行しとけよ」とお願いしておく仕組みです。これにより、ユーザーの様々な操作（ボタンのクリック、セレクトボックスの値の変更、スライダーのドラッグなど）に応じて、対応するJavaScriptの関数が実行されます。
* **再生ボタン**
　再生モードが「トグル」ならクリックのたびに再生/停止を切り替えます。「ホールド」モードの場合は、マウスボタンを押したとき (`mousedown`) に再生開始、離したとき (`mouseup`) に停止するように、別のイベントリスナーで制御しています。
　
*   **音階・オクターブ・周波数・波形**
　これらの設定値が変更されると、`updateFrequency` 関数が呼ばれて周波数が再計算されたり、再生中であれば音の特性が即座に変更されたりします。
　
*   **音量**
　スライダーや「+」「-」ボタンで音量が変更されると、画面上のパーセント表示が更新され、再生中であれば実際の音量もリアルタイムに変わります。
　
*   **キーボード操作（スペースキー）への対応**:
    ```javascript
    document.addEventListener('keydown', (event) => { // キーが押された瞬間のイベント
      if (event.code === 'Space') { // もし押されたキーがスペースキーなら
        event.preventDefault(); // スペースキーによるページのスクロールなど、ブラウザのデフォルト動作を抑制
        // 再生モードに応じた再生/停止処理 (トグル/ホールド)
        // ... (playButtonのクリック時と似たロジック)
      }
    });
    document.addEventListener('keyup', (event) => { // キーが離された瞬間のイベント
      if (event.code === 'Space' && modeSelect.value === 'hold') { // ホールドモードでスペースキーが離されたら
        stopTone(); // 音を停止
      }
    });
    ```
　スペースキーを押すことでも再生/停止ができるように、キーボードイベントにも対応しています。`event.preventDefault()` は、意図しないブラウザの標準動作（例えばスペースキーでページがスクロールするなど）を防ぐために重要です。

*   **タッチイベントへの対応**:
　スマートフォンやタブレットなどのタッチデバイスでも快適に操作できるように、再生ボタンには `touchstart` イベントリスナーも追加しています。これにより、タップ操作でも再生/停止が機能します。

## まとめ
　これらのHTML、CSS、JavaScriptのファイルが互いに連携し合うことで、ブラウザ上で動作し、RC-505mkIIと組み合わせて使えるトーンジェネレータが実現されています。
 
　本プロジェクトの目的は、ループステーションの練習時にOSC BOTなどのエフェクトを使用する際に、「あーーー」といった声を出す必要がある状況を避け、PCから直接RC-505mkIIへテスト用の音を入力できるようにすることでした。
 
　その実現のため、HTMLで音階、オクターブ、周波数、波形、音量、再生モードといった各種設定を行うためのコントロールパネルの構造を定義し、CSSによってこれらの要素が見やすく、操作しやすく、かつPCやスマートフォンなど異なる画面サイズにも対応するデザインを施しました。
 
　そして、JavaScriptがこれらのUIと連動し、選択された値に基づいて音の周波数を計算したり、再生/停止の動作を制御したりしています。具体的には、再生ボタンやスペースキーに応じた再生停止の処理や、音量スライダーの変更をリアルタイムで反映させたりといった、円滑なコントロールを実現する機能を実現しています。
 
　この音声処理の核心を担うのが、script.js で活用されている Web Audio API です。このAPIを用いることで、指定された周波数と波形の音をブラウザ上で動的に生成し、音量を調整して出力することが可能になりました。
 
　結果として、ルーパーは特別なソフトウェアをインストールすることなく、ウェブブラウザを通じて手軽に生成した音源をRC-505mkIIに入力できるようになり、周囲を気にせず音作りに集中できる環境を実現しました。

　
**どなたでも利用できますので、ご自由にお使いください。**

https://looper-tone.replit.app/

## ソースコード
　このアプリケーションを構成するソースコードです。
　3つのファイルを同じ階層に配置することで動作します。
　改造したい方はご自由にどうぞ。
　
<details><summary>index.html 全文はこちら</summary>

```html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ルーパー向けトーンジェネレータ</title>
  <link href="style.css" rel="stylesheet" type="text/css" />
</head>

<body>
  <div class="container">
    <div class="controls" id="controls">
      <h3>ルーパー向け<h3>
          <h2>トーンジェネレータ</h2>
          <div class="control-group">
            <label>音階とオクターブ:</label>
            <div class="note-select">
              <select id="note">
                <option value="C">C</option>
                <option value="D♭">D♭</option>
                <option value="D">D</option>
                <option value="E♭">E♭</option>
                <option value="E">E</option>
                <option value="F">F</option>
                <option value="F♯">F♯</option>
                <option value="G">G</option>
                <option value="A♭">A♭</option>
                <option value="A" selected>A</option>
                <option value="B♭">B♭</option>
                <option value="B">B</option>
              </select>
              <select id="octave">
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
                <option value="4" selected>4</option>
                <option value="5">5</option>
                <option value="6">6</option>
                <option value="7">7</option>
                <option value="8">8</option>
                <option value="9">9</option>
              </select>
            </div>
          </div>
          <div class="control-group">
            <label for="frequency">周波数 (Hz):</label>
            <input type="number" id="frequency" min="16.35" max="7902.13" value="440" step="0.01">
          </div>
          <div class="control-group">
            <label for="waveform">波形:</label>
            <select id="waveform">
              <option value="sine">正弦波</option>
              <option value="square">方形波</option>
              <option value="sawtooth">のこぎり波</option>
              <option value="triangle">三角波</option>
            </select>
          </div>

          <div class="control-group">
            <label for="volume">音量:</label>
            <div class="volume-control">
              <div class="volume-buttons">
                <button id="volumeDown">-</button>
                <button id="volumeUp">+</button>
              </div>
              <input type="range" id="volume" min="0" max="100" value="50">
              <span id="volumeValue">50%</span>
            </div>
          </div>
          <div class="control-group">
            <label for="mode">再生モード:</label>
            <select id="mode">
              <option value="hold">ホールド (押している間)</option>
              <option value="toggle">トグル (切り替え)</option>
            </select>
          </div>
          <button id="playButton" class="play">再生</button>
          <div id="status">停止中</div>
          <p class="play-instruction">音を再生するには、<strong>スペースキー</strong>または<strong>再生ボタン</strong>を押してください。</p>
          <div class="footer">
            <p>開発者: arinomi (<a href="https://twitter.com/arinomi_loop" target="_blank">@arinomi_loop</a>)</p>
          </div>
    </div>
    <div class="instructions">
      <h3>RC-505mkIIでの設定方法</h3>
      <p>「ルーパー向けトーンジェネレーター」は、BOSS RC-505mkIIループステーションと組み合わせて、トーン音をループトラックに録音したりエフェクトをかけたりするために設計されています。以下の手順で設定してください。
      </p>
      <h4>必要なもの</h4>
      <ul>
        <li>BOSS RC-505mkII</li>
        <li>パソコン (Windows または Mac)</li>
        <li>USBケーブル (Aタイプ - Bタイプ)</li>
        <li>Webブラウザ (このサイトを閲覧するため)</li>
      </ul>
      <h4>設定手順</h4>
      <ol>
        <li><strong>パソコンとRC-505mkIIを接続</strong><br>付属のUSBケーブルを使用して、パソコンのUSBポートとRC-505mkII背面のUSB端子を接続します。</li>
        <li>
          <strong>パソコンの音声出力を設定</strong><br>パソコンからの音声がRC-505mkIIに出力されるようにします。<br><br><em>Windows:</em><br>タスクバーのスピーカーアイコンを右クリックし、「サウンド設定」を選択。出力デバイスで「RC-505mkII」を選択します。<br><br><em>Mac:</em><br>「システム環境設定」→「サウンド」→「出力」タブで「RC-505mkII」を選択します。
        </li>
        <li><strong>RC-505mkIIのUSBオーディオ設定</strong><br>パソコンからの音声をループトラックで使用できるように設定します。<br><br>
          - [MENU] ボタンを押してシステム設定を開きます。<br>
          - [◀] [▶] ボタンを使用して「USB」の項目を探す。<br>
          - 「USB」を選択する<br>
          - 「ROUTING」のつまみを右端に回して「LOOP IN」に設定。<br><br>
          <strong>「LOOP IN」の役割:</strong> USB経由の音声（このトーンジェネレーターの出力）がループトラックの入力として扱われ、録音やインプットFXの適用が可能になります。（参考:
          RC-505mkII パラメーター・ガイド p.28）<br><br>
          「INPUT LEVEL」を調整し、レベルメーターを見ながら音量を最適化します。（参考: RC-505mkII 取扱説明書 p.18）<br><br>
          [EXIT] ボタンを数回押してプレイ画面に戻ります。
        </li>
        <li><strong>トーンジェネレーターで音を再生</strong><br>このサイトの左側パネルで音階、オクターブ、波形、音量を設定し、「再生」ボタンまたはスペースキーで音を鳴らします。</li>
        <li><strong>RC-505mkIIで録音・エフェクト適用</strong><br>
          - INPUT FX セクションでエフェクト（例: OSC BOT）を選択。<br>
          - INPUT FXの「INSERT」が「ALL」に設定されていることを確認。（参考: RC-505mkII パラメーター・ガイド p.5）<br>
          - 録音したいトラックの [►/●] ボタンを押して録音開始。トーンジェネレーターの再生ボタンをクリック又はスペースキーを押下することで、音声が入力されます。INPUT FXをかけることも可能です。（参考:
          RC-505mkII 取扱説明書 p.11）
        </li>
      </ol>
      <h4>トラブルシューティング</h4>
      <ul>
        <li><strong>音が出ない/入力されない:</strong><br>
          - パソコンの出力デバイスが「RC-505mkII」に設定されているか確認。<br>
          - RC-505mkIIの「USB ROUTING」が「LOOP IN」に設定されているか確認。<br>
          - [OUTPUT LEVEL] つまみやスピーカー/ヘッドホンの音量を確認。<br>
          - USBケーブルの接続を確認。
        </li>
        <li><strong>音が歪む:</strong><br>
          - RC-505mkIIの「INPUT LEVEL」を下げて調整。<br>
          - トーンジェネレーターの音量スライダーを下げて調整。
        </li>
      </ul>
    </div>
  </div>

  <script src="script.js"></script>
</body>

</html>
```

</details>
<details><summary>style.css 全文はこちら</summary>

```css
body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background-color: #f4f4f4;
  margin: 0;
  padding: 20px;
  box-sizing: border-box;
  display: flex;
  justify-content: center;
  min-height: 100vh;
}

.container {
  display: flex;
  width: 100%;
  max-width: 1200px;
  background: white;
  border-radius: 15px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.controls {
  flex: 1;
  padding: 30px;
  background: #fafafa;
  transition: background-color 0.3s;
}

.controls.playing {
  animation: pulse 2s infinite;
}

@keyframes pulse {
  0% {
    background-color: #fafafa;
  }

  50% {
    background-color: #e6f3e6;
  }

  100% {
    background-color: #fafafa;
  }
}

.instructions {
  flex: 2;
  padding: 30px;
  overflow-y: auto;
}

h2 {
  color: #333;
  margin-bottom: 20px;
}

.control-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
  color: #555;
}

input[type="number"],
select {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 5px;
  box-sizing: border-box;
}

.note-select {
  display: flex;
  gap: 10px;
}

.note-select select {
  flex: 1;
}

.volume-control {
  display: flex;
  align-items: center;
}

.volume-control input[type="range"] {
  flex: 1;
  margin: 0 10px;
}

.volume-control span {
  min-width: 30px;
  text-align: right;
  color: #333;
}

.volume-buttons {
  display: flex;
  gap: 5px;
}

.volume-buttons button {
  background: #eee;
  border: 1px solid #ddd;
  padding: 5px 10px;
  cursor: pointer;
  border-radius: 5px;
}

.volume-buttons button:hover {
  background: #ddd;
}

button#playButton {
  width: 100%;
  border: none;
  padding: 12px;
  cursor: pointer;
  border-radius: 5px;
  font-size: 16px;
  margin-top: 10px;
  transition: background-color 0.3s, color 0.3s;
}

button#playButton.play {
  background-color: #4CAF50;
  color: white;
}

button#playButton.stop {
  background-color: #f44336;
  color: white;
}

button#playButton.hold-active {
  background-color: #2196F3;
  color: white;
}

button#playButton.play:hover {
  background-color: #45a049;
}

button#playButton.stop:hover {
  background-color: #d32f2f;
}

button#playButton.hold-active:hover {
  background-color: #1976D2;
}

#status {
  text-align: center;
  margin-top: 15px;
  font-weight: bold;
  color: #333;
}

#status.playing {
  color: #4CAF50;
  animation: blink 1s infinite;
}

.play-instruction {
  font-size: 0.85em;
  color: #666;
}

@keyframes blink {
  50% {
    opacity: 0.5;
  }
}

.instructions h3 {
  color: #333;
  margin-bottom: 10px;
}

.instructions p,
.instructions ol,
.instructions ul {
  color: #555;
  line-height: 1.6;
}

.instructions ol,
.instructions ul {
  padding-left: 20px;
}

.instructions li {
  margin-bottom: 10px;
}

.footer {
  text-align: center;
  margin-top: 20px;
  color: #777;
}

.footer a {
  color: #4CAF50;
  text-decoration: none;
}

.footer a:hover {
  text-decoration: underline;
}

@media (max-width: 900px) {
  .container {
    flex-direction: column;
  }

  .controls,
  .instructions {
    flex: none;
    max-height: none;
  }
}
```
</details>
<details><summary>script.js 全文はこちら</summary>

```javascript
const ctx = new (window.AudioContext || window.webkitAudioContext)();
let oscillator = null;
let gainNode = null;
let isPlaying = false;

const controls = document.getElementById('controls');
const playButton = document.getElementById('playButton');
const frequencyInput = document.getElementById('frequency');
const noteSelect = document.getElementById('note');
const octaveSelect = document.getElementById('octave');
const waveformSelect = document.getElementById('waveform');
const volumeInput = document.getElementById('volume');
const volumeValue = document.getElementById('volumeValue');
const volumeUp = document.getElementById('volumeUp');
const volumeDown = document.getElementById('volumeDown');
const modeSelect = document.getElementById('mode');
const status = document.getElementById('status');

const noteOffsets = {
  'C': 0, 'D♭': 1, 'D': 2, 'E♭': 3, 'E': 4, 'F': 5,
  'F♯': 6, 'G': 7, 'A♭': 8, 'A': 9, 'B♭': 10, 'B': 11
};
const A4_FREQ = 440;
const A4_OCTAVE = 4;
const A4_NOTE_OFFSET = noteOffsets['A'];

function calculateFrequency(note, octave) {
  const noteOffset = noteOffsets[note];
  const totalOffset = (octave - A4_OCTAVE) * 12 + (noteOffset - A4_NOTE_OFFSET);
  return A4_FREQ * Math.pow(2, totalOffset / 12);
}

function updateFrequency() {
  const note = noteSelect.value;
  const octave = parseInt(octaveSelect.value);
  const freq = calculateFrequency(note, octave);
  frequencyInput.value = freq.toFixed(2);
  if (isPlaying) {
    oscillator.frequency.setValueAtTime(freq, ctx.currentTime);
  }
}

noteSelect.addEventListener('change', updateFrequency);
octaveSelect.addEventListener('change', updateFrequency);

function updateVolumeDisplay() {
  volumeValue.textContent = `${volumeInput.value}%`;
}

function setVolume(value) {
  volumeInput.value = Math.max(0, Math.min(100, value));
  updateVolumeDisplay();
  if (isPlaying && gainNode) {
    gainNode.gain.setValueAtTime(volumeInput.value / 100, ctx.currentTime);
  }
}

function startTone() {
  if (oscillator) {
    oscillator.stop();
  }
  oscillator = ctx.createOscillator();
  gainNode = ctx.createGain();
  oscillator.type = waveformSelect.value;
  oscillator.frequency.setValueAtTime(parseFloat(frequencyInput.value), ctx.currentTime);
  gainNode.gain.setValueAtTime(volumeInput.value / 100, ctx.currentTime);

  oscillator.connect(gainNode);
  gainNode.connect(ctx.destination);
  oscillator.start();
  isPlaying = true;
  status.textContent = '再生中';
  status.classList.add('playing');
  controls.classList.add('playing');
  playButton.textContent = '停止';
  playButton.classList.remove('play');
  playButton.classList.add('stop');
}

function stopTone() {
  if (oscillator) {
    oscillator.stop();
    oscillator = null;
    gainNode = null;
    isPlaying = false;
    status.textContent = '停止中';
    status.classList.remove('playing');
    controls.classList.remove('playing');
    playButton.textContent = '再生';
    playButton.classList.remove('stop');
    playButton.classList.add('play');
  }
}

playButton.addEventListener('click', () => {
  if (modeSelect.value === 'toggle') {
    if (isPlaying) {
      stopTone();
    } else {
      startTone();
    }
  }
});

playButton.addEventListener('mousedown', () => {
  if (modeSelect.value === 'hold' && !isPlaying) {
    startTone();
  }
});

playButton.addEventListener('mouseup', () => {
  if (modeSelect.value === 'hold' && isPlaying) {
    stopTone();
  }
});

playButton.addEventListener('mouseleave', () => {
  if (modeSelect.value === 'hold' && isPlaying) {
    stopTone();
  }
});

document.addEventListener('keydown', (event) => {
  if (event.code === 'Space') {
    event.preventDefault();
    if (modeSelect.value === 'hold') {
      if (!isPlaying) {
        startTone();
      }
    } else if (modeSelect.value === 'toggle' && !event.repeat) {
      if (isPlaying) {
        stopTone();
      } else {
        startTone();
      }
    }
  }
});

document.addEventListener('keyup', (event) => {
  if (event.code === 'Space' && modeSelect.value === 'hold') {
    stopTone();
  }
});

frequencyInput.addEventListener('input', () => {
  if (isPlaying) {
    oscillator.frequency.setValueAtTime(parseFloat(frequencyInput.value), ctx.currentTime);
  }
});

waveformSelect.addEventListener('change', () => {
  if (isPlaying) {
    stopTone();
    startTone();
  }
});

volumeInput.addEventListener('input', () => {
  updateVolumeDisplay();
  if (isPlaying && gainNode) {
    gainNode.gain.setValueAtTime(volumeInput.value / 100, ctx.currentTime);
  }
});

volumeUp.addEventListener('click', () => {
  setVolume(parseInt(volumeInput.value) + 1);
});

volumeDown.addEventListener('click', () => {
  setVolume(parseInt(volumeInput.value) - 1);
});

playButton.addEventListener('touchstart', (event) => {
  event.preventDefault();
  if (modeSelect.value === 'hold' && !isPlaying) {
    startTone();
  } else if (modeSelect.value === 'toggle') {
    if (isPlaying) {
      stopTone();
    } else {
      startTone();
    }
  }
});

updateFrequency();
updateVolumeDisplay();
volumeInput.addEventListener('wheel', (event) => {
  event.preventDefault();
  const delta = Math.sign(event.deltaY);
  setVolume(parseInt(volumeInput.value) + (delta === 1 ? -1 : 1));
});
```
</details>