yuji

← 制作物一覧

Bad Aplle!! by Go

ターミナルの中で動画を ASCII アートとして再生する Go 製の CLI。Bad Apple!! を音付きで流す。

制作時期
体制
個人開発
使用技術
  • Go
  • CLI

つくったもの

ターミナルの中で「Bad Apple!!」を ASCII アートとして再生する CLI です。音も一緒に鳴ります。

映像の各ピクセルを明るさに応じた文字に置き換えて表示します。明るいところほど濃い文字(@)、暗いところほど薄い文字( .)になるので、黒背景のターミナルで見ると元の映像に近い見た目になります。

ターミナルで asciiplay を再生している様子

実際の出力を 80×30 の端末サイズで収録したものです。本物は 30fps で、ウィンドウ全体を使い、音も鳴ります。

表示サイズは起動した瞬間の端末の大きさで決まります。ウィンドウを最大化してフォントを小さくするほど解像度が上がり、絵として見やすくなります。

技術選定の理由

Go を選んだのは、言語の学習が目的だったからです。生成 AI にコードを書かせるのではなく、詰まったときに文法や仕組みを質問する程度にとどめて、実装は自分の手で書いています。

ffmpeg には、映像まわりの重い処理をまとめて任せました。コンテナの解析、コーデックのデコード、フレームレートの調整、端末サイズへの縮小、グレースケール化まで、すべて -vf のフィルタ指定で済ませています。

画像ファイルを経由せず、rawvideopix_fmt=gray で標準出力に吐かせてパイプで受け取る形にしました。ヘッダもエンコードもない、1ピクセル1バイトの輝度がそのまま流れてくるので、Go 側は「バイトを文字に置き換える」ことと「時間を測る」ことだけに専念できます。

音声は ffplay を別プロセスで起動して丸ごと任せています。オーディオデバイスの制御を自分で書かずに済む代わりに、面倒を見るプロセスが1つ増えました。

工夫したところ

  • 1フレームを固定長にした。 pad フィルタで余白を黒く埋めているので、1フレームは必ず「横×縦」バイトになります。おかげでパイプから固定長を読むだけでフレームを切り出せます。パイプにはフレームという区切りが存在しないので、この前提がないと1バイトずれた時点で以降ずっと絵が壊れます。
  • 時計を積み上げない。 「1フレーム分待つ」を繰り返すと誤差が積もって音とずれていくので、開始時刻からの絶対時間で「何枚目を何時に描くか」を決めています。遅れているフレームは読み飛ばして描画しません。
  • ちらつかせない。 画面をクリアせずカーソルを左上に戻して上書きします。1フレーム分をバッファに組み立ててから一度だけ書き込むことで、描画の途中が見えないようにしています。
  • 縮小を間引きにしない。 ffmpeg 側で面積平均によるリサンプリングを指定しています。輝度が滑らかになるので、10段階しかない文字の濃淡でも絵が破綻しません。

苦労したところ

いちばん時間を使ったのは AA 化ではなく、終了処理でした。

カーソルを消したまま Ctrl+C を押すと端末に戻れなくなる、映像が終わっても音だけ鳴り続ける、といった問題が出ます。後始末を defer に書けば済むと思っていたのですが、log.Fatal も Ctrl+C も defer を実行せずにプロセスを終わらせるため、まったく走っていませんでした。

その過程は記事に書いています。

外部プロセスのお片付け

これから

  • 再生中のウィンドウのリサイズに追従する
  • 再生する動画をコマンドライン引数で指定できるようにする
  • Ctrl+C 以外の経路で終了したときにも ffplay を確実に止める