Wio Terminalが届いたので、例の影絵を再生してみました。
ひとまず描画データは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で用意し、ffmpegでbmpファイルとして書き出します。
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