Last active
December 13, 2022 20:05
-
-
Save blkcatman/35fffc8e566a81aad09045ec24ee7bb4 to your computer and use it in GitHub Desktop.
Unity上で音声信号からLinear timecodeに変換するヘルパーメソッド
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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