Wio TerminalでBad Apple!!の影絵を再生してみた

Wio Terminalが届いたので、例の影絵を再生してみました。


Wio TerminalでBad Apple

ひとまず描画データはPCからUSB UARTで送信しており、音声は別再生です。
白黒2値限定ですが、差分だけを描画するという方法で、一部の前フレームとの差分が極端に大きいシーンを除いて320px × 240pxで30fps出ることが確認できました。
(とはいえほぼこの影絵専用なのであんまり応用が効かないという難点が…)

Get Started with Wio Terminal - Seeedウィキ(日本語版) に従って、Arduinoで開発していますが、
Wio Terminal側の再生用のソースコード(↓)はわずか32行で、非常に手軽に開発ができるようになっているのが良いですね。

#include"TFT_eSPI.h"
TFT_eSPI tft;

void setup() {
  tft.begin();
  tft.setRotation(1);
  digitalWrite(LCD_BACKLIGHT, HIGH);
  tft.fillScreen(TFT_BLACK);
}

uint16_t read_word() {  // シリアルからwordデータ受信
  while (!Serial.available());
  uint16_t x = Serial.read();
  while (!Serial.available());
  return (x << 8) | (uint16_t)Serial.read();
}

void loop() {
  static uint32_t col = TFT_WHITE;  // 白黒交互に描画する
  uint32_t offset = 0;
  int cnt = read_word();  // 差分描画データ数
  while (cnt--) {
    uint32_t start = offset + read_word();  // 差分描画開始位置
    if (start == 0xffff) {
      offset = 0xffff;
      start = offset + read_word();
    }
    uint16_t len = read_word();  // 連続して描画するピクセル数
    uint32_t finish = start + len;
    for (uint32_t j = start; j < finish; j = j - j % 320 + 320) {
      tft.drawFastHLine(j % 320, j / 320, finish - j, col);  // drawFastHLineで横1ライン分を描画
    }
  }

  col = col == TFT_WHITE ? TFT_BLACK : TFT_WHITE;
}


なおデータは以下のように作っています。

影絵の元動画を320px × 240px 30fpsで用意し、ffmpegbmpファイルとして書き出します。

ffmpeg -i bad_apple_240p.mp4 -vcodec bmp out_%04d.bmp     

ここから、下記のrubyスクリプトを使って差分データに変換します。データフォーマットは( <データ点数>, [<開始ピクセル位置>, <連続ピクセル数>] * n )を、白の描画データ・黒の描画データの順で2回書くものとしました。数値は全てBigEndian 16bitですが、320x240=76800 は16bitに収まりきらないので、開始位置が 65535 以上になった時点でマーカー 0xffff を置いてそこから先は 65535 を加算するというルールにしています。

前フレームとの差分が極端に大きいと、描画点数が増えて遅延が蓄積してしまうので、実機で描画に要する時間を測りながら変換し、間に合わないようならフレームをスキップします。
上記のスケッチを書き込んだWio TerminalのUARTを指定して、

ruby convert.rb > /dev/cu.usbmodemXXXX

のように実行します。

class BMP
  def initialize(filename)
    open(filename, "rb") { |f| @buf = f.read }

    buf_size = (@buf[2, 4].unpack("l*"))[0]
    @width = (@buf[18, 4].unpack("l*"))[0]
    @height = (@buf[22, 4].unpack("l*"))[0]
    @line_size = @width * 3 + (4 - (@width * 3) % 4) % 4
    @buf = @buf[54, buf_size]
  end

  def pix(x, y, c)
    addr = (@height - 1 - y) * @line_size + x * 3
    @buf[addr + c].ord
  end
end

FILE_PAT = "bmps/out_%04d.bmp"
WIDTH = 320
HEIGHT = 240
THRESH = 128
UINT16_MAX = 65535

last_image = BMP.new(FILE_PAT % 1)
skip_next = 0
((ARGV[0] || 2).to_i..6570).each do |i|
  if skip_next > 0
    warn "** skip #{skip_next}"
    File.write("diff/#{i}.dat", '')
    skip_next -= 1
    next
  end

  image = BMP.new(FILE_PAT % i)

  col = nil
  start = nil
  out = ["", ""]
  cnt = [0, 0]
  total = 0
  overflow = [false, false]

  (0..HEIGHT).each do |y|
    (0...WIDTH).each do |x|
      c = image.pix(x, y, 0)
      l = last_image.pix(x, y, 0)

      diff = nil
      diff = 0 if c < THRESH && l >= THRESH
      diff = 1 if c >= THRESH && l < THRESH

      if col && (y == HEIGHT || diff != col)
        finish = y * WIDTH + x
        if !overflow[col] && start >= UINT16_MAX
          out[col] << [UINT16_MAX].pack("n")
          overflow[col] = true
        end
        out[col] << [start % UINT16_MAX, finish - start].pack("n*")
        cnt[col] += 1
        total += finish - start + 1
        col = nil
      end

      break if y == HEIGHT

      if diff && col != diff
        start = y * WIDTH + x
        col = diff
      end
    end
  end

  data = [cnt[1]].pack("n") + out[1] + [cnt[0]].pack("n") + out[0]
  File.write("diff/#{i}.dat", data)
  t0 = Time.now.to_f
  STDOUT.write data
  STDOUT.flush
  t = Time.now.to_f - t0
  warn "#{i} : #{cnt[0]} #{cnt[1]} : #{total}  #{(t * 1000).to_i} ms"

  last_image = image

  skip_next = ((t * 1000 + 13) / 33).to_i
  skip_next = [10, skip_next].min
end


再生するときは、以下のように時間を同期しつつデータを送るだけです。音声はいい感じにタイミングを合わせて別途再生してくださいw

ruby play.rb > /dev/cu.usbmodemXXXX
warn "\n\n"
start = Time.now.to_f

sframe = (ARGV[0] || 2).to_i
(sframe..6570).each do |i|
  data = File.read("diff/#{i}.dat")
  warn "\e[2A#{i}  #{data.bytesize}B"
  STDOUT.write data
  STDOUT.flush

  wait = start + (i - sframe + 1) / 30.0 - Time.now.to_f
  warn "wait #{(wait * 1000).to_i} ms    "
  warn "\n\n\n" if wait < -0.1
  sleep [wait, 0].max
end