前回は、PythonistaでU.F.O. SAを操作することができた。
しかし、U.F.O SAの真価は、音声や動画との連動において発揮される。
今回は、音声ファイルとそれに対応するcsvファイルを用いて、U.F.O SAを音声連動させる方法を考える。
準備するもの
- U.F.O. SA
- iPhone or iPad
- Pythonista (有料アプリ)
参考にしたサイト
騎士の物語
中国語のサイトです。Google翻訳を駆使してください。
Pythonista cb module
Pythonista公式のcb moduleのチュートリアル。
Pythonista sound module
Pythonista公式のsound moduleのチュートリアル。
ディレクトリ構成
ディレクトリ構成は以下の通りとします。
root/
├─ main.py
├─ music/
│ └─ hoge.mp3
└─ csv/
└─ hoge.csv
音声ファイルは、標準のファイルアプリ、または、icloudから、Pythonistaフォルダに入れるなど、頑張って入れてください。(うまく移せないときがあった気がしますが、どうやって移せるようにしたか忘れました。)
music_path = "/music/hoge.mp3"
csv_path = "/csv/hoge.csv"
音声ファイルの再生
Pythonistaには、音声ファイルを再生するsoundモジュールが存在する。
そのため、簡単に音声ファイルを実行することができる。
音声ファイルの再生は以下のコードでできます。
import sound
audio = sound.Player(music_path)
audio.play()
さらに、audio.current_time =
で秒数を代入することで、再生開始時間を変更することができます。
csvファイルを読み解く
一般的にU.F.O SA用に配布されているcsvファイルの中身は以下のようになっています。
Time / ms | rotation direction | rotation speed |
---|---|---|
10 | 0 | 20 |
30 | 1 | 30 |
90 | 0 | 10 |
ヘッダーは、見やすさのために着けただけで、実際のファイルには書いてありません。
- 時間: スタートからの経過時間。単位が[ms]であることに注意。
- 回転方向: 0が右回転、1が左回転。
- 回転速度: 0–100の整数値。
上の例では、10 ms経ってから1つ目の命令(回転速度20で右回転)を実行します。
次にスタートから30 ms経った時点で2つ目の命令(回転速度30で左回転)を実行します。
1つ目と2つ目の間の20 msは、1つ目の命令が実行され続けます。
これをリストに格納するためにcsvモジュールを用います。
with open(csv_path) as f:
reader = csv.reader(f)
csv_data = [map(int, r) for r in reader]
命令をU.F.O SAに送る
前回と同様に、cbモジュールを用いて、命令を送信します。
まずは、用いるモジュールをインポートします。
import csv
import time
import cb
import sound
再生時間の取得にaudio.current_timeを用いたいのですが、秒数を整数型で返すので、大雑把です。
したがって、時間取得のためにtimeモジュールを入れています。
cb.set_central_delegate()
の引数となるクラスのメソッドであるdid_discover_characteristics()
に以下を前回同様に以下のようにします。
def did_discover_characteristics(self, s, error):
print('Did discover characteristics...')
for c in s.characteristics:
print(c.uuid)
if c.uuid == CHAR_SSID:
self.peripheral.set_notify_value(c, True)
self.send_rotation_commands(c)
そして、send_rotation_commands()
の中身を次のように書きます。
def send_rotation_commands(self, c):
audio = sound.Player(path)
audio_start_time = 0 # 再生開始時間 / seconds
audio.current_time = audio_start_time
audio.play()
start = time.time()
for t, d, s in csv_data:
while time.time()-start <= t/10 - audio_start_time:
pass
if time.time()-start <= t/10 - audio_start_time+0.1:
s = int(s*0.8)
b = b'\x02\x01'+(([0x00, 0x01][d] << 7) | s).to_bytes(1, byteorder='big')
self.peripheral.write_characteristic_value(c, b, True)
print(t, b)
while audio.playing:
pass
再生時間をaudio_start_time
に代入し、audio.current_time
に格納することで、再生開始時間を設定します。
再生開始時にtime.time()
で開始時間を取得しています。
for t, d, s in csv_data:
while audio_start_time + time.time() - start <= t/10:
pass
if audio_start_time + time.time() - start <= t/10 + 0.1:
b = b'\x02\x01'+(([0x00, 0x01][d] << 7) | s).to_bytes(1, byteorder='big')
self.peripheral.write_characteristic_value(c, b, True)
print(t, b)
for文でt, d, sにそれぞれ、時間と回転方向、回転速度を代入します。
while節では、(再生開始時間 + 経過時間) <= (命令の時間)である限り、待機します。
tは[ms]なので、t/10で秒に直してあげる必要があります。
それをわずかに超えた、(再生開始時間 + 経過時間) <= (命令の時間)+ 0.1を満たす場合のみif節の中身を実行します。
つまり、(命令の時間) < (再生開始時間 + 経過時間) <= (命令の時間)+ 0.1のときのみ実行するということです。
途中から再生を始めた際に、(再生開始時間 + 経過時間) <= (命令の時間)+ 0.1の条件がないと、再生開始時間前に時間設定されている命令が一気に実行されるため、ラグが発生します。この条件により、命令の時間が再生時間より0.1秒以上短い命令は除去しています。
これによって、音声ファイルとそれに対応するcsvファイルを用いて、U.F.O SAを音声連動させることができます。
コード全文
import csv
import time
import cb
import sound
SERVISE_SSID = '40EE1111-63EC-4B7F-8CE7-712EFD55B90E'
CHAR_SSID = '40EE2222-63EC-4B7F-8CE7-712EFD55B90E'
music_path = "/music/hoge.mp3"
csv_path = "/csv/hoge.csv"
AUDIO_START_TIME = 0
with open(csv_path) as f:
reader = csv.reader(f)
csv_data = [map(int, r) for r in reader]
class UFOSAManager (object):
def __init__(self):
self.peripheral = None
def did_discover_peripheral(self, p):
print(p.name)
if p.name and 'UFOSA' in p.name and not self.peripheral:
self.peripheral = p
print('Connecting UFO SA...')
cb.connect_peripheral(p)
def did_connect_peripheral(self, p):
print('Connected:', p.name)
print('Discovering services...')
p.discover_services()
def did_fail_to_connect_peripheral(self, p, error):
print('Failed to connect: %s' % (error,))
def did_disconnect_peripheral(self, p, error):
print('Disconnected, error: %s' % (error,))
self.peripheral = None
def did_discover_services(self, p, error):
for s in p.services:
if s.uuid == SERVISE_SSID:
print('Discovered UFO SA servise, discovering characteristitcs...')
p.discover_characteristics(s)
def did_discover_characteristics(self, s, error):
print('Did discover characteristics...')
for c in s.characteristics:
print(c.uuid)
if c.uuid == CHAR_SSID:
self.peripheral.set_notify_value(c, True)
self.send_rotation_commands(c)
def did_write_value(self, c, error):
print('Did enable UFO SA')
cb.reset()
exit()
def did_update_value(self, c, error):
print('test')
def send_rotation_commands(self, c):
audio = sound.Player(path)
audio.current_time = AUDIO_START_TIME
audio.play()
start = time.time()
for t, d, s in csv_data:
while audio_start_time + time.time() - start <= t/10:
pass
if audio_start_time + time.time() - start <= t/10 + 0.1:
b = b'\x02\x01'+(([0x00, 0x01][d] << 7) | s).to_bytes(1, byteorder='big')
self.peripheral.write_characteristic_value(c, b, True)
print(t, b)
while audio.playing:
pass
mngr = UFOSAManager()
cb.set_central_delegate(mngr)
print('Scanning for peripherals...')
cb.scan_for_peripherals()
try:
while True: pass
except KeyboardInterrupt:
# Disconnect everything:
cb.reset()
最後に
これで、iPhoneから気軽にU.F.O SAの音声連動が可能になりました。
基本的にサイクロンSAやピストンSAでも方法は同じであると考えられます。
細かいところを詰めるとさらに快適に使えるはずですので、適宜、修正してください。
Pythonistaは無限の可能性を秘めているアプリですので、もっともっと活用していけたらと思います。