SMPTE时间同步

LTC,MIDI Timecode,ArtTime

Views:  times Updated on September 30, 2023 Posted by elmagnifico on September 19, 2023

Foreword

SMPTE 时间同步,实际能搜到使用的例子

绝对时间同步

使用UTC时间,但是UTC时间往往是没有ms时间的,所以要更高精度的时候,UTC时间不合适

SMPTE同步

SMPTE是个大型时间同步规则的制定机构,有多个不同情况下使用的时间同步标准。

image-20230919174736965

最初只是给视频信息做同步的,后续音频、灯光等等多媒体设备加入,扩充了使用范围和标准。以前电视最常见的没信号的彩条也是这个组织规定的。

直接使用SMPTE 大概率是从视频或者是音频的信号进行同步接入。

Timecode on a digital slate

最常见的时间同步就是拍摄现场的场记板,做的好的同步,会和摄像机、录音设备等联动,记录当前的时间。每天可能有成百上千条素材产生的时候,通过时间同步就很容易定位到某一个时刻发生的各个视频内容,方便后期、剪辑、审阅等等。

而如果没有时间同步,想想那个素材量,整理起来得有多麻烦。

多台设备做时间同步,一般会用Genlock,相当于是校准了每个设备上的系统时钟,防止他们跑偏

SMPTE成立时间非常早,早期技术不发达,定下来的一些标准不够好(29.97fps这种),导致后来要去兼容,于是弄出来了丢帧时间码和非丢帧时间码。

非丢帧时间码就是各种整数的FPS

img

丢帧时间码则是非整数FPS的情况,因为时间码上没办法做小数,所以通过丢帧实现小数fps

img

这种方式实现的效果,就是实际播放的时候只会丢弃这个时间码,而不会丢失实际的帧,这样一份素材也能很容易兼容到不通FPS中

SMPTE也非常类似Unicode那一套,编码方式多种多样,不通载体里形式不一样。

LTC

LTC,Linear(Longitudinal)Timecode,LTC是SMPTE的编码方式之一。一般叫做线性时间编码,或者纵向时间编码

Timecode example

主要是通过时分秒、帧来确定时间的。帧的话实际要根据选择的模式,比如24fps、30fps、29.97fps、60fps等

image-20230919175612710

LTC是规定了使用80bits,10bytes作为一个时间同步帧的存储结构。

MIDI Timecode同步

MTC,MIDI timecode,MIDI Time Code(MTC)是MIDI中的一个子协议。在MTC的协议数据中包含了SMPTE的时间信息。

这个本质上还是非常简单的,是直接把LTC的编码包进去了,只是外部是MIDI的数据帧而已

ArtTime 同步

The data format is compatible with both longitudinal time code and MIDI time code. The four key types of Film, EBU, Drop Frame and SMPTE are also encoded.

Art-Net 中的同步是兼容了多种时间编码的,协议包内就可以区分清楚。

ArtTime 则是选择从灯光设备角度进行接入

image-20230919183729584

Art-Net中就没有直接使用LTC的编码格式,而是自己定义了一套。

声音同步

SMPTE可以编码,所以也有人直接编码以后嵌入音频之中,这样识别到特定的音频就知道这里指代的时间戳了

音视频附加SMPTE信息

给一段音频附加了SMPTE信息,可以将LTC通道指定为左/右声道,直接播放就能听到LTC的特定频率了

image-20230920101706278

一般控制灯光等设备,是单独指定某个通道,音乐还是正常播放的,最后将这个通道输出给这些设备就行了,需要注意各通道的帧率要一致,否则定位有偏差。

https://www.reaper.fm/

中文语言包

https://stash.reaper.fm/v/41464/REAPER_zh_CN_www.szzyyzz.com.rar

SMPTE 数据帧获取思路

从MIDI设备读取

可以通过硬件直接将SMPTE转换成MIDI或者USB的MIDI设备输出,同样如果是三者之一的任何一个接口输入,都可以转换成其他的方式输出

