LTC(タイムコード)でUnityTimelineを制御してみた
参考記事
LTCread はこちらの記事を参考にスクリプトを書き換えています。
TimelineSyncがタイムラインを制御するために記述したものです。
コード
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
public class LTCread : MonoBehaviour
{
[SerializeField] private Dropdown deviceDropdown;
private string m_TimeCode = "00:00:00;00";
[SerializeField] private int m_SampleRate = 44100; // 既定のサンプルレートを設定します。
[SerializeField] private bool is60Fps = false; // 60fpsのLTCサポート用のフラグ
[SerializeField] private Text ltcSecondsText;
private const int DEVICE_REC_LENGTH = 10;
private AudioClip m_LtcAudioInput;
private int m_LastAudioPos;
private int m_SameAudioLevelCount;
private int m_LastAudioLevel;
private int m_LastBitCount;
private string m_BITPattern = "";
[SerializeField, Range(0.0f, 1.0f)] private float m_AudioThreshold;
private string m_Gain;
private GUIStyle m_TimeCodeStyle;
private void Start()
{
deviceDropdown.ClearOptions();
foreach (var device in Microphone.devices)
{
deviceDropdown.options.Add(new Dropdown.OptionData(device));
}
deviceDropdown.onValueChanged.AddListener(delegate
{
OnDeviceDropdownValueChanged();
});
if (Microphone.devices.Length > 0)
{
m_LtcAudioInput = Microphone.Start(Microphone.devices[0], true, DEVICE_REC_LENGTH, 44100);
}
m_TimeCodeStyle = new GUIStyle
{
fontSize = 64,
normal = { textColor = Color.white }
};
}
private void OnDeviceDropdownValueChanged()
{
string selectedDevice = deviceDropdown.options[deviceDropdown.value].text;
Microphone.End(selectedDevice);
m_LtcAudioInput = Microphone.Start(selectedDevice, true, DEVICE_REC_LENGTH, 44100);
}
private void Update()
{
DecodeAudioToTcFrames();
}
private void OnGUI()
{
GUI.Label(new Rect(0, 0, 200, 100), m_TimeCode, m_TimeCodeStyle);
GUI.Label(new Rect(0, 100, 200, 100), $"Selected Device: {deviceDropdown.options[deviceDropdown.value].text}", m_TimeCodeStyle);
}
// 現在までのオーディオ入力を取得しフレーム情報にデコードしていく
private void DecodeAudioToTcFrames() {
float[] waveData = GetUpdatedAudio(m_LtcAudioInput);
if (waveData.Length == 0) {
return;
}
var gain = waveData.Select(Mathf.Abs).Sum() / waveData.Length;
m_Gain = $"{gain:F6}";
if (gain < m_AudioThreshold) return;
int pos = 0;
int bitThreshold = m_LtcAudioInput.frequency / 3100; // 適当
while (pos < waveData.Length) {
int count = CheckAudioLevelChanged(waveData, ref pos, m_LtcAudioInput.channels);
if (count <= 0) continue;
if (count < bitThreshold) {
// 「レベル変化までが短い」パターンが2回続くと1
if (m_LastBitCount < bitThreshold) {
m_BITPattern += "1";
m_LastBitCount = bitThreshold; // 次はここを通らないように
} else {
m_LastBitCount = count;
}
} else {
// 「レベル変化までが長い」パターンは0
m_BITPattern += "0";
m_LastBitCount = count;
}
}
// 1フレームぶん取れたかな?
if (m_BITPattern.Length >= 80) {
int bpos = m_BITPattern.IndexOf("0011111111111101"); // SYNC WORD
if (bpos > 0) {
string timeCodeBits = m_BITPattern.Substring(0, bpos + 16);
m_BITPattern = m_BITPattern.Substring(bpos + 16);
if (timeCodeBits.Length >= 80) {
timeCodeBits = timeCodeBits.Substring(timeCodeBits.Length - 80);
m_TimeCode = DecodeBitsToFrame(timeCodeBits);
}
}
}
// パターンマッチしなさすぎてビットパターンバッファ長くなっちゃったら削る
if (m_BITPattern.Length > 160) {
m_BITPattern = m_BITPattern.Substring(80);
}
}
// マイク入力から録音データの生データを得る。
// オーディオ入力が進んだぶんだけ処理して float[] に返す
private float[] GetUpdatedAudio(AudioClip audioClip) {
int nowAudioPos = Microphone.GetPosition(null);
float[] waveData = new float[0];
if (m_LastAudioPos < nowAudioPos) {
int audioCount = nowAudioPos - m_LastAudioPos;
waveData = new float[audioCount];
audioClip.GetData(waveData, m_LastAudioPos);
} else if (m_LastAudioPos > nowAudioPos) {
int audioBuffer = audioClip.samples * audioClip.channels;
int audioCount = audioBuffer - m_LastAudioPos;
float[] wave1 = new float[audioCount];
audioClip.GetData(wave1, m_LastAudioPos);
float[] wave2 = new float[nowAudioPos];
if (nowAudioPos != 0) {
audioClip.GetData(wave2, 0);
}
waveData = new float[audioCount + nowAudioPos];
wave1.CopyTo(waveData, 0);
wave2.CopyTo(waveData, audioCount);
}
m_LastAudioPos = nowAudioPos;
return waveData;
}
// 録音データの生データから、0<1, 1>0の変化が発生するまでのカウント数を得る。
// もしデータの最後に到達したら-1を返す。
private int CheckAudioLevelChanged(float[] data, ref int pos, int channels) {
while (pos < data.Length) {
int nowLevel = Mathf.RoundToInt(Mathf.Sign(data[pos]));
// レベル変化があった
if (m_LastAudioLevel != nowLevel) {
int count = m_SameAudioLevelCount;
m_SameAudioLevelCount = 0;
m_LastAudioLevel = nowLevel;
return count;
}
// 同じレベルだった
m_SameAudioLevelCount++;
pos += channels;
}
return -1;
}
// ---------------------------------------------------------------------------------
// フレームデコード
private int Decode1Bit(string b, int pos) {
return int.Parse(b.Substring(pos, 1));
}
private int Decode2Bits(string b, int pos) {
int r = 0;
r += Decode1Bit(b, pos);
r += Decode1Bit(b, pos + 1) * 2;
return r;
}
private int Decode3Bits(string b, int pos) {
int r = 0;
r += Decode1Bit(b, pos);
r += Decode1Bit(b, pos + 1) * 2;
r += Decode1Bit(b, pos + 2) * 4;
return r;
}
private int Decode4Bits(string b, int pos) {
int r = 0;
r += Decode1Bit(b, pos);
r += Decode1Bit(b, pos + 1) * 2;
r += Decode1Bit(b, pos + 2) * 4;
r += Decode1Bit(b, pos + 3) * 8;
return r;
}
private string DecodeBitsToFrame(string bits) {
int frames;
if (is60Fps)
{
frames = Decode4Bits(bits, 0) + Decode3Bits(bits, 8) * 10; // 60fps対応のデコード
}
else
{
frames = Decode4Bits(bits, 0) + Decode2Bits(bits, 8) * 10; // 通常のデコード
}
int secs = Decode4Bits(bits, 16) + Decode3Bits(bits, 24) * 10;
int mins = Decode4Bits(bits, 32) + Decode3Bits(bits, 40) * 10;
int hours = Decode4Bits(bits, 48) + Decode2Bits(bits, 56) * 10;
return $"{hours:D2}:{mins:D2}:{secs:D2};{frames:D2}";
ltcSecondsText.text = $"{secs:D2} seconds";
}
public string CurrentTimeCode
{
get { return m_TimeCode; }
}
}
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
[RequireComponent(typeof(PlayableDirector))]
public class TimelineSync : MonoBehaviour
{
public LTCread ltcReader; // LTCread スクリプトへの参照
private PlayableDirector director;
private double lastLTCtime = 0;
private int fps = 30; // or any appropriate default value
private void Start()
{
director = GetComponent<PlayableDirector>();
director.Play();
}
private void Update()
{
double currentLTCtime = ConvertTimeCodeToSeconds(ltcReader.CurrentTimeCode);
//Debug.Log($"Current LTC Time: {currentLTCtime}");
SyncTimeline(currentLTCtime);
lastLTCtime = currentLTCtime;
}
// LTCタイムコードを秒数に変換する
private double ConvertTimeCodeToSeconds(string timeCode)
{
string[] parts = timeCode.Split(':', ';');
int hours = int.Parse(parts[0]);
int minutes = int.Parse(parts[1]);
int seconds = int.Parse(parts[2]);
int frames = int.Parse(parts[3]);
double frameDuration = 1.0 / fps; // fpsを60や30などの適切な値に設定する
return hours * 3600 + minutes * 60 + seconds + frames * frameDuration;
}
// Timelineを指定した時間に同期させる
private void SyncTimeline(double seconds)
{
//Debug.Log($"Director Time: {director.time}, Setting Time: {seconds}");
director.time = seconds;
//director.Evaluate();
}
}