diff --git a/Codecs/SoundFlow.Codecs.FFMpeg/SoundFlow.Codecs.FFMpeg.csproj b/Codecs/SoundFlow.Codecs.FFMpeg/SoundFlow.Codecs.FFMpeg.csproj index 239af7f8..aabd1913 100644 --- a/Codecs/SoundFlow.Codecs.FFMpeg/SoundFlow.Codecs.FFMpeg.csproj +++ b/Codecs/SoundFlow.Codecs.FFMpeg/SoundFlow.Codecs.FFMpeg.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable true diff --git a/Extensions/SoundFlow.Extensions.WebRtc.Apm/SoundFlow.Extensions.WebRtc.Apm.csproj b/Extensions/SoundFlow.Extensions.WebRtc.Apm/SoundFlow.Extensions.WebRtc.Apm.csproj index 02de82ba..c6d69074 100644 --- a/Extensions/SoundFlow.Extensions.WebRtc.Apm/SoundFlow.Extensions.WebRtc.Apm.csproj +++ b/Extensions/SoundFlow.Extensions.WebRtc.Apm/SoundFlow.Extensions.WebRtc.Apm.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable true diff --git a/Midi/SoundFlow.Midi.PortMidi/SoundFlow.Midi.PortMidi.csproj b/Midi/SoundFlow.Midi.PortMidi/SoundFlow.Midi.PortMidi.csproj index 2c62b85d..1a65555b 100644 --- a/Midi/SoundFlow.Midi.PortMidi/SoundFlow.Midi.PortMidi.csproj +++ b/Midi/SoundFlow.Midi.PortMidi/SoundFlow.Midi.PortMidi.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable true diff --git a/Native/miniaudio-backend/build-wasm/build.ps1 b/Native/miniaudio-backend/build-wasm/build.ps1 index d6718749..b81babd0 100644 --- a/Native/miniaudio-backend/build-wasm/build.ps1 +++ b/Native/miniaudio-backend/build-wasm/build.ps1 @@ -1,8 +1,8 @@ # Set up Paths -$Env:EMSDK_PYTHON = "C:\Program Files\dotnet\packs\Microsoft.NET.Runtime.Emscripten.3.1.34.Python.win-x64\8.0.22\tools\python.exe" -$Env:DOTNET_EMSCRIPTEN_LLVM_ROOT = "C:\Program Files\dotnet\packs\Microsoft.NET.Runtime.Emscripten.3.1.34.Sdk.win-x64\8.0.22\tools\bin" -$Env:DOTNET_EMSCRIPTEN_BINARYEN_ROOT = "C:\Program Files\dotnet\packs\Microsoft.NET.Runtime.Emscripten.3.1.34.Sdk.win-x64\8.0.22\tools" -$Env:DOTNET_EMSCRIPTEN_NODE_JS = "C:\Program Files\dotnet\packs\Microsoft.NET.Runtime.Emscripten.3.1.34.Node.win-x64\8.0.22\tools\bin\node.exe" +$Env:EMSDK_PYTHON = "C:\Program Files\dotnet\packs\Microsoft.NET.Runtime.Emscripten.3.1.56.Python.win-x64\10.0.6\tools\python.exe" +$Env:DOTNET_EMSCRIPTEN_LLVM_ROOT = "C:\Program Files\dotnet\packs\Microsoft.NET.Runtime.Emscripten.3.1.56.Sdk.win-x64\10.0.6\tools\bin" +$Env:DOTNET_EMSCRIPTEN_BINARYEN_ROOT = "C:\Program Files\dotnet\packs\Microsoft.NET.Runtime.Emscripten.3.1.56.Sdk.win-x64\10.0.6\tools" +$Env:DOTNET_EMSCRIPTEN_NODE_JS = "C:\Program Files\dotnet\packs\Microsoft.NET.Runtime.Emscripten.3.1.56.Node.win-x64\10.0.6\tools\bin\node.exe" # Set Cache Location $Env:EM_CACHE = "$env:USERPROFILE\.emscripten_cache_dotnet" @@ -12,11 +12,11 @@ $Env:EM_FROZEN_CACHE = "0" # Clean previous build artifacts, excluding this script Write-Host "Cleaning build directory..." -Get-ChildItem -Path . -Exclude 'build.ps1' | Remove-Item -Recurse -Force +# Get-ChildItem -Path . -Exclude 'build.ps1' | Remove-Item -Recurse -Force # Configure Write-Host "Configuring with emcmake..." -& "C:\Program Files\dotnet\packs\Microsoft.NET.Runtime.Emscripten.3.1.34.Sdk.win-x64\8.0.22\tools\emscripten\emcmake.bat" cmake .. -DCMAKE_BUILD_TYPE=Release +& "C:\Program Files\dotnet\packs\Microsoft.NET.Runtime.Emscripten.3.1.56.Sdk.win-x64\10.0.6\tools\emscripten\emcmake.bat" cmake .. -DCMAKE_BUILD_TYPE=Release # Build Write-Host "Building with cmake..." diff --git a/Native/miniaudio-backend/build-wasm/libminiaudio.a b/Native/miniaudio-backend/build-wasm/libminiaudio.a index 5e83d022..2ac6a99d 100644 Binary files a/Native/miniaudio-backend/build-wasm/libminiaudio.a and b/Native/miniaudio-backend/build-wasm/libminiaudio.a differ diff --git a/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Browser/SoundFlow.Samples.AvaloniaCrossPlatform.Browser.csproj b/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Browser/SoundFlow.Samples.AvaloniaCrossPlatform.Browser.csproj index 4e7e8e52..454ce78b 100644 --- a/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Browser/SoundFlow.Samples.AvaloniaCrossPlatform.Browser.csproj +++ b/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Browser/SoundFlow.Samples.AvaloniaCrossPlatform.Browser.csproj @@ -1,7 +1,7 @@  Exe - net8.0-browser + net10.0-browser browser-wasm true wwwroot\main.js @@ -13,49 +13,101 @@ - + - - - + + miniaudio + false - - + + + false + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + diff --git a/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Browser/libminiaudio.a b/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Browser/libminiaudio.a index 5e83d022..2ac6a99d 100644 Binary files a/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Browser/libminiaudio.a and b/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Browser/libminiaudio.a differ diff --git a/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Desktop/SoundFlow.Samples.AvaloniaCrossPlatform.Desktop.csproj b/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Desktop/SoundFlow.Samples.AvaloniaCrossPlatform.Desktop.csproj index fad2cb22..05ab6f76 100644 --- a/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Desktop/SoundFlow.Samples.AvaloniaCrossPlatform.Desktop.csproj +++ b/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.Desktop/SoundFlow.Samples.AvaloniaCrossPlatform.Desktop.csproj @@ -1,9 +1,7 @@  WinExe - - net8.0 + net10.0 enable true @@ -13,9 +11,9 @@ - + - + diff --git a/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.csproj b/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.csproj index 70b32459..4421dbd9 100644 --- a/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.csproj +++ b/Samples/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform/SoundFlow.Samples.AvaloniaCrossPlatform.csproj @@ -1,6 +1,6 @@  - net8.0 + net10.0 enable latest true @@ -11,12 +11,12 @@ - - - - + + + + - + diff --git a/Samples/SoundFlow.Samples.EditingMixer/SoundFlow.Samples.EditingMixer.csproj b/Samples/SoundFlow.Samples.EditingMixer/SoundFlow.Samples.EditingMixer.csproj index ad8a5794..e06eb1c2 100644 --- a/Samples/SoundFlow.Samples.EditingMixer/SoundFlow.Samples.EditingMixer.csproj +++ b/Samples/SoundFlow.Samples.EditingMixer/SoundFlow.Samples.EditingMixer.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.Midi.BasicSequencing/SoundFlow.Samples.Midi.BasicSequencing.csproj b/Samples/SoundFlow.Samples.Midi.BasicSequencing/SoundFlow.Samples.Midi.BasicSequencing.csproj index 20abf27c..047c0a2f 100644 --- a/Samples/SoundFlow.Samples.Midi.BasicSequencing/SoundFlow.Samples.Midi.BasicSequencing.csproj +++ b/Samples/SoundFlow.Samples.Midi.BasicSequencing/SoundFlow.Samples.Midi.BasicSequencing.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.Midi.PropertyMapping/SoundFlow.Samples.Midi.PropertyMapping.csproj b/Samples/SoundFlow.Samples.Midi.PropertyMapping/SoundFlow.Samples.Midi.PropertyMapping.csproj index 1fb77325..4b4dfce8 100644 --- a/Samples/SoundFlow.Samples.Midi.PropertyMapping/SoundFlow.Samples.Midi.PropertyMapping.csproj +++ b/Samples/SoundFlow.Samples.Midi.PropertyMapping/SoundFlow.Samples.Midi.PropertyMapping.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.Midi.SynthesisModifiers/SoundFlow.Samples.Midi.SynthesisModifiers.csproj b/Samples/SoundFlow.Samples.Midi.SynthesisModifiers/SoundFlow.Samples.Midi.SynthesisModifiers.csproj index 20abf27c..047c0a2f 100644 --- a/Samples/SoundFlow.Samples.Midi.SynthesisModifiers/SoundFlow.Samples.Midi.SynthesisModifiers.csproj +++ b/Samples/SoundFlow.Samples.Midi.SynthesisModifiers/SoundFlow.Samples.Midi.SynthesisModifiers.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.MultiEngines/SoundFlow.Samples.MultiEngines.csproj b/Samples/SoundFlow.Samples.MultiEngines/SoundFlow.Samples.MultiEngines.csproj index 20abf27c..047c0a2f 100644 --- a/Samples/SoundFlow.Samples.MultiEngines/SoundFlow.Samples.MultiEngines.csproj +++ b/Samples/SoundFlow.Samples.MultiEngines/SoundFlow.Samples.MultiEngines.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.NoiseSuppression/SoundFlow.Samples.NoiseSuppression.csproj b/Samples/SoundFlow.Samples.NoiseSuppression/SoundFlow.Samples.NoiseSuppression.csproj index 8dce7096..2f70f319 100644 --- a/Samples/SoundFlow.Samples.NoiseSuppression/SoundFlow.Samples.NoiseSuppression.csproj +++ b/Samples/SoundFlow.Samples.NoiseSuppression/SoundFlow.Samples.NoiseSuppression.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.Recording/SoundFlow.Samples.Recording.csproj b/Samples/SoundFlow.Samples.Recording/SoundFlow.Samples.Recording.csproj index 20abf27c..047c0a2f 100644 --- a/Samples/SoundFlow.Samples.Recording/SoundFlow.Samples.Recording.csproj +++ b/Samples/SoundFlow.Samples.Recording/SoundFlow.Samples.Recording.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.Security.Authentication/SoundFlow.Samples.Security.Authentication.csproj b/Samples/SoundFlow.Samples.Security.Authentication/SoundFlow.Samples.Security.Authentication.csproj index 20abf27c..047c0a2f 100644 --- a/Samples/SoundFlow.Samples.Security.Authentication/SoundFlow.Samples.Security.Authentication.csproj +++ b/Samples/SoundFlow.Samples.Security.Authentication/SoundFlow.Samples.Security.Authentication.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.Security.Encryption/SoundFlow.Samples.Security.Encryption.csproj b/Samples/SoundFlow.Samples.Security.Encryption/SoundFlow.Samples.Security.Encryption.csproj index 20abf27c..047c0a2f 100644 --- a/Samples/SoundFlow.Samples.Security.Encryption/SoundFlow.Samples.Security.Encryption.csproj +++ b/Samples/SoundFlow.Samples.Security.Encryption/SoundFlow.Samples.Security.Encryption.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.Security.Fingerprinting/SoundFlow.Samples.Security.Fingerprinting.csproj b/Samples/SoundFlow.Samples.Security.Fingerprinting/SoundFlow.Samples.Security.Fingerprinting.csproj index 20abf27c..047c0a2f 100644 --- a/Samples/SoundFlow.Samples.Security.Fingerprinting/SoundFlow.Samples.Security.Fingerprinting.csproj +++ b/Samples/SoundFlow.Samples.Security.Fingerprinting/SoundFlow.Samples.Security.Fingerprinting.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.Security.IntegrityWatermarking/SoundFlow.Samples.Security.IntegrityWatermarking.csproj b/Samples/SoundFlow.Samples.Security.IntegrityWatermarking/SoundFlow.Samples.Security.IntegrityWatermarking.csproj index 20abf27c..047c0a2f 100644 --- a/Samples/SoundFlow.Samples.Security.IntegrityWatermarking/SoundFlow.Samples.Security.IntegrityWatermarking.csproj +++ b/Samples/SoundFlow.Samples.Security.IntegrityWatermarking/SoundFlow.Samples.Security.IntegrityWatermarking.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.Security.OwnershipWatermarking/SoundFlow.Samples.Security.OwnershipWatermarking.csproj b/Samples/SoundFlow.Samples.Security.OwnershipWatermarking/SoundFlow.Samples.Security.OwnershipWatermarking.csproj index 20abf27c..047c0a2f 100644 --- a/Samples/SoundFlow.Samples.Security.OwnershipWatermarking/SoundFlow.Samples.Security.OwnershipWatermarking.csproj +++ b/Samples/SoundFlow.Samples.Security.OwnershipWatermarking/SoundFlow.Samples.Security.OwnershipWatermarking.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.SimplePlayer/SoundFlow.Samples.SimplePlayer.csproj b/Samples/SoundFlow.Samples.SimplePlayer/SoundFlow.Samples.SimplePlayer.csproj index 20abf27c..047c0a2f 100644 --- a/Samples/SoundFlow.Samples.SimplePlayer/SoundFlow.Samples.SimplePlayer.csproj +++ b/Samples/SoundFlow.Samples.SimplePlayer/SoundFlow.Samples.SimplePlayer.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Samples/SoundFlow.Samples.SwitchDevices/SoundFlow.Samples.SwitchDevices.csproj b/Samples/SoundFlow.Samples.SwitchDevices/SoundFlow.Samples.SwitchDevices.csproj index 20abf27c..047c0a2f 100644 --- a/Samples/SoundFlow.Samples.SwitchDevices/SoundFlow.Samples.SwitchDevices.csproj +++ b/Samples/SoundFlow.Samples.SwitchDevices/SoundFlow.Samples.SwitchDevices.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/Src/Backends/MiniAudio/Devices/MiniAudioDevice.cs b/Src/Backends/MiniAudio/Devices/MiniAudioDevice.cs index f8da3214..464a389f 100644 --- a/Src/Backends/MiniAudio/Devices/MiniAudioDevice.cs +++ b/Src/Backends/MiniAudio/Devices/MiniAudioDevice.cs @@ -9,7 +9,7 @@ namespace SoundFlow.Backends.MiniAudio.Devices; internal delegate void OnProcessCallback(nint pOutput, nint pInput, uint frameCount, MiniAudioDevice device); -internal sealed class MiniAudioDevice : IDisposable +internal sealed unsafe class MiniAudioDevice : IDisposable { private readonly nint _device; private readonly OnProcessCallback _onProcess; @@ -24,7 +24,7 @@ public MiniAudioDevice(AudioDevice owner, nint context, DeviceInfo? info, AudioF { if (config is not MiniAudioDeviceConfig miniAudioDeviceConfig) throw new ArgumentException($"config must be of type {typeof(MiniAudioDeviceConfig)}"); - + Info = info; Format = format; _onProcess = onProcess; @@ -41,12 +41,24 @@ public MiniAudioDevice(AudioDevice owner, nint context, DeviceInfo? info, AudioF var pSfConfig = MarshalConfig( miniAudioDeviceConfig, configHandles); +#if BROWSER + var callback = (delegate* unmanaged[Cdecl])&MiniAudioEngine.OnAudioData; + var pointer = (IntPtr)callback; + + var deviceConfig = Native.AllocateDeviceConfig( + Capability, + (uint)Format.SampleRate, + pointer, + pSfConfig + ); +#else var deviceConfig = Native.AllocateDeviceConfig( Capability, (uint)Format.SampleRate, MiniAudioEngine.DataCallback, pSfConfig ); +#endif _device = Native.AllocateDevice(); var result = Native.DeviceInit(context, deviceConfig, _device); @@ -55,9 +67,10 @@ public MiniAudioDevice(AudioDevice owner, nint context, DeviceInfo? info, AudioF if (result != MiniAudioResult.Success) { Native.Free(_device); - throw new InvalidOperationException($"Unable to init device {info?.Name ?? "Default Device"}. Result: {result}"); + throw new InvalidOperationException( + $"Unable to init device {info?.Name ?? "Default Device"}. Result: {result}"); } - + MiniAudioEngine.RegisterEngineHandle(_device, Engine); } finally @@ -188,7 +201,7 @@ public void Process(nint pOutput, nint pInput, uint frameCount) public void Dispose() { Stop(); - + // Remove the device from the engine's instance map. Engine.UnregisterDevice(_device); MiniAudioEngine.UnregisterEngineHandle(_device); diff --git a/Src/Backends/MiniAudio/MiniAudioEngine.cs b/Src/Backends/MiniAudio/MiniAudioEngine.cs index 4904d0fe..1c8ffac2 100644 --- a/Src/Backends/MiniAudio/MiniAudioEngine.cs +++ b/Src/Backends/MiniAudio/MiniAudioEngine.cs @@ -9,6 +9,10 @@ using SoundFlow.Backends.MiniAudio.Enums; using SoundFlow.Backends.MiniAudio.Structs; +#if BROWSER +using System.Runtime.CompilerServices; +#endif + namespace SoundFlow.Backends.MiniAudio; /// @@ -20,7 +24,9 @@ public class MiniAudioEngine : AudioEngine private readonly List _activeDevices = []; private readonly MiniAudioBackend[]? _backendPriority; +#if !BROWSER internal static readonly Native.AudioCallback DataCallback = OnAudioData; +#endif private readonly ConcurrentDictionary _deviceMap = new(); private static readonly ConcurrentDictionary ActiveEngineHandles = new(); @@ -113,13 +119,16 @@ public MiniAudioEngine(IEnumerable? backendPriority = null) InitializeBackend(); } - private static void OnAudioData(nint pDevice, nint pOutput, nint pInput, uint frameCount) +#if BROWSER + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] +#endif + internal static void OnAudioData(nint pDevice, nint pOutput, nint pInput, uint frameCount) { // Look up the GCHandle using the native device pointer. if (!ActiveEngineHandles.TryGetValue(pDevice, out var engineHandle) || engineHandle.Target is not MiniAudioEngine engine || !engine._deviceMap.TryGetValue(pDevice, out var managedDevice)) return; - + // Safely get the engine instance from the handle. managedDevice.Process(pOutput, pInput, frameCount); } @@ -327,7 +336,7 @@ public override FullDuplexDevice SwitchDevice(FullDuplexDevice oldDevice, Device return newDevice; } - + private void OnDeviceDisposing(object? sender, EventArgs e) { if (sender is AudioDevice device) @@ -360,7 +369,7 @@ public override void UpdateAudioDevicesInfo() if (result != MiniAudioResult.Success) throw new InvalidOperationException($"Unable to get devices. MiniAudio result: {result}"); - + var playbackCount = (int)playbackCountUint; var captureCount = (int)captureCountUint; @@ -373,7 +382,7 @@ public override void UpdateAudioDevicesInfo() var nativePlayback = new DeviceInfoNative[playbackCount]; // 2. Read from pointer into native structs pPlaybackDevices.ReadIntoArray(nativePlayback, playbackCount); - + // 3. Convert to public structs (Deep Copy) PlaybackDevices = new DeviceInfo[playbackCount]; for (var i = 0; i < playbackCount; i++) diff --git a/Src/Backends/MiniAudio/Native.cs b/Src/Backends/MiniAudio/Native.cs index 69e4aefc..8e177483 100644 --- a/Src/Backends/MiniAudio/Native.cs +++ b/Src/Backends/MiniAudio/Native.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using SoundFlow.Backends.MiniAudio.Enums; using SoundFlow.Enums; @@ -7,31 +8,33 @@ namespace SoundFlow.Backends.MiniAudio; internal static unsafe partial class Native { + #if BROWSER -private const string LibraryName = "__Internal"; // Nothing works, not "__Internal" or "*" or "miniaudio" + private const string LibraryName = "libminiaudio"; #else private const string LibraryName = "miniaudio"; -#endif +#endif + #region Delegates - + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void AudioCallback(nint device, nint output, nint input, uint length); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate MiniAudioResult BufferProcessingCallback( - nint pCodecContext, // The native decoder/encoder instance pointer (ma_decoder*, ma_encoder*) - nint pBuffer, // The buffer pointer (void* pBufferOut or const void* pBufferIn) - ulong bytesRequested, // The number of bytes requested (bytesToRead or bytesToWrite) - out ulong bytesTransferred // The actual number of bytes processed/transferred (size_t*) + nint pCodecContext, // The native decoder/encoder instance pointer (ma_decoder*, ma_encoder*) + nint pBuffer, // The buffer pointer (void* pBufferOut or const void* pBufferIn) + ulong bytesRequested, // The number of bytes requested (bytesToRead or bytesToWrite) + out ulong bytesTransferred // The actual number of bytes processed/transferred (size_t*) ); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate MiniAudioResult SeekCallback(nint pDecoder, long byteOffset, SeekPoint origin); - + #endregion - + #region Initialization - + static Native() { // Ignore Resolver in Browser since it throws "MONO_WASM: Exception marshalling result of JS promise to CS" @@ -41,23 +44,11 @@ static Native() private static class NativeLibraryResolver { + [UnsupportedOSPlatform("browser")] public static nint Resolve(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { - // 1. Get the platform-specific library file name (e.g., "libminiaudio.so", "miniaudio.dll"). var platformSpecificName = GetPlatformSpecificLibraryName(libraryName); - - // Handle Browser (WASM) specifically. - /* - * Actually throwing in browser console - * logging.ts:26 MONO_WASM: Exception marshalling result of JS promise to CS: ExitStatusmessage: "Program terminated with exit(1)"name: "ExitStatus"silent: truestack: "Error\n at Object.Me [as mono_exit] (https://localhost:5001/_framework/dotnet.js:3:18993)\n at Fl.e.onAbort.e.onAbort (https://localhost:5001/_framework/dotnet.runtime.js:3:215571)\n at abort (https://localhost:5001/_framework/dotnet.native.js:859:22)\n at _dlopen (https://localhost:5001/_framework/dotnet.native.js:5296:7)\n at SystemNative_GetDefaultSearchOrderPseudoHandle (https://localhost:5001/_framework/dotnet.native.wasm:wasm-function[27957]:0x8949aa)\n at do_icall (https://localhost:5001/_framework/dotnet.native.wasm:wasm-function[16249]:0x637f86)\n at do_icall_wrapper (https://localhost:5001/_framework/dotnet.native.wasm:wasm-function[16134]:0x632f0a)\n at mono_interp_exec_method (https://localhost:5001/_framework/dotnet.native.wasm:wasm-function[16127]:0x624cf7)\n at interp_runtime_invoke (https://localhost:5001/_framework/dotnet.native.wasm:wasm-function[16167]:0x63481f)\n at mono_jit_runtime_invoke (https://localhost:5001/_framework/dotnet.native.wasm:wasm-function[20704]:0x749bee)"status: 1[[Prototype]]: Object -pt @ logging.ts:26 -(anonymous) @ marshal-to-cs.ts:339 - */ - if (OperatingSystem.IsBrowser()) - { - return NativeLibrary.GetMainProgramHandle(); - } // 2. Try to load the library using its platform-specific name, allowing OS to find it in standard paths. if (NativeLibrary.TryLoad(platformSpecificName, assembly, searchPath, out var library)) @@ -69,11 +60,11 @@ public static nint Resolve(string libraryName, Assembly assembly, DllImportSearc if (File.Exists(fullPath) && NativeLibrary.TryLoad(fullPath, out library)) return library; - + Console.WriteLine($"Loading ${platformSpecificName} from path: {relativePath} || Full path {fullPath}"); - + // 4. If not found, use Load() to let the runtime throw a detailed DllNotFoundException. - return NativeLibrary.Load(fullPath); + return NativeLibrary.Load(fullPath); } /// @@ -86,11 +77,11 @@ private static string GetPlatformSpecificLibraryName(string libraryName) if (OperatingSystem.IsMacOS()) return $"lib{libraryName}.dylib"; - + // For iOS frameworks, the binary has the same name as the framework if (OperatingSystem.IsIOS()) return libraryName; - + if (OperatingSystem.IsBrowser()) return $"lib{libraryName}.a"; @@ -141,7 +132,7 @@ private static string GetLibraryPath(string libraryName) } else if (OperatingSystem.IsAndroid()) { - rid = RuntimeInformation.ProcessArchitecture switch + rid = RuntimeInformation.ProcessArchitecture switch { Architecture.X64 => "android-x64", Architecture.Arm => "android-arm", @@ -172,7 +163,7 @@ private static string GetLibraryPath(string libraryName) }; } else if (OperatingSystem.IsBrowser()) - { + { return Path.Combine(relativeBase, "browser-wasm", "native", platformSpecificName); } else @@ -184,109 +175,246 @@ private static string GetLibraryPath(string libraryName) return Path.Combine(relativeBase, rid, "native", platformSpecificName); } } - + #endregion - + #region Encoder +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_encoder_init")] + public static extern MiniAudioResult EncoderInit(BufferProcessingCallback onRead, SeekCallback onSeekCallback, + nint pUserData, nint pConfig, nint pEncoder); +#else [LibraryImport(LibraryName, EntryPoint = "ma_encoder_init", StringMarshalling = StringMarshalling.Utf8)] - public static partial MiniAudioResult EncoderInit(BufferProcessingCallback onRead, SeekCallback onSeekCallback, nint pUserData, nint pConfig, nint pEncoder); + public static partial MiniAudioResult EncoderInit(BufferProcessingCallback onRead, SeekCallback onSeekCallback, + nint pUserData, nint pConfig, nint pEncoder); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_encoder_uninit")] + public static extern void EncoderUninit(nint pEncoder); +#else [LibraryImport(LibraryName, EntryPoint = "ma_encoder_uninit")] public static partial void EncoderUninit(nint pEncoder); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_encoder_write_pcm_frames")] + public static extern MiniAudioResult EncoderWritePcmFrames(nint pEncoder, nint pFramesIn, ulong frameCount, + out ulong pFramesWritten); +#else [LibraryImport(LibraryName, EntryPoint = "ma_encoder_write_pcm_frames")] public static partial MiniAudioResult EncoderWritePcmFrames(nint pEncoder, nint pFramesIn, ulong frameCount, out ulong pFramesWritten); +#endif #endregion #region Decoder +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_decoder_init")] + public static extern MiniAudioResult DecoderInit(BufferProcessingCallback onRead, SeekCallback onSeekCallback, + nint pUserData, + nint pConfig, nint pDecoder); +#else [LibraryImport(LibraryName, EntryPoint = "ma_decoder_init")] - public static partial MiniAudioResult DecoderInit(BufferProcessingCallback onRead, SeekCallback onSeekCallback, nint pUserData, + public static partial MiniAudioResult DecoderInit(BufferProcessingCallback onRead, SeekCallback onSeekCallback, + nint pUserData, nint pConfig, nint pDecoder); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_decoder_uninit")] + public static extern MiniAudioResult DecoderUninit(nint pDecoder); +#else [LibraryImport(LibraryName, EntryPoint = "ma_decoder_uninit")] public static partial MiniAudioResult DecoderUninit(nint pDecoder); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_decoder_read_pcm_frames")] + public static extern MiniAudioResult DecoderReadPcmFrames(nint decoder, nint framesOut, uint frameCount, + out ulong framesRead); +#else [LibraryImport(LibraryName, EntryPoint = "ma_decoder_read_pcm_frames")] public static partial MiniAudioResult DecoderReadPcmFrames(nint decoder, nint framesOut, uint frameCount, out ulong framesRead); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_decoder_seek_to_pcm_frame")] + public static extern MiniAudioResult DecoderSeekToPcmFrame(nint decoder, ulong frame); +#else [LibraryImport(LibraryName, EntryPoint = "ma_decoder_seek_to_pcm_frame")] public static partial MiniAudioResult DecoderSeekToPcmFrame(nint decoder, ulong frame); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_decoder_get_length_in_pcm_frames")] + public static extern MiniAudioResult DecoderGetLengthInPcmFrames(nint decoder, out ulong length); +#else [LibraryImport(LibraryName, EntryPoint = "ma_decoder_get_length_in_pcm_frames")] public static partial MiniAudioResult DecoderGetLengthInPcmFrames(nint decoder, out ulong length); +#endif #endregion #region Context +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_context_init")] + public static extern MiniAudioResult ContextInit(nint backends, uint backendCount, nint config, nint context); +#else [LibraryImport(LibraryName, EntryPoint = "ma_context_init")] public static partial MiniAudioResult ContextInit(nint backends, uint backendCount, nint config, nint context); - +#endif + +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_context_uninit")] + public static extern void ContextUninit(nint context); +#else [LibraryImport(LibraryName, EntryPoint = "ma_context_uninit")] public static partial void ContextUninit(nint context); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "sf_context_get_backend")] + public static extern MiniAudioBackend ContextGetBackend(nint context); +#else [LibraryImport(LibraryName, EntryPoint = "sf_context_get_backend")] public static partial MiniAudioBackend ContextGetBackend(nint context); +#endif #endregion #region Device +#if BROWSER + [DllImport(LibraryName, EntryPoint = "sf_get_devices")] + public static extern MiniAudioResult GetDevices(nint context, out nint pPlaybackDevices, out nint pCaptureDevices, + out uint playbackDeviceCount, out uint captureDeviceCount); +#else [LibraryImport(LibraryName, EntryPoint = "sf_get_devices")] - public static partial MiniAudioResult GetDevices(nint context, out nint pPlaybackDevices, out nint pCaptureDevices, out uint playbackDeviceCount, out uint captureDeviceCount); + public static partial MiniAudioResult GetDevices(nint context, out nint pPlaybackDevices, out nint pCaptureDevices, + out uint playbackDeviceCount, out uint captureDeviceCount); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_device_init")] + public static extern MiniAudioResult DeviceInit(nint context, nint config, nint device); +#else [LibraryImport(LibraryName, EntryPoint = "ma_device_init")] public static partial MiniAudioResult DeviceInit(nint context, nint config, nint device); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_device_uninit")] + public static extern void DeviceUninit(nint device); +#else [LibraryImport(LibraryName, EntryPoint = "ma_device_uninit")] public static partial void DeviceUninit(nint device); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_device_start")] + public static extern MiniAudioResult DeviceStart(nint device); +#else [LibraryImport(LibraryName, EntryPoint = "ma_device_start")] public static partial MiniAudioResult DeviceStart(nint device); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "ma_device_stop")] + public static extern MiniAudioResult DeviceStop(nint device); +#else [LibraryImport(LibraryName, EntryPoint = "ma_device_stop")] public static partial MiniAudioResult DeviceStop(nint device); +#endif #endregion #region Allocations +#if BROWSER + [DllImport(LibraryName, EntryPoint = "sf_allocate_encoder")] + public static extern nint AllocateEncoder(); +#else [LibraryImport(LibraryName, EntryPoint = "sf_allocate_encoder")] public static partial nint AllocateEncoder(); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "sf_allocate_decoder")] + public static extern nint AllocateDecoder(); +#else [LibraryImport(LibraryName, EntryPoint = "sf_allocate_decoder")] public static partial nint AllocateDecoder(); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "sf_allocate_context")] + public static extern nint AllocateContext(); +#else [LibraryImport(LibraryName, EntryPoint = "sf_allocate_context")] public static partial nint AllocateContext(); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "sf_allocate_device")] + public static extern nint AllocateDevice(); +#else [LibraryImport(LibraryName, EntryPoint = "sf_allocate_device")] public static partial nint AllocateDevice(); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "sf_allocate_decoder_config")] + public static extern nint AllocateDecoderConfig(SampleFormat format, uint channels, uint sampleRate); +#else [LibraryImport(LibraryName, EntryPoint = "sf_allocate_decoder_config")] public static partial nint AllocateDecoderConfig(SampleFormat format, uint channels, uint sampleRate); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "sf_allocate_encoder_config")] + public static extern nint AllocateEncoderConfig(SampleFormat format, uint channels, + uint sampleRate); +#else [LibraryImport(LibraryName, EntryPoint = "sf_allocate_encoder_config")] public static partial nint AllocateEncoderConfig(SampleFormat format, uint channels, uint sampleRate); +#endif +#if BROWSER + [DllImport(LibraryName, EntryPoint = "sf_allocate_device_config")] + public static extern nint AllocateDeviceConfig(Capability capabilityType, uint sampleRate, + IntPtr dataCallback, nint pSfConfig); +#else [LibraryImport(LibraryName, EntryPoint = "sf_allocate_device_config")] - public static partial nint AllocateDeviceConfig(Capability capabilityType, uint sampleRate, AudioCallback dataCallback, nint pSfConfig); + public static partial nint AllocateDeviceConfig(Capability capabilityType, uint sampleRate, + AudioCallback dataCallback, nint pSfConfig); +#endif #endregion #region Utils +#if BROWSER + [DllImport(LibraryName, EntryPoint = "sf_free")] + public static extern void Free(nint ptr); +#else [LibraryImport(LibraryName, EntryPoint = "sf_free")] public static partial void Free(nint ptr); - +#endif + +#if BROWSER + [DllImport(LibraryName, EntryPoint = "sf_free_device_infos")] + public static extern void FreeDeviceInfos(nint deviceInfos, uint count); +#else [LibraryImport(LibraryName, EntryPoint = "sf_free_device_infos")] public static partial void FreeDeviceInfos(nint deviceInfos, uint count); +#endif #endregion } \ No newline at end of file diff --git a/Src/Components/Recorder.cs b/Src/Components/Recorder.cs index 1e48e264..87e15e31 100644 --- a/Src/Components/Recorder.cs +++ b/Src/Components/Recorder.cs @@ -60,30 +60,31 @@ public class Recorder : IDisposable /// This is used when recording directly to memory or for custom processing, instead of to a file. /// public AudioProcessCallback? ProcessCallback; - + /// /// Gets or sets the configuration for digitally signing the recorded file. /// If set, a detached signature file (.sig) will be generated upon stopping the recording. /// Only applies when recording to a file. /// public SignatureConfiguration? SigningConfiguration { get; set; } - + private readonly AudioCaptureDevice _captureDevice; private ISoundEncoder? _encoder; private readonly List _modifiers = []; private readonly List _analyzers = []; private readonly AudioEngine _engine; private readonly AudioFormat _format; - + private SoundTags? _tagsToWrite; - + /// /// Initializes a new instance of the class to record audio to a file. /// /// The capture device to record from. /// The final destination path for the recorded audio file. /// The string identifier for the desired encoding format (e.g., "wav", "flac"). Defaults to "wav". - public Recorder(AudioCaptureDevice captureDevice, string filePath, string formatId = "wav") : this(captureDevice, new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None), formatId) + public Recorder(AudioCaptureDevice captureDevice, string filePath, string formatId = "wav") : this(captureDevice, + new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None), formatId) { FilePath = filePath; } @@ -101,7 +102,7 @@ public Recorder(AudioCaptureDevice captureDevice, Stream stream, string formatId { throw new ArgumentException("The provided stream is not writable.", nameof(stream)); } - + _captureDevice = captureDevice; _engine = captureDevice.Engine; SampleFormat = captureDevice.Format.Format; @@ -128,7 +129,7 @@ public Recorder(AudioCaptureDevice captureDevice, AudioProcessCallback callback) FormatId = string.Empty; // No encoding format needed for callback mode. _format = captureDevice.Format; } - + /// /// Gets a read-only list of components applied to the recorder. /// @@ -215,7 +216,7 @@ public async Task StopRecordingAsync() _encoder?.Dispose(); _encoder = null; State = PlaybackState.Stopped; - + try { await Stream.DisposeAsync(); @@ -243,6 +244,7 @@ public async Task StopRecordingAsync() } // 2. Sign File (Authentic Recording) +#if !BROWSER if (SigningConfiguration != null) { var signResult = await FileAuthenticator.SignFileAsync(FilePath, SigningConfiguration); @@ -265,16 +267,17 @@ public async Task StopRecordingAsync() return new IoError($"writing signature file to '{sigPath}'", ex); } } +#endif } } finally { _tagsToWrite = null; } - + return Result.Ok(); } - + /// /// Synchronously stops the recording process. /// @@ -340,7 +343,7 @@ private void OnAudioProcessed(Span samples, Capability capability) // Process analyzers foreach (var analyzer in _analyzers) { - analyzer.Process(samples, Channels); + analyzer.Process(samples, Channels); } // Pass samples diff --git a/Src/Editing/CompositionRenderer.cs b/Src/Editing/CompositionRenderer.cs index d8c5e644..0e7e9fed 100644 --- a/Src/Editing/CompositionRenderer.cs +++ b/Src/Editing/CompositionRenderer.cs @@ -305,6 +305,12 @@ private List GetActiveMidiTracksForRendering() /// public event EventHandler? PositionChanged; + /// + public Task ReadBytesAsync(Span buffer) + { + throw new NotImplementedException(); + } + /// public int ReadBytes(Span buffer) { diff --git a/Src/Editing/Persistence/CompositionProjectManager.cs b/Src/Editing/Persistence/CompositionProjectManager.cs index 2e31e924..b5bf86b9 100644 --- a/Src/Editing/Persistence/CompositionProjectManager.cs +++ b/Src/Editing/Persistence/CompositionProjectManager.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Reflection; +using System.Runtime.Versioning; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization.Metadata; @@ -63,7 +64,7 @@ public static async Task SaveProjectAsync( ? JsonTypeInfoResolver.Combine(SoundFlowJsonContext.Default, customTypeResolver) : SoundFlowJsonContext.Default }; - + var projectData = new ProjectData { ProjectFileVersion = options.ProjectFileVersion, @@ -76,7 +77,8 @@ public static async Task SaveProjectAsync( { Time = m.Time, BeatsPerMinute = m.BeatsPerMinute }).ToList(), Modifiers = SerializeEffects(composition.Modifiers, jsonOptions), Analyzers = SerializeEffects(composition.Analyzers, jsonOptions), - MidiTargets = SerializeEffects(composition.MidiTargets.OfType().Select(n => n.Target), jsonOptions), + MidiTargets = SerializeEffects(composition.MidiTargets.OfType().Select(n => n.Target), + jsonOptions), MidiMappings = SerializeMappings(composition.MappingManager.Mappings) }; @@ -200,6 +202,7 @@ public static async Task SaveProjectAsync( await File.WriteAllTextAsync(projectFilePath, json); // Signing Integration +#if !BROWSER if (options.SigningConfiguration != null) { var sigResult = await FileAuthenticator.SignFileAsync(projectFilePath, options.SigningConfiguration); @@ -213,6 +216,7 @@ public static async Task SaveProjectAsync( Log.Warning($"Failed to sign project file: {sigResult.Error?.Message}"); } } +#endif composition.ClearDirtyFlag(); } @@ -225,7 +229,9 @@ public static async Task SaveProjectAsync( /// The path to the signature file (.sig). If null, defaults to projectFilePath + ".sig". /// The signature configuration containing the Public Key. /// A result containing true if the project is valid and authentic; otherwise, false or an error. - public static async Task> VerifyProjectAsync(string projectFilePath, string? signatureFilePath, SignatureConfiguration config) + [UnsupportedOSPlatform("browser")] + public static async Task> VerifyProjectAsync(string projectFilePath, string? signatureFilePath, + SignatureConfiguration config) { if (!File.Exists(projectFilePath)) return new NotFoundError("File", $"Project file not found: {projectFilePath}"); @@ -435,8 +441,8 @@ private static byte[] CreateEmptyWavHeader(int sampleRate, int channels, SampleF /// A tuple containing the loaded Composition and a list of missing/unresolved source references. public static async Task<(Composition Composition, List UnresolvedSources)> LoadProjectAsync( - AudioEngine engine, - AudioFormat format, + AudioEngine engine, + AudioFormat format, string projectFilePath, IJsonTypeInfoResolver? customTypeResolver = null) { @@ -474,10 +480,13 @@ private static byte[] CreateEmptyWavHeader(int sampleRate, int channels, SampleF }; composition.TempoTrack.Clear(); composition.TempoTrack.AddRange(projectData.TempoTrack.Select(m => new TempoMarker(m.Time, m.BeatsPerMinute))); - composition.Modifiers.AddRange(DeserializeEffects(format, projectData.Modifiers, composition, jsonOptions)); - composition.Analyzers.AddRange(DeserializeEffects(format, projectData.Analyzers, composition, jsonOptions)); - - var deserializedMidiControllables = DeserializeEffects(format, projectData.MidiTargets, composition, jsonOptions); + composition.Modifiers.AddRange( + DeserializeEffects(format, projectData.Modifiers, composition, jsonOptions)); + composition.Analyzers.AddRange( + DeserializeEffects(format, projectData.Analyzers, composition, jsonOptions)); + + var deserializedMidiControllables = + DeserializeEffects(format, projectData.MidiTargets, composition, jsonOptions); composition.MidiTargets.AddRange(deserializedMidiControllables.Select(c => new MidiTargetNode(c))); @@ -579,9 +588,11 @@ private static byte[] CreateEmptyWavHeader(int sampleRate, int channels, SampleF FadeOutCurve = projectSegment.Settings.FadeOutCurve, }; segmentSettings.Modifiers.AddRange( - DeserializeEffects(format, projectSegment.Settings.Modifiers, composition, jsonOptions)); + DeserializeEffects(format, projectSegment.Settings.Modifiers, composition, + jsonOptions)); segmentSettings.Analyzers.AddRange( - DeserializeEffects(format, projectSegment.Settings.Analyzers, composition, jsonOptions)); + DeserializeEffects(format, projectSegment.Settings.Analyzers, composition, + jsonOptions)); var segment = new AudioSegment( format, @@ -649,19 +660,23 @@ private static byte[] CreateEmptyWavHeader(int sampleRate, int channels, SampleF if (midiTrack == null) continue; // Try to find the target in the internal list first. - var targetNode = composition.MidiTargets.FirstOrDefault(t => t.Name == projectMidiTrack.TargetComponentName); + var targetNode = + composition.MidiTargets.FirstOrDefault(t => t.Name == projectMidiTrack.TargetComponentName); // If not found, it might be a physical output device. if (targetNode == null) { - var outputDevice = engine.MidiOutputDevices.FirstOrDefault(d => d.Name == projectMidiTrack.TargetComponentName); + var outputDevice = + engine.MidiOutputDevices.FirstOrDefault(d => d.Name == projectMidiTrack.TargetComponentName); if (outputDevice.Name != null) { // This is complex. The MidiManager owns device nodes. We need a way to request one. // For now, this part of linking is a known limitation if not an internal target. - Log.Warning($"Could not find MIDI output device '{projectMidiTrack.TargetComponentName}' to link to track '{midiTrack.Name}'."); + Log.Warning( + $"Could not find MIDI output device '{projectMidiTrack.TargetComponentName}' to link to track '{midiTrack.Name}'."); } } + midiTrack.Target = targetNode; } @@ -824,7 +839,8 @@ public static bool RelinkMissingMedia( #endregion // Helper method to serialize modifiers/analyzers - private static List SerializeEffects(IEnumerable effects, JsonSerializerOptions jsonOptions) where T : class + private static List SerializeEffects(IEnumerable effects, + JsonSerializerOptions jsonOptions) where T : class { var effectDataList = new List(); foreach (var effect in effects) @@ -853,7 +869,8 @@ private static List SerializeEffects(IEnumerable effec if (value != null) { var propTypeInfo = jsonOptions.GetTypeInfo(prop.PropertyType); - parameters[prop.Name] = JsonValue.Create(JsonSerializer.SerializeToElement(value, propTypeInfo)); + parameters[prop.Name] = + JsonValue.Create(JsonSerializer.SerializeToElement(value, propTypeInfo)); } } catch (Exception ex) @@ -897,7 +914,8 @@ private static List DeserializeEffects(AudioFormat format, List public interface ISoundDataProvider : IDisposable { + + /// + /// + /// + [UnsupportedOSPlatform("browser")] + public void Initialize(); + + /// + /// + /// + public void InitializeAsync(); + /// /// Gets the current playback position in samples. /// @@ -53,7 +66,18 @@ public interface ISoundDataProvider : IDisposable /// A task representing the asynchronous read operation. The task result contains the number of bytes actually /// read. May be less than the requested number if the end of the data is reached. /// + [UnsupportedOSPlatform("browser")] int ReadBytes(Span buffer); + + /// + /// Reads the specified number of audio bytes into the given buffer asynchronously. + /// + /// The buffer to write the bytes to. + /// + /// A task representing the asynchronous read operation. The task result contains the number of bytes actually + /// read. May be less than the requested number if the end of the data is reached. + /// + Task ReadBytesAsync(Span buffer); /// /// Sets the playback position to the specified sample offset. diff --git a/Src/Providers/AssetDataProvider.cs b/Src/Providers/AssetDataProvider.cs index 0f976006..fd0751ce 100644 --- a/Src/Providers/AssetDataProvider.cs +++ b/Src/Providers/AssetDataProvider.cs @@ -163,6 +163,12 @@ private void Initialize(AudioEngine engine, Stream stream, ReadOptions options, /// public event EventHandler? PositionChanged; + /// + public Task ReadBytesAsync(Span buffer) + { + throw new NotImplementedException(); + } + /// public int ReadBytes(Span buffer) { diff --git a/Src/Providers/ChunkedDataProvider.cs b/Src/Providers/ChunkedDataProvider.cs index afaa7cc3..3930b5cb 100644 --- a/Src/Providers/ChunkedDataProvider.cs +++ b/Src/Providers/ChunkedDataProvider.cs @@ -167,6 +167,12 @@ public int Position /// public event EventHandler? PositionChanged; + /// + public Task ReadBytesAsync(Span buffer) + { + throw new NotImplementedException(); + } + /// public int ReadBytes(Span buffer) { diff --git a/Src/Providers/MicrophoneDataProvider.cs b/Src/Providers/MicrophoneDataProvider.cs index 24ef692a..91fe7c7b 100644 --- a/Src/Providers/MicrophoneDataProvider.cs +++ b/Src/Providers/MicrophoneDataProvider.cs @@ -135,6 +135,12 @@ private void OnAudioDataReceived(Span samples, Capability capability) } } } + + /// + public Task ReadBytesAsync(Span buffer) + { + throw new NotImplementedException(); + } /// public int ReadBytes(Span buffer) diff --git a/Src/Providers/NetworkDataProvider.cs b/Src/Providers/NetworkDataProvider.cs index 4e184a8b..4d262459 100644 --- a/Src/Providers/NetworkDataProvider.cs +++ b/Src/Providers/NetworkDataProvider.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Net; using System.Net.Http.Headers; +using System.Runtime.Versioning; using System.Text; using SoundFlow.Abstracts; using SoundFlow.Enums; @@ -119,6 +120,12 @@ private async Task InitializeInternalAsync(AudioEngine engine, AudioFormat? form } } + /// + public Task ReadBytesAsync(Span buffer) + { + throw new NotImplementedException(); + } + /// public int ReadBytes(Span buffer) { @@ -233,6 +240,8 @@ internal abstract class NetworkDataProviderBase(AudioEngine engine, AudioFormat? public abstract Task InitializeAsync(); + public abstract Task ReadBytesAsync(Span buffer); + [UnsupportedOSPlatform("browser")] public abstract int ReadBytes(Span buffer); public abstract void Seek(int sampleOffset); @@ -297,7 +306,7 @@ public override async Task InitializeAsync() _stream = bufferedStream; } - var formatInfoResult = SoundMetadataReader.Read(_stream, readOptions); + var formatInfoResult = await SoundMetadataReader.ReadAsync(_stream, readOptions); if (formatInfoResult is { IsSuccess: true, Value: not null }) { @@ -334,6 +343,11 @@ public override async Task InitializeAsync() CanSeek = _stream.CanSeek; } + public override Task ReadBytesAsync(Span buffer) + { + throw new NotImplementedException(); + } + public override int ReadBytes(Span buffer) { if (IsDisposed || _decoder == null) return 0; @@ -420,8 +434,12 @@ public override async Task InitializeAsync() // Start background buffering _ = BufferHlsStreamAsync(_cancellationTokenSource.Token); } - - + + public override Task ReadBytesAsync(Span buffer) + { + throw new NotImplementedException(); + } + private async Task DetermineSegmentFormatAsync(CancellationToken cancellationToken) { var firstSegmentUrl = _hlsSegments[0].Uri; @@ -728,7 +746,7 @@ public void StartProducerTask(HttpResponseMessage sourceResponse) var bytesRead = await sourceStream.ReadAsync(tempBuffer, _cts.Token); if (bytesRead == 0) break; // End of network stream - Write(tempBuffer, 0, bytesRead); + await WriteAsync(tempBuffer, 0, bytesRead); } if (!_cts.IsCancellationRequested) SignalCompletion(); @@ -746,6 +764,34 @@ public void StartProducerTask(HttpResponseMessage sourceResponse) }); } + // TODO: Add semaphore instead of lock + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + while (_bytesAvailable == 0 && _state == DownloadState.Buffering && !_isDisposed) + await Task.Delay(10, cancellationToken); + + if (_bytesAvailable == 0) return 0; + + var bytesToRead = Math.Min(count, _bytesAvailable); + + // Read from circular buffer + var firstChunkSize = Math.Min(bytesToRead, _buffer.Length - _readPosition); + Buffer.BlockCopy(_buffer, _readPosition, buffer, offset, firstChunkSize); + _readPosition = (_readPosition + firstChunkSize) % _buffer.Length; + + if (firstChunkSize < bytesToRead) + { + var secondChunkSize = bytesToRead - firstChunkSize; + Buffer.BlockCopy(_buffer, _readPosition, buffer, offset + firstChunkSize, secondChunkSize); + _readPosition = (_readPosition + secondChunkSize) % _buffer.Length; + } + + _bytesAvailable -= bytesToRead; + Monitor.PulseAll(_lock); // Signal producer that space is available + return bytesToRead; + } + + [UnsupportedOSPlatform("browser")] public override int Read(byte[] buffer, int offset, int count) { lock (_lock) @@ -772,11 +818,32 @@ public override int Read(byte[] buffer, int offset, int count) } _bytesAvailable -= bytesToRead; - Monitor.PulseAll(_lock); // Signal producer that space is available return bytesToRead; } } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (count == 0) return Task.CompletedTask; + if (_isDisposed) return Task.CompletedTask; + + // Write to circular buffer + var firstChunkSize = Math.Min(count, _buffer.Length - _writePosition); + Buffer.BlockCopy(buffer, offset, _buffer, _writePosition, firstChunkSize); + _writePosition = (_writePosition + firstChunkSize) % _buffer.Length; + + if (firstChunkSize < count) + { + var secondChunkSize = count - firstChunkSize; + Buffer.BlockCopy(buffer, offset + firstChunkSize, _buffer, _writePosition, secondChunkSize); + _writePosition = (_writePosition + secondChunkSize) % _buffer.Length; + } + + _bytesAvailable += count; + return Task.CompletedTask; + } + + [UnsupportedOSPlatform("browser")] public override void Write(byte[] buffer, int offset, int count) { if (count == 0) return; diff --git a/Src/Providers/QueueDataProvider.cs b/Src/Providers/QueueDataProvider.cs index 5cb28b07..b7dbc6ab 100644 --- a/Src/Providers/QueueDataProvider.cs +++ b/Src/Providers/QueueDataProvider.cs @@ -54,14 +54,17 @@ public class QueueDataProvider : ISoundDataProvider /// This parameter is ignored if is null. /// /// Thrown if sampleRate is not positive. - public QueueDataProvider(AudioFormat format, int? maxSamples = null, QueueFullBehavior fullBehavior = QueueFullBehavior.Throw) + public QueueDataProvider(AudioFormat format, int? maxSamples = null, + QueueFullBehavior fullBehavior = QueueFullBehavior.Throw) { if (format.SampleRate <= 0) throw new ArgumentOutOfRangeException(nameof(format), "Sample rate must be positive."); - + if (!maxSamples.HasValue && fullBehavior != QueueFullBehavior.Throw) - throw new ArgumentException("QueueFullBehavior cannot be set to Block or Drop for a queue with no sample limit.", nameof(fullBehavior)); - + throw new ArgumentException( + "QueueFullBehavior cannot be set to Block or Drop for a queue with no sample limit.", + nameof(fullBehavior)); + SampleRate = format.SampleRate; SampleFormat = format.Format; _maxSamples = maxSamples; @@ -69,7 +72,7 @@ public QueueDataProvider(AudioFormat format, int? maxSamples = null, QueueFullBe } #region Properties - + /// public int Position { get; private set; } @@ -104,18 +107,18 @@ public int SamplesAvailable } } } - + /// /// Gets the total number of samples enqueued so far. /// public long TotalSamplesEnqueued { - get - { + get + { lock (_lock) { return _totalSamplesEnqueued; - } + } } } @@ -151,27 +154,34 @@ public void AddSamples(ReadOnlySpan samples) switch (_fullBehavior) { case QueueFullBehavior.Throw: - throw new InvalidOperationException("Adding these samples would exceed the maximum size of the queue."); + throw new InvalidOperationException( + "Adding these samples would exceed the maximum size of the queue."); case QueueFullBehavior.Drop: return; // Silently drop the samples and return. case QueueFullBehavior.Block: +#if BROWSER + throw new NotSupportedException("QueueDataProvider does not support blocking in the browser."); +#else // Block until space is available for the entire sample block. while (!IsDisposed && _sampleQueue.Count + samples.Length > _maxSamples.Value) { Monitor.Wait(_lock); } + // Re-check disposed status after waking up. ObjectDisposedException.ThrowIf(IsDisposed, this); break; +#endif } } - + foreach (var sample in samples) { _sampleQueue.Enqueue(sample); } + _totalSamplesEnqueued += samples.Length; } } @@ -180,7 +190,7 @@ public void AddSamples(ReadOnlySpan samples) public int ReadBytes(Span buffer) { if (IsDisposed || buffer.IsEmpty) return 0; - + var samplesRead = 0; var shouldFireEndOfStream = false; var spaceWasFreed = false; @@ -211,7 +221,7 @@ public int ReadBytes(Span buffer) shouldFireEndOfStream = true; _endOfStreamFired = true; } - + // If space was freed, notify any waiting producer threads. if (spaceWasFreed) { @@ -226,6 +236,12 @@ public int ReadBytes(Span buffer) return samplesRead; } + // + public Task ReadBytesAsync(Span buffer) + { + throw new NotImplementedException(); + } + /// /// Resets the provider to its initial state, clearing the sample queue and resetting the position. /// This allows the instance to be reused. Any threads blocked in will be unblocked. @@ -241,7 +257,7 @@ public void Reset() _totalSamplesEnqueued = 0; _isAddingCompleted = false; _endOfStreamFired = false; - + // Wake up any threads that were blocked, as the queue is now empty. Monitor.PulseAll(_lock); } @@ -261,17 +277,18 @@ public void CompleteAdding() } /// - public void Seek(int offset) => throw new InvalidOperationException("Seeking is not supported by the QueueDataProvider."); + public void Seek(int offset) => + throw new InvalidOperationException("Seeking is not supported by the QueueDataProvider."); /// public void Dispose() { if (IsDisposed) return; - + lock (_lock) { if (IsDisposed) return; - + IsDisposed = true; _sampleQueue.Clear(); diff --git a/Src/Providers/RawDataProvider.cs b/Src/Providers/RawDataProvider.cs index 71305b5f..f6274bb1 100644 --- a/Src/Providers/RawDataProvider.cs +++ b/Src/Providers/RawDataProvider.cs @@ -139,6 +139,12 @@ public int ReadBytes(Span buffer) return samplesActuallyRead; } + // + public Task ReadBytesAsync(Span buffer) + { + throw new NotImplementedException(); + } + /// /// Thrown if the provider has been disposed. /// Thrown if seeking is not supported on the underlying PCM stream. diff --git a/Src/Providers/StreamDataProvider.cs b/Src/Providers/StreamDataProvider.cs index 2108ac45..948ff99f 100644 --- a/Src/Providers/StreamDataProvider.cs +++ b/Src/Providers/StreamDataProvider.cs @@ -25,7 +25,7 @@ public sealed class StreamDataProvider : ISoundDataProvider /// Optional configuration for metadata reading. public StreamDataProvider(AudioEngine engine, Stream stream, ReadOptions? options = null) { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + ArgumentNullException.ThrowIfNull(stream); options ??= new ReadOptions(); var formatInfoResult = SoundMetadataReader.Read(_stream, options); @@ -76,7 +76,7 @@ public StreamDataProvider(AudioEngine engine, Stream stream, ReadOptions? option /// The stream to read audio data from. public StreamDataProvider(AudioEngine engine, AudioFormat format, Stream stream) { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + ArgumentNullException.ThrowIfNull(stream); var formatInfoResult = SoundMetadataReader.Read(_stream, new ReadOptions { @@ -145,6 +145,12 @@ public StreamDataProvider(AudioEngine engine, AudioFormat format, Stream stream) /// public event EventHandler? PositionChanged; + /// + public Task ReadBytesAsync(Span buffer) + { + throw new NotImplementedException(); + } + /// public int ReadBytes(Span buffer) { diff --git a/Src/Security/AudioEncryptor.cs b/Src/Security/AudioEncryptor.cs index 6429ee9b..12f5cc1b 100644 --- a/Src/Security/AudioEncryptor.cs +++ b/Src/Security/AudioEncryptor.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using SoundFlow.Interfaces; @@ -15,6 +16,7 @@ namespace SoundFlow.Security; /// /// Provides high-level methods to encrypt and decrypt audio using the format. /// +[UnsupportedOSPlatform("browser")] public static class AudioEncryptor { private const int BufferSize = 8192; // 8KB buffer diff --git a/Src/Security/Configuration/SignatureConfiguration.cs b/Src/Security/Configuration/SignatureConfiguration.cs index a7dde552..611b5954 100644 --- a/Src/Security/Configuration/SignatureConfiguration.cs +++ b/Src/Security/Configuration/SignatureConfiguration.cs @@ -1,10 +1,12 @@ -using SoundFlow.Security.Utils; +using System.Runtime.Versioning; +using SoundFlow.Security.Utils; namespace SoundFlow.Security.Configuration; /// /// Configuration settings for digital signature operations (signing and verification). /// +[UnsupportedOSPlatform("browser")] public class SignatureConfiguration { /// diff --git a/Src/Security/FileAuthenticator.cs b/Src/Security/FileAuthenticator.cs index d9893f12..e7d2a934 100644 --- a/Src/Security/FileAuthenticator.cs +++ b/Src/Security/FileAuthenticator.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System.Runtime.Versioning; +using System.Security.Cryptography; using SoundFlow.Security.Configuration; using SoundFlow.Structs; @@ -8,6 +9,7 @@ namespace SoundFlow.Security; /// Provides methods to sign and verify files using ECDSA Digital Signatures. /// This ensures file authenticity and integrity at the binary container level. /// +[UnsupportedOSPlatform("browser")] public static class FileAuthenticator { private const int BufferSize = 8192; diff --git a/Src/Security/Modifiers/StreamEncryptionModifier.cs b/Src/Security/Modifiers/StreamEncryptionModifier.cs index ef2a6e94..05a680af 100644 --- a/Src/Security/Modifiers/StreamEncryptionModifier.cs +++ b/Src/Security/Modifiers/StreamEncryptionModifier.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; +using System.Runtime.Versioning; using System.Security.Cryptography; using SoundFlow.Abstracts; using SoundFlow.Security.Configuration; @@ -21,6 +22,7 @@ namespace SoundFlow.Security.Modifiers; /// for maintaining the sample-count synchronization of the audio engine. /// /// +[UnsupportedOSPlatform("browser")] public sealed class StreamEncryptionModifier : SoundModifier, IDisposable { private readonly Aes _aes; @@ -45,7 +47,7 @@ public StreamEncryptionModifier(EncryptionConfiguration config) if (config.Iv.Length != 12 && config.Iv.Length != 16) throw new ArgumentException("AES-CTR recommends a 12-byte nonce or 16-byte IV.", nameof(config)); - + _aes = Aes.Create(); _aes.KeySize = 256; _aes.Key = config.Key; diff --git a/Src/Security/Utils/SignatureKeyGenerator.cs b/Src/Security/Utils/SignatureKeyGenerator.cs index 8c459d3d..76a734f0 100644 --- a/Src/Security/Utils/SignatureKeyGenerator.cs +++ b/Src/Security/Utils/SignatureKeyGenerator.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System.Runtime.Versioning; +using System.Security.Cryptography; using SoundFlow.Security.Configuration; namespace SoundFlow.Security.Utils; @@ -7,6 +8,7 @@ namespace SoundFlow.Security.Utils; /// /// Utility for generating secure ECDSA key pairs for file signing. /// +[UnsupportedOSPlatform("browser")] public static class SignatureKeyGenerator { /// diff --git a/Src/SoundFlow.csproj b/Src/SoundFlow.csproj index e1a6cfcf..6568b81f 100644 --- a/Src/SoundFlow.csproj +++ b/Src/SoundFlow.csproj @@ -1,7 +1,7 @@  - net8.0;net8.0-browser + net10.0;net10.0-browser enable enable true @@ -43,5 +43,10 @@ + + + + \ No newline at end of file diff --git a/global.json b/global.json index dad2db5e..a11f48e1 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.0", + "version": "10.0.0", "rollForward": "latestMajor", "allowPrerelease": true }