DOREMiDi SMPTE LTC转MIDI MTC USB

DOREMiDi SMPTE LTC转MIDI MTC USB

通过REAPER播放带有LTC的音频

image-20230920165251908

将电脑输出转给LTC IN,然后就能看到MIDI-OX中采集到的音频帧,同时DOREMiDi 的显示屏上也有对应的数值显示

image-20230920165236563

image-20230920165627582

可以看到通过这种方式是在系统中添加了一个音频设备

image-20230920170531727

设备类型是MIDI,所以可以被MIDI-OX检测到

image-20230920170554190

那么如果要获取到这个LTC帧,就可以通过读取MIDI设备,读取具体的数据包,解包获得

从音频设备读取

还有一种思路就是直接读取音频设备的数据流,然后从中摘取出对应的LTC帧,本质上LTC也只是在音频中的某一个通道里存在而已

image-20230918132718363

设备上是比较简单的,直接将音频接入声卡或者USB麦克风之类的设备中,然后读取数据流,解包即可。

Focusrite福克斯特声卡Scarlett 2i2三代

读取验证SMPTE

直接读取SMPTE

https://timecodesync.com/monitor/#download

TimeCode Monitor 确实不错,可以直接从音频上读取到TimeCode信息,无需转换到MIDI中

Image

可以自由选择音频输入接口

MIDI方式

https://github.com/naudio/NAudio

C#,使用NAudio中的MIDI接口读取即可,有现成的例子就不说了。

音频方式

java提取SMPTE信息

https://stackoverflow.com/questions/7368615/java-class-for-extracting-ltc-smpte-timecode-from-an-audio-signal

c# SMPTE时间转换库,仅仅是转换而已

https://github.com/ailen0ada/Timecode4net

libltc C读取和处理ltc的库,但是是基于POSIX的,没有现成的windows下的库

https://github.com/x42/libltc

基于libltc的C#封装

https://github.com/elliotwoods/LTCSharp

方法一

硬搜,由于LTC的结尾一定是0011 1111 1111 1101,所以可以根据特征直接搜索音频字节流中的LTC帧。

实际测试根本无法找到对应的帧,这个思路还是有点不对

方法二

使用NAudio的NAudio.WaveInEvent来读取麦克风,然后通过GetPosition获取位置

        //
        // 摘要:
        //     Gets the current position in bytes from the wave input device. it calls directly
        //     into waveInGetPosition)
        //
        // 返回结果:
        //     Position in bytes
        public long GetPosition()
        {
            MmTime mmTime = default(MmTime);
            mmTime.wType = 4u;
            MmException.Try(WaveInterop.waveInGetPosition(waveInHandle, out mmTime, Marshal.SizeOf(mmTime)), "waveInGetPosition");
            if (mmTime.wType != 4)
            {
                throw new Exception($"waveInGetPosition: wType -> Expected {4}, Received {mmTime.wType}");
            }

            return mmTime.cb;
        }
        
namespace NAudio.Wave
{
    //
    // 摘要:
    //     MmTime http://msdn.microsoft.com/en-us/library/dd757347(v=VS.85).aspx
    [StructLayout(LayoutKind.Explicit)]
    public struct MmTime
    {
        public const int TIME_MS = 1;

        public const int TIME_SAMPLES = 2;

