LoginSignup
0
0

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

Last updated at Posted at 2023-11-09

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



0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0