LoginSignup
1
1

プログラミングのお題としてCrystalで簡単なbeepコマンドを作る

Last updated at Posted at 2024-01-03

これは Crystal Advent Calendar 2023 の24日目の記事です。一つ前の記事は Crystalの並列実行のやり方を調べた です。

ある程度時間のかかる処理が完了したことを知りたい場合、たとえば OS の通知機能などを使う方法もありますが、音を出すという方法もあります。

視覚とは別の聴覚というチャネルを使って使って通知するメリットはいくつかあります。視線移動が不要どころか、PCの前に座って画面を見ている必要もなく、大きめの音が出るようにしておけば少し離れた場所で別の作業をしていても気付ける、音が出ると楽しい、などです。

Linux にはそんな用途に(も)使える beep というコマンドがあるのですが、これはPCスピーカーがないマシンだと使えません。そこで自作、というわけです。

最低限の例

私の Crystal の学習も兼ねて最低限のものを書いてみました。

# 矩形波を生成する
# return: -1.0 <= x <= 1.0
def osc_square(t : Float, freq : Float) : Float
  # 1周期分の時間(秒)
  t_cycle = 1.0 / freq

  pos_cycle = (t % t_cycle) / t_cycle
  pos_cycle < 0.5 ? 1.0 : -1.0
end

def clip(v : Float) : Float
  if v < -1.0
    -1.0
  elsif v > 1.0
    1.0
  else
    v
  end
end

def write_u8(v : UInt8)
  slice = Bytes.new(1)
  slice[0] = v
  STDOUT.write slice
end

# --------------------------------

SAMPLING_RATE = 44100.0

freq     = ARGV[0].to_f # 周波数(Hz)
duration = ARGV[1].to_f # 長さ(秒)
amp      = ARGV[2].to_f # 振幅(音の大きさ)

(0...SAMPLING_RATE * duration).each do |i|
  t = i / SAMPLING_RATE
  v = osc_square(t, freq) * amp
  v_u8 = ((clip(v) + 1) / 2 * 255).to_u8
  write_u8(v_u8)
end

1サンプルごとに1バイト(UInt8)ずつ値を出力するだけの素朴な作りです。オシレータ(波形を生成する処理)は時間と周波数を入力とする関数という形にしています。効率は悪そうですが分かりやすさ優先で。

パパッと書いたナイーブな実装で、改善点はいろいろありそう……といっても自分で使う分にはこんなので十分なんですけどね。こんなので十分実用になります。

以下でビルドします。

crystal build beepgen.cr -o beepgen

beepgen という名前の通り、このプログラムが行うことはビープ音のデータを生成するだけです。生成した音を鳴らすにはどうするかというと、Linux だったら aplay コマンドに丸投げするのが手軽です。UNIX哲学に従って、上手に音を出してくれるコマンドにお任せしましょう。

次のようなシェルスクリプトでラップし、コマンドとして使えるようにしました。aplay は標準入力での受け渡しに対応しているのでパイプで渡せます。

#!/bin/bash

# "$@" を使って引数をそのまま beepgen に渡す
./beepgen "$@" \
  | aplay --rate=44100 --format=U8 --quiet

これでできあがり。このシェルスクリプトをコマンドとして使います。

# 周波数、長さ、振幅(音の大きさ)を指定して実行
./beep.sh 440 0.5 0.05

簡易に済ますためパラメータの指定方法はオリジナルの beep コマンドとは異なっています。

使い方の例

たとえば処理の終了ステータスを見て成功していたら高い音、失敗していたら低い音を出したいならこんな感じ。

do_something # ... 大きなプログラムのコンパイルなど、ある程度時間のかかる処理
exit_status=$?
if [ $exit_status -eq 0 ]; then
  # 成功したら高い音
  ./beep.sh 880 0.5 0.05
else
  # 失敗したら低い音
  ./beep.sh 220 0.5 0.05
fi

周波数や長さを変えて連続で実行することでバリエーションを作ることもできます。たとえば次のように連続で実行すると「ピポピポ」という感じの音になります。

./beep.sh 220 0.1 0.05
./beep.sh 440 0.1 0.05
./beep.sh 220 0.1 0.05
./beep.sh 440 0.1 0.05

次の例ではランダムな周波数を指定しています。昔のコンピュータの効果音(?)みたいな「ピパポポピポパピ……」という音が出ます。

for i in $(seq 10); do
  freq=$((100 + RANDOM % 1000))
  ./beep.sh $freq 0.1 0.05
done

改造案

思いついたものを挙げてみました。上の最低限の例では(一番簡単なので)矩形波を生成するようにしていますが、若干耳障りなのでサイン波、三角波くらいは対応したいですね。

  • 矩形波以外の音も出せるようにする
    • サイン波、三角波、鋸波、ノイズ、FM音源、...
    • オプションで切り替え
  • パラメータの指定方法をオリジナルの beep コマンドに寄せる
  • WAV ファイルを出力する
    • 今回作ったものは生の波形データのみなので、ヘッダを追加してちゃんとした WAV ファイルを出力する
  • 旋律を引数で指定できるようにする
    • "440,0.1 880,0.2 110,0.1 220,0.2" みたいな感じで指定できるようにする
    • ”c8 g8 d4 r8 a2 ...” のように MML っぽく書けるようにする
  • aplay コマンドを使わずに音を再生する

和音も出せるようにして……とかやりだすとちょっとしたソフトウェアシンセサイザーみたいになっていきそう。

バージョン

  $ crystal version
Crystal 1.10.1 [c6f3552f5] (2023-10-13)

LLVM: 15.0.7
Default target: x86_64-unknown-linux-gnu

参考

この記事を読んだ人は(ひょっとしたら)こちらも読んでいます

1
1
0

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
1
1