        public const int TIME_BYTES = 4;        

可以看到通过MM的方式获取到的时间戳,是只能选择ms、采样时间、字节时间

image-20230926154834747

结合MSDN来看,很显然这三个都不是我们想要的,而TIME_SMPTE这个格式,NAudio根本不支持,而实际上Windows Media Player其实早就说了不支持SMPTE,因为SMPTE数据格式是14帧,而正常的WMP只能做到12帧,所以这里会有问题。

再往深里看 NAudio内部音频都是调用winmm.dll接口实现的,SMPTE对应的值是8,如果传8进去,返回的依然是4,会提示错误,实际不支持。

image-20230926164210105

方法三

https://github.com/alantelles/py-ltc-reader

py-ltc-reader确实是直接读取到frame信息,然后逐帧解析出了里面的SMPTE信息,可以参考里面的解法

仔细看里面关于SMPTE的解析,发现实际上他是从音频读取到采样帧以后,又重新做了一次编码。

这也是为什么方法一无法正常读取到同步标志的关键字节,因为那个字节是编码后的字节。

编码格式是这样的:

image-20230926184553528

每一bit,其实都是一个载波区间,一个载波区间内,数据如果发生了反转,那么就认为是1,如果没有发生翻转,就认为是0.

LTC同步的0011 1111 1111 1101,如果放在这里就需要扩展成4个字节的变化。

音频采样一般都是2字节的位宽,其中2字节位宽中,还会有大小端的问题,普通PC是小端,所以后面的字节是高位字节,两个拼一起才是实际的值

image-20230926192219409

0xFFFA = -6

而这个值实际上是代正负性的,raw音频格式的每一个采样帧,本身是(-32768 到+32767),这个数值是PCM经过对数等等计算后得到的一个值

img

由于是raw数据,所以要得到这个曲线翻转,就需要判断这个数值是小于0还是大于0,从而判断翻转。

代码解析
def decode_ltc(wave_frames):
    global jam
    frames = []
    output = ''
    out2 = ''
    last = None
    toggle = True
    sp = 1
    first = True
    for i in range(0,len(wave_frames),2):
        data = wave_frames[i:i+2]
        pos = audioop.minmax(data,2)
        // 波形判断
        if pos[0] < 0:
            cyc = 'Neg'
        else:
            cyc = 'Pos'
        // 发现波形翻转了
        if cyc != last:
            // 波形翻转并且波形大于7的时候才会认为是一个正常的信号
            if sp >= 7:
                out2 = 'Samples: '+str(sp)+' '+cyc+'\n'
                // 如果波形大于14其实就是一个0的表示跨过了2个区间
                if sp > 14:
                    bit = '0'
                    output += str(bit)
                else:
                    // 如果波形是小于14大于7说明这个波形是1个区间就发生了翻转表示1
                    if toggle:
                        bit = '1'
                        output += str(bit)
                        // 记录翻转做2个区间的匹配
                        toggle = False
                    else:
                        toggle = True
                // 如果得到的值比后2字节同步长了那么这个时候可以判断是否得到了正确的LTC
                if len(output) >= len(SYNC_WORD):
                    // 2字节同步码相等
                    if output[-len(SYNC_WORD):] == SYNC_WORD:
                        // 并且当前记录的长度是大于10字节的
                        if len(output) > 80:
                            frames.append(output[-80:])
                            output = ''
                            os.system('clear')
                            print('Jam received:',decode_frame(frames[-1])['formatted_tc'])
                            // 按照LTC解析其中的时间信息即可
                            jam = decode_frame(frames[-1])['formatted_tc']
            sp = 1
            last = cyc
        else:
            // 记录波形的宽度
            sp += 1

python代码里把周期写死了,7是一半周期,14是完整周期,这个值和前面定义的采样速率和LTC帧率有关系(48000),正确的写法应该是跟随频率改变而改变。

比如采样速率是48000Hz,LTC本身发送是30帧每秒,一帧是80bits,每个采样点是2bytes

推理可知:
1帧会有1600个采样点,每bit就有20个采样点=也就是40bytes或者20个 int16 bytes
那么就是每个声音脉冲宽度是20个数据,半宽就是10

所以7和14是比较保守的写法,具体就是:
采样速率/帧率/80 = 一个bit的音频数据的宽度
采样速率/帧率/80/2 = 半个bit的音频数据的宽度

这里面唯一需要手动处理的大概就是帧率,最好可以提前设置一下,或者是自动帧率(尝试使用不同帧率去检测数据是否正确)

帧率探测,有没有什么办法直接探测到准确帧率?

可以通过多次测量正负周期的最长长度,再结合当前的采样速度,就能反算出来当前的帧率具体是多少了。

经过调试已经可以正常获取到LTC 时间了

image-20230930205825975

名词解释

目前现存的音频底层调用接口应该就是这四个:

