34
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-01-06

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


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

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

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

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

作ったショートケーキ

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

https://rsp-daigo.github.io/morse-manabu/

image.png
※Windows Chrome、iPhone Safariでのみ動作確認しています。

なぜ必要なのか

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

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

人工衛星からはモールス信号が送信される

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

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

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

ご要件

基本的な事項

  • ブラウザで学習できる
  • スマホで学習できる

出題方法

  • モールス信号を文字化したものを表示する
  • 合わせて音も出力する

解答方法

  • A~Z、0~9をポチる
  • 正解だったら正解トーストを表示し、次を出題
  • 間違っていたら、正解情報を表示する
  • 正解情報のクリックで次の出題
    正解_不正解.png

音の出力

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

iPhoneで音が出なくなる

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

出典(Apple公式の出展はサクッと見つけられませんでした)
iOSのMobile Safari でWeb Audio API を利用したサウンドが再生されない (タッチ制約による制限) - JavaScript プログラミング

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

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

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 });
    }
    // 1セット再生し終えた後のインターバル
    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


明日は @hokke_mirin さんの「パソコンの自作って難しいのか?」です。

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

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

34
15
2

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
34
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?