Skip to content

Instantly share code, notes, and snippets.

@blkcatman
Last active December 13, 2022 20:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blkcatman/35fffc8e566a81aad09045ec24ee7bb4 to your computer and use it in GitHub Desktop.
Save blkcatman/35fffc8e566a81aad09045ec24ee7bb4 to your computer and use it in GitHub Desktop.
Unity上で音声信号からLinear timecodeに変換するヘルパーメソッド
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Mathematics;
public static class LtcHelper
{
public static readonly string SyncWord = "0011111111111101";
public struct WaveLengthData
{
public int Position;
public float WaveLength;
public float Sign;
}
public static void GetMaxSignalLevels(in Span<float> sourceBuffer, int totalChannels, ref Span<float> levels)
{
if (levels.Length < totalChannels) throw new OutOfMemoryException();
for (int i = 0; i < sourceBuffer.Length; i += totalChannels)
{
// チャンネルごとに最大値を取得する
for (int j = 0; j < levels.Length; j++)
{
levels[j] = math.max(math.abs(sourceBuffer[i + j]), levels[j]);
}
}
}
public static bool HasSignal(in Span<float> levels, float threshold)
{
var result = false;
foreach (var level in levels)
{
// しきい値以上であればtrueを返却する
result = level > threshold;
}
return result;
}
public static void GetMonauralData(
in ReadOnlySpan<float> sourceBuffer,
ref Span<float> destBuffer,
int totalChannels,
int targetChannel)
{
if (destBuffer.Length < sourceBuffer.Length / totalChannels) throw new OutOfMemoryException();
var length = sourceBuffer.Length / totalChannels;
for (int i = 0; i < length; i++)
{
destBuffer[i] = sourceBuffer[i * totalChannels + targetChannel];
}
}
public static void Binarize(in ReadOnlySpan<float> sourceBuffer, ref Span<float> destBuffer)
{
if (destBuffer.Length < sourceBuffer.Length) throw new OutOfMemoryException();
for (var i = 0; i < sourceBuffer.Length; i++)
{
// sign関数を使って信号を1.0f, -1.0f, (0.0f)に丸める
destBuffer[i] = math.sign(sourceBuffer[i]);
}
}
public static IReadOnlyList<WaveLengthData> GetWaveLengthArrayFromBinarizedFloats(
in ReadOnlySpan<float> sourceBuffer,
int samplingRate)
{
var list = new List<WaveLengthData>();
float currentAmplitude = sourceBuffer[0];
int localCount = 0;
int globalCount = 0;
foreach (var source in sourceBuffer)
{
localCount++;
globalCount++;
// 信号が正負反転している場合、カウントを区切る
if (source * currentAmplitude < 0f && math.abs(source) > 0.99f)
{
// 波長データを追加する
list.Add(new WaveLengthData()
{
Position = globalCount,
WaveLength = (float)localCount / samplingRate,
Sign = currentAmplitude
});
currentAmplitude = source;
localCount = 0;
}
}
return list;
}
// ビットデータタイプ
// 0 = Bit0
// 1 = Bit1
// E = エラー
public static string DecodeWaveLengthToBits(
in IEnumerable<WaveLengthData> waveLengthArray,
float minWaveLengthBit0,
float maxWaveLengthBit0,
float minWaveLengthBit1,
float maxWaveLengthBit1)
{
string bitData = "";
// 最初のデータに対して符号を逆転させる
float currentSign = waveLengthArray.FirstOrDefault().Sign > 0f ? -1f : 1f;
bool hasHalfBit1 = false;
foreach (var waveLengthData in waveLengthArray)
{
var sign = waveLengthData.Sign;
var wave = waveLengthData.WaveLength;
// 波長からBit0またはBit1かを判定する
var bit0Result = wave > minWaveLengthBit0 && wave < maxWaveLengthBit0;
var bit1Result = wave > minWaveLengthBit1 && wave < maxWaveLengthBit1;
// データがBit0でもBit1でも無い場合はエラーとする
if (!(bit0Result ^ bit1Result))
{
bitData += 'E';
continue;
}
if (bit0Result)
{
// 一つ前の要素でBit1のフラグが立っている場合は、前の要素をエラーとする
if (hasHalfBit1)
{
bitData += 'E';
hasHalfBit1 = false;
}
bitData += '0';
}
else if (hasHalfBit1)
{
// Bit1表現は逆相とペアになっているので、前の要素と合わせてBit1として表現する
// 一つ前の要素の符号が反転していればBit1とみなし、反転していない場合はエラーとする
if (sign * currentSign < 0f)
bitData += '1';
else
bitData += 'E';
hasHalfBit1 = false;
}
else
{
// Bit1のフラグを建てる
hasHalfBit1 = true;
}
currentSign = sign;
}
return bitData;
}
public static bool DecodeBitsToTimecode(string bitData, out Timecode timecode)
{
timecode = default;
// データが80ビット未満の場合はデコード失敗扱いにする
if (bitData.Length < 80) return false;
// ビットデータにエラーが含まれている場合はデコード失敗扱いにする
if (bitData.LastIndexOf('E') > -1) return false;
// フレームをデコードする
var frameOnes = Convert.ToInt32(Reverse(bitData.Substring(0, 4)), 2);
var frameTens = Convert.ToInt32(Reverse(bitData.Substring(8, 2)), 2);
timecode.Frame = frameOnes + frameTens * 10;
// DropFrame, ColorFrameのフラグを取得する
timecode.DropFrame = CheckBit(bitData[10]);
timecode.ColorFrame = CheckBit(bitData[11]);
// 秒をデコードする
var secondsOnes = Convert.ToInt32(Reverse(bitData.Substring(16, 4)), 2);
var secondsTens = Convert.ToInt32(Reverse(bitData.Substring(24, 3)), 2);
timecode.Seconds = secondsOnes + secondsTens * 10;
// 極性補正(PolarityCorrection)のフラグを取得する
timecode.PolarityCorrection = CheckBit(bitData[27]);
// 分をデコードする
var minutesOnes = Convert.ToInt32(Reverse(bitData.Substring(32, 4)), 2);
var minutesTens = Convert.ToInt32(Reverse(bitData.Substring(40, 3)), 2);
timecode.Minutes = minutesOnes + minutesTens * 10;
// BinaryGroup0フラグを取得する
timecode.BinaryGroupFlag0 = CheckBit(bitData[43]);
// 時をデコードする
var hoursOnes = Convert.ToInt32(Reverse(bitData.Substring(48, 4)), 2);
var hoursTens = Convert.ToInt32(Reverse(bitData.Substring(56, 2)), 2);
timecode.Hours = hoursOnes + hoursTens * 10;
// BinaryGroup1, BinaryGroup2のフラグを取得する
timecode.BinaryGroupFlag1 = CheckBit(bitData[58]);
timecode.BinaryGroupFlag2 = CheckBit(bitData[59]);
// 同期ワード(SyncWord)をチェックする
if (!string.Equals(bitData.Substring(64, 16), SyncWord)) return false;
return true;
}
private static string Reverse(string data)
{
// 文字列を逆転する
return new string(data.Reverse().ToArray());
}
private static bool CheckBit(char bit)
{
// Bit1の場合はtrueを返す
return bit == '1';
}
}
#nullable enable
using UnityEngine;
using System.Linq;
public class MicrophoneGrabber : MonoBehaviour
{
public delegate void AudioDataReadDelegate(float[] data, int channels);
public event AudioDataReadDelegate? OnAudioDataRead;
[SerializeField]
private string micFilterName = string.Empty;
[SerializeField]
private int bufferSeconds = 1;
public int SamplingRate => 44100;
private void Start()
{
var deviceName = GetDeviceName(micFilterName);
if (deviceName == null)
{
Debug.LogWarning($"Audio Device (keywords: {micFilterName}) was not found.");
return;
}
var clip = Microphone.Start(deviceName, true, bufferSeconds, SamplingRate);
var source = gameObject.AddComponent<AudioSource>();
source.clip = clip;
source.loop = true;
while (Microphone.GetPosition(deviceName) < 0) { }
source.Play();
}
private void OnAudioFilterRead(float[] data, int channels) {
// オーディオデータの処理を外部に移譲する
OnAudioDataRead?.Invoke(data, channels);
// そのままだとTimecodeの音声が鳴り響くので、ゲインを0にする
for (int i = 0; i < data.Length; i++)
{
data[i] *= 0f;
}
}
private static string? GetDeviceName(string filterName)
{
return Microphone.devices.FirstOrDefault(device => device.Contains(filterName));
}
}
public struct Timecode
{
public int Frame;
public int Seconds;
public int Minutes;
public int Hours;
public bool DropFrame;
public bool ColorFrame;
public bool PolarityCorrection;
public bool BinaryGroupFlag0;
public bool BinaryGroupFlag1;
public bool BinaryGroupFlag2;
public override string ToString()
{
return $"{Hours:00}:{Minutes:00}:{Seconds:00}:{Frame:00}";
}
}
using System;
using UnityEngine;
[RequireComponent(typeof(MicrophoneGrabber))]
public class TimecodeDecoder : MonoBehaviour
{
[SerializeField]
private MicrophoneGrabber microphoneGrabber;
private int samplingRate;
private float[] binaryBuffer;
private int currentBinarySize;
public Timecode CurrentTime { get; private set; }
private void Reset()
{
microphoneGrabber = GetComponent<MicrophoneGrabber>();
}
private void ProcessAudioData(float[] data, int channels)
{
// ここにTimecode取得の処理を記述する
Span<float> maxLevels = stackalloc float[channels];
LtcHelper.GetMaxSignalLevels(data, channels, ref maxLevels);
// 信号の振幅が一定未満なら、無信号とみなす
if (!LtcHelper.HasSignal(maxLevels, 0.2f))
{
if (currentBinarySize > 0)
{
Span<float> source = new Span<float>(binaryBuffer, 0, currentBinarySize);
source.Fill(0f);
currentBinarySize = 0;
}
return;
}
// チャンネル一つぶんの信号を取り出す
var monauralDataLength = data.Length / channels;
float[] monauralData = new float[monauralDataLength];
Span<float> monauralSource = new Span<float>(monauralData);
LtcHelper.GetMonauralData(data, ref monauralSource, channels, 0);
// 信号を2値化する
float[] binarizedData = new float[monauralDataLength];
Span<float> binarizedDest = new Span<float>(binarizedData);
LtcHelper.Binarize(monauralSource, ref binarizedDest);
// 2値化データをバッファにコピーする
var storeDest = new Span<float>(binaryBuffer, currentBinarySize, monauralDataLength);
binarizedDest.CopyTo(storeDest);
currentBinarySize += monauralDataLength;
// 一定以上データをバッファに蓄積したら処理を行う
if (currentBinarySize > 2000)
{
// 信号の波長を取得する
var signalSource = new ReadOnlySpan<float>(binaryBuffer, 0, currentBinarySize);
var waveLengthArray =
LtcHelper.GetWaveLengthArrayFromBinarizedFloats(signalSource, samplingRate);
// ビットデータ配列に変換する
var bitArray = LtcHelper.DecodeWaveLengthToBits(waveLengthArray,
0.0004f,
0.0005f,
0.0002f,
0.00026f
);
// ビットデータ配列内にSyncWordが含まれているかチェックする
var syncWordPosition = bitArray.LastIndexOf(LtcHelper.SyncWord, StringComparison.Ordinal);
// SyncWordが入っていない場合は処理を中断する
if (syncWordPosition < 0)
{
// 1秒以上バッファにデータがたまっているときはバッファを初期化する
if (currentBinarySize > samplingRate)
{
var clearSource = new Span<float>(binaryBuffer);
clearSource.Fill(0f);
currentBinarySize = 0;
}
return;
}
// SyncWordが含まれている箇所より前にタイムコードを計算できるデータ量が含まれている時
if (syncWordPosition >= 64)
{
// タイムコード80bitぶんのデータを切り出す
var bitData = bitArray.Substring(syncWordPosition - 64, 80);
// タイムコードに変換する
if (LtcHelper.DecodeBitsToTimecode(bitData, out var timecode))
{
CurrentTime = timecode;
}
}
// 後処理: バッファの先頭からSyncWordの末尾の位置を取得する
var segmentPosition = waveLengthArray[syncWordPosition + 16].Position;
var tempBufferSize = currentBinarySize - segmentPosition;
if (tempBufferSize > 0)
{
// SyncWordの末尾からのデータをバッファの先頭に移動する
var tempBuffer = new float[tempBufferSize];
var temp = new Span<float>(tempBuffer);
// Tempバッファにデータをコピー
var tempSource = new Span<float>(binaryBuffer, segmentPosition, tempBufferSize);
tempSource.CopyTo(temp);
// Tempバッファを2値化データ用のバッファの先頭にコピーする
var bufferDest = new Span<float>(binaryBuffer, 0, tempBufferSize);
temp.CopyTo(bufferDest);
currentBinarySize = tempBufferSize;
}
}
}
private void Start()
{
samplingRate = microphoneGrabber.SamplingRate;
binaryBuffer = new float[samplingRate * 2];
microphoneGrabber.OnAudioDataRead += ProcessAudioData;
}
private void OnApplicationQuit()
{
microphoneGrabber.OnAudioDataRead -= ProcessAudioData;
}
}
using TMPro;
using UnityEngine;
[RequireComponent(typeof(TimecodeDecoder))]
public class TimecodeViewer : MonoBehaviour
{
[SerializeField]
private TimecodeDecoder timecodeDecoder;
[SerializeField]
private TextMeshProUGUI tmpText;
private void Reset()
{
timecodeDecoder = GetComponent<TimecodeDecoder>();
}
void Update()
{
if (tmpText != null && timecodeDecoder != null)
{
tmpText.text = timecodeDecoder.CurrentTime.ToString();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment