今年のショートケーキは役に立つはず

趣味で宇宙開発を行う団体「[リーマンサット・プロジェクト](https://www.rymansat.com)」がお送りする[新春アドベントカレンダー](https://qiita.com/rsp-dgkz/items/a6ff573c07dbe2c6166f)です。
私は[RSP-01という超小型人工衛星](https://www.rymansat.com/satellites)のチャット機能、地上システムを担当しております。

---
今年もショートケーキを作ったという話です。

ショートケーキの定義
　[https://jflute.hatenadiary.jp/entry/20180223/mastercurrent](https://jflute.hatenadiary.jp/entry/20180223/mastercurrent)

昨年のショートケーキ
　[https://qiita.com/rsp-dgkz/items/83268377f8309ddb10f3](https://qiita.com/rsp-dgkz/items/83268377f8309ddb10f3)

今年のショートケーキ
　今回の記事は、モールス信号の必要性と少しだけWeb Audio APIについて書いています。

# 作ったショートケーキ
今回のショートケーキはモールス信号の勉強サイトです。
モールス信号なんて今の時代必要なの？と思われるかも知れませんが、必要なんです。**一部の民にとって**。

[https://rsp-daigo.github.io/morse-manabu/](https://rsp-daigo.github.io/morse-manabu/)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526509/489104ec-ad67-c9f3-7ff7-6b006b0cff78.png)
※Windows Chrome、iPhone Safariでのみ動作確認しています。


#### なぜ必要なのか
リーマンサットが趣味で創った人工衛星であるRSP-01は、2021年に打ち上がります。
宇宙に放出された暁には、衛星と通信したいじゃないですか。自分の手で。
この「自分の手」がポイントで、衛星へコマンドを送るには3級以上のアマチュア無線（以降3アマ）の資格が必要です。

3アマの試験問題には、モールス信号が出てきます。
巷にはモールス信号を覚えるためのサイトはいくつかあるのですが、私にフィットするものが無かったので作りました。**試験が終わった後に**。

#### 人工衛星からはモールス信号が送信される
多くの人工衛星からはCWやビーコンと呼ばれるモールス信号が送信されています。
僕らのRSP-01もモールス信号を出します。
なぜモールス信号を使うかというと、主には以下の理由です。

* 特別なハードやソフトウェアが無くても可聴化できるため、公開すべき／したい情報に適している。聞こうと思えば誰でも聞ける。
* 電波状態が悪くとも人間の耳で解読できれば、情報を得ることができる。
* FMパケット通信に比べて、CWは衛星側の送信時の電力が少なくて済む。つまり、衛星の状態など確実に地上へ送信したい情報の送信に適している。

従って、人工衛星を運用するものにとってモールス信号は、3アマを取得した後も実際に必要なのです。
**僕たち一部の必要な民！**


# ご要件
#### 基本的な事項
- ブラウザで学習できる
- スマホで学習できる

#### 出題方法
- モールス信号を**文字化したものを表示**する
- 合わせて**音も出力**する

#### 解答方法
- A～Z、0～9をポチる
- 正解だったら正解トーストを表示し、次を出題
- 間違っていたら、正解情報を表示する
- 正解情報のクリックで次の出題
![正解_不正解.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526509/36ba1d03-4c4f-2fc2-4cbd-9ee766a60821.png)

# 音の出力
今回はWeb Audio APIを使って、任意のモールス信号を出力することにしました。
ブラウザで音をオーディオを使うのなんて楽勝っしょ！なんて思ってましたが中々でした。

#### iPhoneで音が出なくなる 
Mobile Safariでは、ユーザ操作を起点とした実行コンテキスでオシレータのstart()を呼ばなければならない制約があるとの事です。

> 出典（Apple公式の出展はサクッと見つけられませんでした）
> [iOSのMobile Safari でWeb Audio API を利用したサウンドが再生されない (タッチ制約による制限) - JavaScript プログラミング](https://www.ipentec.com/document/software-mobile-safari-ios-web-audio-api-touch-limit)

開発の当初からこの情報は入手していたので意識してコーディングしていたのですが、この「実行コンテキスト」がどこかで切れてしまうのか、iPhoneでの連続した再生に難儀しました。
最終的には、タップ操作から出来るだけ近しい処理で window.webkitAudioContext を new することで安定しました。

以下が音を再生する箇所のソースです。

```javascript:morse_player.js
// 再生速度（現在は固定）
const MORSE_SPEED = 100;

// 長点
const MORSE_LONG = '－';

// Sleepメソッド化
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

export default class MorsePlayer {
  constructor() {
    this.aurdioRunnable = false;
    this.audioReset = false;

    this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    this.oscillator = null;
  }

  /**
   * 生成中か返す
   */
  isAurdioRunnable() {
    return this.aurdioRunnable;
  }

  /**
   * 中断要求中か返す
   */
  isAudioReset() {
    return this.audioReset;
  }

  /**
   * モールス信号を再生する
   */
  async playMorseSignal(morseText) {
    // 点と線でばらしてループ
    const ch = Array.from(morseText);
    const gainList = [];
    for (const item of ch) {
      // 長点は短点の3倍
      let playTime = 1;
      if (item === MORSE_LONG) {
        playTime = 3;
      }

      // 再生速度の設定
      playTime *= MORSE_SPEED;

      gainList.push({ playTime, vol: 0.9 });

      // 要素間のインターバル
      const interval = 1 * MORSE_SPEED;
      gainList.push({ playTime: interval, vol: 0 });
    }
    // １セット再生し終えた後のインターバル
    gainList.push({ playTime: 1000, vol: 0 });

    // 再生実行
    await this.playAudio(gainList);
  }

  /**
   * オーディオを再生する
   */
  async playAudio(gainList) {
    try {
      this.aurdioRunnable = true;

      this.oscillator = this.audioCtx.createOscillator();
      this.oscillator.type = 'sine';
      this.oscillator.frequency.value = 440;
      const gain = this.audioCtx.createGain();
      gain.connect(this.audioCtx.destination);

      this.oscillator.start();
      this.oscillator.connect(gain);

      // 指定要求があったら抜ける
      while (!this.audioReset) {
        let totalPlayTime = 0;
        const baseTime = this.audioCtx.currentTime;
        for (const gainItem of gainList) {
          gain.gain.setValueAtTime(gainItem.vol, baseTime + totalPlayTime);

          totalPlayTime += gainItem.playTime / 1000;
        }

        // 停止要求をチェックするため再生時間分Slepp
        await sleep(totalPlayTime * 1000);
      }

      this.oscillator.stop();
      this.oscillator = null;

      this.audioReset = false;
      this.aurdioRunnable = false;
    } catch (e) {
      alert(e);
    }
  }

  /**
   * 再生の停止を要求する
   * @param {function} callback コールバック
   */
  async stop(callback) {
    console.log('stopAudio');

    // 停止要求を通知
    this.audioReset = true;

    // 停止を待つ
    while (this.audioReset) {
      await sleep(100);
    }

    // 停止後のコールバック呼び出し
    callback();
  }
}
```

playMorseSignal()に「・－－」などのモールス信号を文字列化したものを渡します。
「・－－」を分割し、以下の配列を作成します。

[0] 100ms再生（・）
[1] 100ms無音
[2] 300ms再生（－）
[3] 100ms無音
[4] 300ms再生（－）
[5] 300ms無音（次のモールスとのインターバル）

配列をplayAudio()に渡し、停止要求があるまで繰り返し再生し続けます。

---
出来てしまえば簡単ですが、やはり色々とありますね。

ソースはこちらです。
[https://github.com/rsp-daigo/morse-manabu](https://github.com/rsp-daigo/morse-manabu)

---
明日は @hokke_mirin さんの「[パソコンの自作って難しいのか？](https://qiita.com/hokke_mirin/items/d002532fc7c9238557f9)」です。

リーマンサット・プロジェクトは「普通の人が集まって宇宙開発しよう」を合言葉に活動をしている民間団体です。
興味を持たれた方は [https://www.rymansat.com/join](https://www.rymansat.com/join) からお気軽にどうぞ。

超小型人工衛星の開発の1から10までを経験できるという恐ろしいほど甘美な団体です。