UnityとLTC（タイムコード）を利用してTimelineを再生してみた

## LTC（タイムコード）でUnityTimelineを制御してみた

### 参考記事

https://note.com/hikohiro/n/n4c6a248f0910

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();
    }
}



~~~