  • WaveOut
  • DirectSound
  • ASIO
  • WASAPI

其他的要么被淘汰了,要么被其中某一个取代了。

ASIO

ASIO,Audio Stream Input Output。ASIO是专业声卡驱动模式的一种简称,简单说Windows的在音频领域的实时性不够好,所以为了能绕过Windows在驱动上的限制而开发出来的一种驱动模式,通过这种模式可以把音频的延迟降到底10ms级别,可以说非常块了。

但是要支持ASIO,还需要有硬件驱动支持,并不是任何一个声卡都可以使用ASIO的驱动模式。

DirectSound

DirectSound,是DirectXAudio下的一个部件,它本身可以认为是对音频设备的一层抽象,基本是Windows标配,他的延迟比ASIO要高一些,但是比起其他方式低很多了。

image-20230925161102319

网易云的输出设备选择

WaveOut

Waveout ,是比较老的播放音频的接口,兼容老设备的时候经常需要选择这种模式的接口

WASAPI

Windows Audio Session API,WASAPI是Windows Core Audio的一部分,是从Vista开始引入的,作为应用层最底层的音频API。所以现在用的DirectSound系列也好,waveXxx(也就是MME)也好,他们都是基于Core Audio的,而音频流的管理,则是通过WASAPI。

WASAPI想要解决的问题就是ASIO解决的问题,低延迟。

MME

MME,MultiMedia Extensions,多媒体扩展,级别最低的驱动,属于上古产品,替代他的就是WaveOut

WDM

WDM,Windows Driver Model,这个是比较大的说法,windows基本上大部分驱动都可以统一到这个模型下,自然音频驱动也在其中。

还有一个WDM,可以认为是音频通道跳线系统,主要是将各个音频输入以后,通过软跳线选择,将他们输出到各个输出设备的一个选择软件。

GSIF

GSIF,是TASCAM GigaStudio软件(一个虚拟音频采样设备)连接到电脑声卡之间的专用协议和API,英文缩写可能是GigaSampler InterFace。GSIF的重点是降低音频传输的延迟。Tascam已于2008年7月31日停止开发GigaStudio及相关软件,同年12月31日停止GigaStudio销售和技术支持。这个基本已经消失在历史长河中了。

NAudio

https://github.com/naudio/NAudio

NAudio,一个C#用来处理Audio和MIDI的库。

Summary

得亏是有python解析的例子,不然想找这个解析还真有点难。

Quote

https://zhuanlan.zhihu.com/p/101728723

https://blog.frame.io/2017/07/17/timecode-and-frame-rates/

https://www.douyin.com/video/7158293264326495528

https://developer.huawei.com/consumer/cn/doc/development/Media-Guides/ltc-code-0000001525719745

https://www.cnblogs.com/CodeWorkerLiMing/p/12007384.html

https://stackoverflow.com/questions/2099016/extracting-smpte-timecode-from-audio-stream?rq=4

https://stackoverflow.com/questions/33722080/c-sharp-read-ltc-timecode-from-audio-stream

https://blog.csdn.net/MosesAaron/article/details/122781256

https://www.cnblogs.com/PeaZomboss/p/17035785.html

https://learn.microsoft.com/zh-cn/windows-hardware/drivers/audio/introduction-to-wdm-audio-drivers

https://learn.microsoft.com/en-us/previous-versions//dd757347(v=vs.85)?redirectedfrom=MSDN

http://www.philrees.co.uk/articles/timecode.htm#word

https://forum.arduino.cc/t/realizing-an-smpte-timecode-reader-generator-with-arduino/124085