Skip to content

Commit

Permalink
com.utilities.encoder.wav 2.1.0 (#24)
Browse files Browse the repository at this point in the history
- com.utilities.audio -> 2.1.0
- better support for output sample rate when recording
  • Loading branch information
StephenHodgson authored Jan 19, 2025
1 parent df124fb commit c8e0007
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 129 deletions.
27 changes: 12 additions & 15 deletions Runtime/AudioClipExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,15 @@ public static byte[] EncodeToWav(this AudioClip audioClip, bool trim)
/// <param name="audioClip"><see cref="AudioClip"/> to convert.</param>
/// <param name="bitDepth">Optional, bit depth to encode. Defaults to <see cref="PCMFormatSize.SixteenBit"/>.</param>
/// <param name="trim">Optional, trim the silence at beginning and end.</param>
/// <param name="outputSampleRate">Optional, the expected sample rate. Defaults to 44100.</param>
/// <returns><see cref="AudioClip"/> encoded to WAV as byte array.</returns>
public static byte[] EncodeToWav(this AudioClip audioClip, PCMFormatSize bitDepth = PCMFormatSize.SixteenBit, bool trim = false)
public static byte[] EncodeToWav(this AudioClip audioClip, PCMFormatSize bitDepth = PCMFormatSize.SixteenBit, bool trim = false, int outputSampleRate = 44100)
{
if (audioClip == null)
{
throw new ArgumentNullException(nameof(audioClip));
}

if (audioClip == null) { throw new ArgumentNullException(nameof(audioClip)); }
var bitsPerSample = 8 * (int)bitDepth;
var sampleRate = audioClip.frequency;
var channels = audioClip.channels;
var pcmData = audioClip.EncodeToPCM(bitDepth, trim);
var pcmData = audioClip.EncodeToPCM(bitDepth, trim, outputSampleRate);
return WavEncoder.EncodeWav(pcmData, channels, sampleRate, bitsPerSample);
}

Expand All @@ -50,23 +47,23 @@ public static async Task<byte[]> EncodeToWavAsync(this AudioClip audioClip, bool
/// <param name="audioClip"><see cref="AudioClip"/> to convert.</param>
/// <param name="bitDepth">Optional, bit depth to encode. Defaults to <see cref="PCMFormatSize.SixteenBit"/>.</param>
/// <param name="trim">Optional, trim the silence at beginning and end.</param>
/// <param name="outputSampleRate">Optional, the expected sample rate. Defaults to 44100.</param>
/// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
/// <returns><see cref="MemoryStream"/>.</returns>
public static async Task<byte[]> EncodeToWavAsync(this AudioClip audioClip, PCMFormatSize bitDepth = PCMFormatSize.SixteenBit, bool trim = false, CancellationToken cancellationToken = default)
public static async Task<byte[]> EncodeToWavAsync(this AudioClip audioClip, PCMFormatSize bitDepth = PCMFormatSize.SixteenBit, bool trim = false, int outputSampleRate = 44100, CancellationToken cancellationToken = default)
{
if (audioClip == null)
{
throw new ArgumentNullException(nameof(audioClip));
}

await Awaiters.UnityMainThread; // ensure we're on main thread so we can access unity apis
if (audioClip == null) { throw new ArgumentNullException(nameof(audioClip)); }
await Awaiters.UnityMainThread; // ensure we're on main thread, so we can access unity apis
cancellationToken.ThrowIfCancellationRequested();
var bitsPerSample = 8 * (int)bitDepth;
var sampleRate = audioClip.frequency;
var channels = audioClip.channels;
var pcmData = audioClip.EncodeToPCM(bitDepth, trim);
var pcmData = audioClip.EncodeToPCM(bitDepth, trim, outputSampleRate);
await Awaiters.BackgroundThread; // switch to background thread to prevent blocking main thread
cancellationToken.ThrowIfCancellationRequested();
var encodedBytes = WavEncoder.EncodeWav(pcmData, channels, sampleRate, bitsPerSample);
await Awaiters.UnityMainThread; // return to main thread before returning result
cancellationToken.ThrowIfCancellationRequested();
return encodedBytes;
}
}
Expand Down
125 changes: 13 additions & 112 deletions Runtime/WavEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using UnityEngine.Scripting;
using Utilities.Async;
using Utilities.Audio;
using Microphone = Utilities.Audio.Microphone;

namespace Utilities.Encoding.Wav
{
Expand All @@ -30,6 +29,7 @@ internal static byte[] EncodeWav(byte[] pcmData, int channels, int sampleRate, i
return stream.ToArray();
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteWavHeader(BinaryWriter writer, int channels, int sampleRate, int bitsPerSample = 16, int pcmDataLength = 0)
{
// We'll calculate the file size and protect against overflow.
Expand Down Expand Up @@ -84,7 +84,7 @@ public async Task StreamRecordingAsync(ClipData clipData, Func<ReadOnlyMemory<by
{
using var stream = new MemoryStream();
await using var writer = new BinaryWriter(stream);
WriteWavHeader(writer, clipData.Channels, clipData.SampleRate);
WriteWavHeader(writer, clipData.Channels, clipData.OutputSampleRate);
writer.Flush();
var headerData = stream.ToArray();

Expand All @@ -94,7 +94,7 @@ public async Task StreamRecordingAsync(ClipData clipData, Func<ReadOnlyMemory<by
}

await bufferCallback.Invoke(headerData);
await InternalStreamRecordAsync(clipData, null, bufferCallback, cancellationToken).ConfigureAwait(false);
await PCMEncoder.InternalStreamRecordAsync(clipData, null, bufferCallback, PCMEncoder.DefaultSampleProvider, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
Expand Down Expand Up @@ -161,20 +161,23 @@ public async Task<Tuple<string, AudioClip>> StreamSaveToDiskAsync(ClipData clipD
}

var totalSampleCount = 0;
var finalSamples = new float[clipData.MaxSamples ?? clipData.SampleRate * RecordingManager.MaxRecordingLength];
var maxSampleLength = clipData.MaxSamples ?? clipData.OutputSampleRate * RecordingManager.MaxRecordingLength * clipData.Channels;
var finalSamples = new float[maxSampleLength];
var writer = new BinaryWriter(outStream);

try
{
WriteWavHeader(writer, clipData.Channels, clipData.SampleRate);
WriteWavHeader(writer, clipData.Channels, clipData.OutputSampleRate);

try
{
(finalSamples, totalSampleCount) = await InternalStreamRecordAsync(clipData, finalSamples, async buffer =>
async Task BufferCallback(ReadOnlyMemory<byte> buffer)
{
writer.Write(buffer.Span);
await Task.Yield();
}, cancellationToken).ConfigureAwait(true);
}

(finalSamples, totalSampleCount) = await PCMEncoder.InternalStreamRecordAsync(clipData, finalSamples, BufferCallback, PCMEncoder.DefaultSampleProvider, cancellationToken).ConfigureAwait(true);
}
finally
{
Expand Down Expand Up @@ -235,7 +238,7 @@ public async Task<Tuple<string, AudioClip>> StreamSaveToDiskAsync(ClipData clipD
Array.Copy(finalSamples, microphoneData, microphoneData.Length);
await Awaiters.UnityMainThread; // switch back to main thread to call unity apis
// Create a new copy of the final recorded clip.
var newClip = AudioClip.Create(clipData.Name, microphoneData.Length, clipData.Channels, clipData.SampleRate, false);
var newClip = AudioClip.Create(clipData.Name, microphoneData.Length, clipData.Channels, clipData.OutputSampleRate, false);
newClip.SetData(microphoneData, 0);
result = new Tuple<string, AudioClip>(outputPath, newClip);
callback?.Invoke(result);
Expand All @@ -258,9 +261,10 @@ public static async Task WriteToFileAsync(string path, byte[] pcmData, int chann
try
{
await Awaiters.BackgroundThread;
var bitsPerSample = 8 * (int)bitDepth;
await using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true);
await using var writer = new BinaryWriter(fileStream);
cancellationToken.ThrowIfCancellationRequested();
var bitsPerSample = 8 * (int)bitDepth;
WriteWavHeader(writer, channels, sampleRate, bitsPerSample, pcmData.Length);
writer.Write(pcmData);
writer.Flush();
Expand All @@ -270,108 +274,5 @@ public static async Task WriteToFileAsync(string path, byte[] pcmData, int chann
Debug.LogException(e);
}
}

private static async Task<(float[], int)> InternalStreamRecordAsync(ClipData clipData, float[] finalSamples, Func<ReadOnlyMemory<byte>, Task> bufferCallback, CancellationToken cancellationToken)
{
try
{
var sampleCount = 0;
var shouldStop = false;
var lastMicrophonePosition = 0;
var sampleBuffer = new float[clipData.BufferSize];
do
{
await Awaiters.UnityMainThread; // ensure we're on main thread to call unity apis
var microphonePosition = Microphone.GetPosition(clipData.Device);

if (microphonePosition <= 0 && lastMicrophonePosition == 0)
{
// Skip this iteration if there's no new data
// wait for next update
continue;
}

var isLooping = microphonePosition < lastMicrophonePosition;
int samplesToWrite;

if (isLooping)
{
// Microphone loopback detected.
samplesToWrite = clipData.BufferSize - lastMicrophonePosition;

if (RecordingManager.EnableDebug)
{
Debug.LogWarning($"[{nameof(RecordingManager)}] Microphone loopback detected! [{microphonePosition} < {lastMicrophonePosition}] samples to write: {samplesToWrite}");
}
}
else
{
// No loopback, process normally.
samplesToWrite = microphonePosition - lastMicrophonePosition;
}

if (samplesToWrite > 0)
{
cancellationToken.ThrowIfCancellationRequested();
clipData.Clip.GetData(sampleBuffer, 0);

for (var i = 0; i < samplesToWrite; i++)
{
var bufferIndex = (lastMicrophonePosition + i) % clipData.BufferSize; // Wrap around index.
var value = sampleBuffer[bufferIndex];
var sample = (short)(Math.Max(-1f, Math.Min(1f, value)) * short.MaxValue);
var sampleData = new ReadOnlyMemory<byte>(new[]
{
(byte)(sample & byte.MaxValue),
(byte)(sample >> 8 & byte.MaxValue)
});

try
{
await bufferCallback.Invoke(sampleData).ConfigureAwait(false);
}
catch (Exception e)
{
Debug.LogException(new Exception($"[{nameof(WavEncoder)}] error occurred when buffering audio", e));
}

if (finalSamples is { Length: > 0 })
{
finalSamples[sampleCount * clipData.Channels + i] = sampleBuffer[bufferIndex];
}
}

lastMicrophonePosition = microphonePosition;
sampleCount += samplesToWrite;

if (RecordingManager.EnableDebug)
{
Debug.Log($"[{nameof(RecordingManager)}] State: {nameof(RecordingManager.IsRecording)}? {RecordingManager.IsRecording} | Wrote {samplesToWrite} samples | last mic pos: {lastMicrophonePosition} | total samples: {sampleCount} | isCancelled? {cancellationToken.IsCancellationRequested}");
}
}

if (clipData.MaxSamples.HasValue && sampleCount >= clipData.MaxSamples || cancellationToken.IsCancellationRequested)
{
if (RecordingManager.EnableDebug)
{
Debug.Log("Breaking internal record loop!");
}

shouldStop = true;
}
} while (!shouldStop);
return (finalSamples, sampleCount);
}
finally
{
RecordingManager.IsRecording = false;
Microphone.End(clipData.Device);

if (RecordingManager.EnableDebug)
{
Debug.Log($"[{nameof(RecordingManager)}] Recording stopped");
}
}
}
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Utilities.Encoder.Wav",
"description": "Simple library for WAV encoding support.",
"keywords": [],
"version": "2.0.2",
"version": "2.1.0",
"unity": "2021.3",
"documentationUrl": "https://github.com/RageAgainstThePixel/com.utilities.encoder.wav#documentation",
"changelogUrl": "https://github.com/RageAgainstThePixel/com.utilities.encoder.wav/releases",
Expand All @@ -17,7 +17,7 @@
"url": "https://github.com/StephenHodgson"
},
"dependencies": {
"com.utilities.audio": "2.0.2"
"com.utilities.audio": "2.1.0"
},
"samples": [
{
Expand Down

0 comments on commit c8e0007

Please sign in to comment.