diff --git a/Runtime/OggEncoder.cs b/Runtime/OggEncoder.cs index 80908ec..14dd900 100644 --- a/Runtime/OggEncoder.cs +++ b/Runtime/OggEncoder.cs @@ -21,6 +21,7 @@ public class OggEncoder : IEncoder [Preserve] public OggEncoder() { } + [Preserve] public static float[][] ConvertSamples(float[] samples, int channels) { var buffer = new float[channels][]; @@ -54,13 +55,15 @@ private static void ValidateSamples(float[][] samples, int channels) } } + [Preserve] public static byte[] ConvertToBytes(float[] samples, int sampleRate, int channels, float quality = 1f) => ConvertToBytes(ConvertSamples(samples, channels), sampleRate, channels, quality); + [Preserve] public static byte[] ConvertToBytes(float[][] samples, int sampleRate, int channels, float quality = 1f) { ValidateSamples(samples, channels); - InitOggStream(sampleRate, channels, quality, out var oggStream, out var processingState); + WriteOggHeader(sampleRate, channels, quality, out var oggStream, out var processingState); using var outStream = new MemoryStream(); var sampleLength = samples[0].Length; oggStream.FlushPages(outStream, false); @@ -70,13 +73,15 @@ public static byte[] ConvertToBytes(float[][] samples, int sampleRate, int chann return outStream.ToArray(); } + [Preserve] public static async Task ConvertToBytesAsync(float[] samples, int sampleRate, int channels, float quality = 1f, CancellationToken cancellationToken = default) => await ConvertToBytesAsync(ConvertSamples(samples, channels), sampleRate, channels, quality, cancellationToken); + [Preserve] public static async Task ConvertToBytesAsync(float[][] samples, int sampleRate, int channels, float quality = 1f, CancellationToken cancellationToken = default) { ValidateSamples(samples, channels); - InitOggStream(sampleRate, channels, quality, out var oggStream, out var processingState); + WriteOggHeader(sampleRate, channels, quality, out var oggStream, out var processingState); using var outStream = new MemoryStream(); var sampleLength = samples[0].Length; await oggStream.FlushPagesAsync(outStream, false, cancellationToken).ConfigureAwait(false); @@ -84,238 +89,237 @@ public static async Task ConvertToBytesAsync(float[][] samples, int samp processingState.WriteEndOfStream(); await oggStream.FlushPagesAsync(outStream, true, cancellationToken).ConfigureAwait(false); var result = outStream.ToArray(); - await outStream.DisposeAsync().ConfigureAwait(false); await Awaiters.UnityMainThread; return result; } - public async Task> StreamSaveToDiskAsync(AudioClip clip, string saveDirectory, CancellationToken cancellationToken, Action> callback = null, [CallerMemberName] string callingMethodName = null) + /// + [Preserve] + public Task StreamRecordingAsync(ClipData microphoneClipData, Func, Task> bufferCallback, CancellationToken cancellationToken, string callingMethodName = null) + => throw new NotImplementedException("Use PCMEncoder instead"); + + /// + [Preserve] + public async Task> StreamSaveToDiskAsync(ClipData clipData, string saveDirectory, Action> callback, CancellationToken cancellationToken, [CallerMemberName] string callingMethodName = null) { if (callingMethodName != nameof(RecordingManager.StartRecordingAsync)) { - throw new InvalidOperationException($"{nameof(StreamSaveToDiskAsync)} can only be called from {nameof(RecordingManager.StartRecordingAsync)}"); - } - - if (!Microphone.IsRecording(null)) - { - throw new InvalidOperationException("Microphone is not initialized!"); - } - - if (RecordingManager.IsProcessing) - { - throw new AccessViolationException("Recoding already in progress!"); + throw new InvalidOperationException($"{nameof(StreamSaveToDiskAsync)} can only be called from {nameof(RecordingManager.StartRecordingAsync)} not {callingMethodName}"); } + var outputPath = string.Empty; RecordingManager.IsProcessing = true; + Tuple result = null; - if (RecordingManager.EnableDebug) - { - Debug.Log($"[{nameof(RecordingManager)}] Recording process started..."); - } - - var sampleCount = 0; - var clipName = clip.name; - var channels = clip.channels; - var bufferSize = clip.samples; - var sampleRate = clip.frequency; - var sampleBuffer = new float[bufferSize]; - var maxSamples = RecordingManager.MaxRecordingLength * sampleRate; - var finalSamples = new float[maxSamples]; - - if (RecordingManager.EnableDebug) - { - Debug.Log($"[{nameof(RecordingManager)}] Initializing data for {clipName}. Channels: {channels}, Sample Rate: {sampleRate}, Sample buffer size: {bufferSize}, Max Sample Length: {maxSamples}"); - } - - if (!Directory.Exists(saveDirectory)) + try { - Directory.CreateDirectory(saveDirectory); - } + Stream outStream; - var path = $"{saveDirectory}/{clipName}.ogg"; + if (!string.IsNullOrWhiteSpace(saveDirectory)) + { - if (File.Exists(path)) - { - Debug.LogWarning($"[{nameof(RecordingManager)}] {path} already exists, attempting to delete..."); - File.Delete(path); - } + if (!Directory.Exists(saveDirectory)) + { + Directory.CreateDirectory(saveDirectory); + } - var outStream = new FileStream(path, FileMode.Create, FileAccess.Write); + outputPath = $"{saveDirectory}/{clipData.Name}.ogg"; - try - { - // setup recording - var shouldStop = false; - var lastMicrophonePosition = 0; - var channelBuffer = new float[channels][]; + if (File.Exists(outputPath)) + { + Debug.LogWarning($"[{nameof(RecordingManager)}] {outputPath} already exists, attempting to delete..."); + File.Delete(outputPath); + } - for (var i = 0; i < channels; i++) + outStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write); + } + else { - channelBuffer[i] = new float[sampleBuffer.Length]; + outStream = new MemoryStream(); } - // initialize file header - InitOggStream(sampleRate, channels, 0.5f, out OggStream oggStream, out ProcessingState processingState); + var totalSampleCount = 0; + var finalSamples = new float[clipData.MaxSamples]; try { - do + WriteOggHeader(clipData.SampleRate, clipData.Channels, 0.5f, out OggStream oggStream, out ProcessingState processingState); + + try { - // Expected to be on the Unity Main Thread. - await Awaiters.UnityMainThread; - var microphonePosition = Microphone.GetPosition(null); + var sampleCount = 0; + var shouldStop = false; + var lastMicrophonePosition = 0; + var channelBuffer = new float[clipData.Channels][]; + var sampleBuffer = new float[clipData.BufferSize]; - if (microphonePosition <= 0 && lastMicrophonePosition == 0) + for (var i = 0; i < clipData.Channels; i++) { - // Skip this iteration if there's no new data - // wait for next update - await Awaiters.UnityMainThread; - continue; + channelBuffer[i] = new float[sampleBuffer.Length]; } - var isLooping = microphonePosition < lastMicrophonePosition; - int samplesToWrite; - - if (isLooping) + do { - // Microphone loopback detected. - samplesToWrite = bufferSize - lastMicrophonePosition; + await Awaiters.UnityMainThread; // ensure we're on main thread to call unity apis + var microphonePosition = Microphone.GetPosition(clipData.Device); - if (RecordingManager.EnableDebug) + if (microphonePosition <= 0 && lastMicrophonePosition == 0) { - Debug.LogWarning($"[{nameof(RecordingManager)}] Microphone loopback detected! [{microphonePosition} < {lastMicrophonePosition}] samples to write: {samplesToWrite}"); + // Skip this iteration if there's no new data + // wait for next update + continue; } - } - else - { - // No loopback, process normally. - samplesToWrite = microphonePosition - lastMicrophonePosition; - } - if (samplesToWrite > 0) - { - clip.GetData(sampleBuffer, 0); + var isLooping = microphonePosition < lastMicrophonePosition; + int samplesToWrite; - for (var i = 0; i < samplesToWrite; i++) + if (isLooping) { - // Write pcm data to buffer. - var bufferIndex = (lastMicrophonePosition + i) % bufferSize; // Wrap around index. - var sample = sampleBuffer[bufferIndex]; + // Microphone loopback detected. + samplesToWrite = clipData.BufferSize - lastMicrophonePosition; - for (var channel = 0; channel < channels; channel++) + if (RecordingManager.EnableDebug) { - channelBuffer[channel][i] = sample; + Debug.LogWarning($"[{nameof(RecordingManager)}] Microphone loopback detected! [{microphonePosition} < {lastMicrophonePosition}] samples to write: {samplesToWrite}"); } - - // Store the sample in the final samples array. - finalSamples[sampleCount * channels + i] = sampleBuffer[bufferIndex]; } + else + { + // No loopback, process normally. + samplesToWrite = microphonePosition - lastMicrophonePosition; + } + + if (samplesToWrite > 0) + { + clipData.Clip.GetData(sampleBuffer, 0); + + for (var i = 0; i < samplesToWrite; i++) + { + var bufferIndex = (lastMicrophonePosition + i) % clipData.BufferSize; // Wrap around index. + var sample = sampleBuffer[bufferIndex]; + + for (var channel = 0; channel < clipData.Channels; channel++) + { + channelBuffer[channel][i] = sample; + } + + // Store the sample in the final samples array. + 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}"); + } - lastMicrophonePosition = microphonePosition; - sampleCount += samplesToWrite; + await FlushPagesAsync(oggStream, outStream, false).ConfigureAwait(false); + ProcessChunk(oggStream, processingState, channelBuffer, samplesToWrite); + } - if (RecordingManager.EnableDebug) + // Check if we have recorded enough samples or if cancellation has been requested + if (oggStream.Finished || sampleCount >= clipData.MaxSamples || cancellationToken.IsCancellationRequested) { - Debug.Log($"[{nameof(RecordingManager)}] State: {nameof(RecordingManager.IsRecording)}? {RecordingManager.IsRecording} | Wrote {samplesToWrite} samples | last mic pos: {lastMicrophonePosition} | total samples: {sampleCount} | isCancelled? {cancellationToken.IsCancellationRequested}"); + shouldStop = true; } + } while (!shouldStop); + + totalSampleCount = sampleCount; + } + catch (Exception e) + { + Debug.LogError($"[{nameof(RecordingManager)}] Failed to write to clip file!\n{e}"); + } + finally + { + // Expected to be on the Unity Main Thread. + await Awaiters.UnityMainThread; + RecordingManager.IsRecording = false; + Microphone.End(null); - await FlushPagesAsync(oggStream, outStream, false).ConfigureAwait(false); - ProcessChunk(oggStream, processingState, channelBuffer, samplesToWrite); + if (RecordingManager.EnableDebug) + { + Debug.Log($"[{nameof(RecordingManager)}] Recording stopped, writing end of stream..."); } - // Check if we have recorded enough samples or if cancellation has been requested - if (oggStream.Finished || sampleCount >= maxSamples || cancellationToken.IsCancellationRequested) + processingState.WriteEndOfStream(); + + // Process any remaining packets after writing the end of stream + while (processingState.PacketOut(out var packet)) { - shouldStop = true; + oggStream.PacketIn(packet); } - } while (!shouldStop); + + await FlushPagesAsync(oggStream, outStream, true); + + if (RecordingManager.EnableDebug) + { + Debug.Log($"[{nameof(RecordingManager)}] Flush stream..."); + } + + // ReSharper disable once MethodSupportsCancellation + await outStream.FlushAsync().ConfigureAwait(false); + + if (RecordingManager.EnableDebug) + { + Debug.Log($"[{nameof(RecordingManager)}] Stream disposed. File write operation complete."); + } + } } catch (Exception e) { - Debug.LogError($"[{nameof(RecordingManager)}] Failed to write to clip file!\n{e}"); + Debug.LogError($"[{nameof(RecordingManager)}] Failed to record clip!\n{e}"); + RecordingManager.IsRecording = false; + RecordingManager.IsProcessing = false; + return null; } finally { - // Expected to be on the Unity Main Thread. - await Awaiters.UnityMainThread; - RecordingManager.IsRecording = false; - Microphone.End(null); - if (RecordingManager.EnableDebug) { - Debug.Log($"[{nameof(RecordingManager)}] Recording stopped, writing end of stream..."); + Debug.Log($"[{nameof(RecordingManager)}] Dispose stream..."); } - processingState.WriteEndOfStream(); - - // Process any remaining packets after writing the end of stream - while (processingState.PacketOut(out var packet)) - { - oggStream.PacketIn(packet); - } + await outStream.DisposeAsync().ConfigureAwait(false); + } - await FlushPagesAsync(oggStream, outStream, true); + if (RecordingManager.EnableDebug) + { + Debug.Log($"[{nameof(RecordingManager)}] Finalized file write. Copying recording into new AudioClip"); + } - if (RecordingManager.EnableDebug) - { - Debug.Log($"[{nameof(RecordingManager)}] Flush stream..."); - } + // Trim the final samples down into the recorded range. + var microphoneData = new float[totalSampleCount * clipData.Channels]; + Array.Copy(finalSamples, microphoneData, microphoneData.Length); - // ReSharper disable once MethodSupportsCancellation - await outStream.FlushAsync().ConfigureAwait(false); + // Expected to be on the Unity Main Thread. + await Awaiters.UnityMainThread; - if (RecordingManager.EnableDebug) - { - Debug.Log($"[{nameof(RecordingManager)}] Stream disposed. File write operation complete."); - } - } + // Create a new copy of the final recorded clip. + var newClip = AudioClip.Create(clipData.Name, microphoneData.Length, clipData.Channels, clipData.SampleRate, false); + newClip.SetData(microphoneData, 0); + result = new Tuple(outputPath, newClip); + callback?.Invoke(result); } catch (Exception e) { - Debug.LogError($"[{nameof(RecordingManager)}] Failed to record clip!\n{e}"); - RecordingManager.IsRecording = false; - RecordingManager.IsProcessing = false; - return null; + Debug.LogException(e); } finally { + RecordingManager.IsProcessing = false; + if (RecordingManager.EnableDebug) { - Debug.Log($"[{nameof(RecordingManager)}] Dispose stream..."); + Debug.Log($"[{nameof(RecordingManager)}] Finished processing..."); } - - await outStream.DisposeAsync().ConfigureAwait(false); } - - if (RecordingManager.EnableDebug) - { - Debug.Log($"[{nameof(RecordingManager)}] Finalized file write. Copying recording into new AudioClip"); - } - - // Trim the final samples down into the recorded range. - var microphoneData = new float[sampleCount * channels]; - Array.Copy(finalSamples, microphoneData, microphoneData.Length); - - // Expected to be on the Unity Main Thread. - await Awaiters.UnityMainThread; - - // Create a new copy of the final recorded clip. - var newClip = AudioClip.Create(clipName, microphoneData.Length, channels, sampleRate, false); - newClip.SetData(microphoneData, 0); - var result = new Tuple(path, newClip); - - RecordingManager.IsProcessing = false; - - if (RecordingManager.EnableDebug) - { - Debug.Log($"[{nameof(RecordingManager)}] Finished processing..."); - } - - callback?.Invoke(result); return result; } - private static void InitOggStream(int sampleRate, int channels, float quality, out OggStream oggStream, out ProcessingState processingState) + private static void WriteOggHeader(int sampleRate, int channels, float quality, out OggStream oggStream, out ProcessingState processingState) { // Stores all the static vorbis bitstream settings var info = VorbisInfo.InitVariableBitRate(channels, sampleRate, quality); @@ -332,7 +336,6 @@ private static void InitOggStream(int sampleRate, int channels, float quality, o // bitstream spec. The second header holds any comment fields. The // third header holds the bitstream codebook. var comments = new Comments(); - var infoPacket = HeaderPacketBuilder.BuildInfoPacket(info); var commentsPacket = HeaderPacketBuilder.BuildCommentsPacket(comments); var booksPacket = HeaderPacketBuilder.BuildBooksPacket(info); diff --git a/Samples~/wav_sample.wav b/Samples~/wav_sample.wav deleted file mode 100644 index 6c8f8ca..0000000 Binary files a/Samples~/wav_sample.wav and /dev/null differ diff --git a/Samples~/wav_sample.wav.meta b/Samples~/wav_sample.wav.meta deleted file mode 100644 index 8ecec79..0000000 --- a/Samples~/wav_sample.wav.meta +++ /dev/null @@ -1,22 +0,0 @@ -fileFormatVersion: 2 -guid: acabee125b76955428f48ef078f1de9b -AudioImporter: - externalObjects: {} - serializedVersion: 6 - defaultSettings: - loadType: 0 - sampleRateSetting: 0 - sampleRateOverride: 44100 - compressionFormat: 1 - quality: 1 - conversionMode: 0 - platformSettingOverrides: {} - forceToMono: 0 - normalize: 1 - preloadAudioData: 1 - loadInBackground: 0 - ambisonic: 0 - 3D: 1 - userData: - assetBundleName: - assetBundleVariant: diff --git a/package.json b/package.json index dd5ef03..4d1e542 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Utilities.Encoder.Ogg", "description": "Simple library for Ogg encoding support.", "keywords": [], - "version": "3.1.6", + "version": "4.0.0", "unity": "2021.3", "documentationUrl": "https://github.com/RageAgainstThePixel/com.utilities.encoder.ogg#documentation", "changelogUrl": "https://github.com/RageAgainstThePixel/com.utilities.encoder.ogg/releases", @@ -15,7 +15,7 @@ "author": "Stephen Hodgson", "url": "https://github.com/StephenHodgson", "dependencies": { - "com.utilities.audio": "1.2.3" + "com.utilities.audio": "2.0.1" }, "samples": [ {