소스 검색

Host: HTTP Range fully implement; Blazor: Use CsCore instead NAudio for broken FLAC decoder

HOME 2 년 전
부모
커밋
ba40721441

+ 1 - 0
FNZCM/FNZCM.BlazorWasm/FNZCM.BlazorWasm.csproj

@@ -15,6 +15,7 @@
     <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
     <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
     <PackageReference Include="NAudio.Core" Version="2.1.0" />
     <PackageReference Include="NAudio.Core" Version="2.1.0" />
     <PackageReference Include="BunLabs.NAudio.Flac" Version="2.0.0" />
     <PackageReference Include="BunLabs.NAudio.Flac" Version="2.0.0" />
+    <PackageReference Include="CSCore" Version="1.2.1.2" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 3 - 3
FNZCM/FNZCM.BlazorWasm/UI/Views/Default/Dialogs/DiscDialogTrackSetTable.razor

@@ -28,9 +28,9 @@
                         <FileIcon Track="@t.item"></FileIcon>
                         <FileIcon Track="@t.item"></FileIcon>
                     </td>
                     </td>
                     <td scope="row" class="p-0 text-nowrap">
                     <td scope="row" class="p-0 text-nowrap">
-                        <span onclick="@(async () => { await WavePlayerModule.InitFlacAsync(t.item.Path); })">LP</span>
-                        @*<span onclick="@WavePlayerModule.Play">P</span>*@
-                        <span onclick="@WavePlayerModule.Stop">S</span>
+                        <span onclick="@(async () => { await WavePlayerModuleCsCore.InitFlacAsync(t.item.Path); })">LP</span>
+                        <span onclick="@WavePlayerModuleCsCore.Play">P</span>
+                        <span onclick="@WavePlayerModuleCsCore.Stop">S</span>
                         <a href="@t.item?.Path" target="@FnzConst.PlayPageTarget">@t.item?.GetTitleOrFilename()</a>
                         <a href="@t.item?.Path" target="@FnzConst.PlayPageTarget">@t.item?.GetTitleOrFilename()</a>
                     </td>
                     </td>
                     <td class="p-0 text-center">@(t.item?.Tag?.Duration.SecondToDur() ?? "?")</td>
                     <td class="p-0 text-center">@(t.item?.Tag?.Duration.SecondToDur() ?? "?")</td>

+ 120 - 0
FNZCM/FNZCM.BlazorWasm/Utility/WavePlayerModuleCsCore.cs

@@ -0,0 +1,120 @@
+using CSCore;
+using System.Runtime.InteropServices.JavaScript;
+using System.Runtime.Versioning;
+using CSCore.Codecs.FLAC;
+using Newtonsoft.Json;
+using Microsoft.AspNetCore.Components.WebAssembly.Http;
+
+namespace FNZCM.BlazorWasm.Utility
+{
+    [SupportedOSPlatform("browser")]
+    public static partial class WavePlayerModuleCsCore
+    {
+        public static int MsPerChunk { get; set; } = 150;
+        public static bool IsInit { get; private set; }
+
+        private static IWaveSource _waveSource;
+        private static ISampleSource _sampleSource;
+
+        public static async Task InitFlacAsync(string url)
+        {
+            Console.WriteLine("create wave source with uri");
+            Console.WriteLine("create http client");
+            var client = new HttpClient();
+            Console.WriteLine("build request message");
+            var req = new HttpRequestMessage(HttpMethod.Get, url);
+            req.SetBrowserResponseStreamingEnabled(true);
+            Console.WriteLine("send request");
+            var rsp = await client.SendAsync(req);
+            if (!rsp.IsSuccessStatusCode)
+            {
+                Console.WriteLine($"fail, {rsp.StatusCode}({(int)rsp.StatusCode}) , {rsp.ReasonPhrase}");
+                return;
+            }
+
+            Console.WriteLine("get stream");
+            var flacStream = await rsp.Content.ReadAsStreamAsync();
+            _waveSource = new FlacFile(flacStream);
+            
+            Console.WriteLine("create sample source");
+            _sampleSource = _waveSource.ToSampleSource();
+            Console.WriteLine("format:" + _sampleSource.WaveFormat);
+            Console.WriteLine("json:" + JsonConvert.SerializeObject(_sampleSource.WaveFormat));
+            Console.WriteLine("init wave player");
+            await InitAsync(_sampleSource);
+            Play();
+        }
+
+        public static async Task InitAsync(ISampleSource sampleSource)
+        {
+            if (!IsInit)
+            {
+                await JSHost.ImportAsync("WavePlayer", "/lib/fnz/fnz-wave-player-module.js");
+                IsInit = true;
+            }
+
+
+            _sampleSource = sampleSource;
+            JsInit(_sampleSource.WaveFormat.Channels, _sampleSource.WaveFormat.SampleRate,nameof(WavePlayerModuleCsCore));
+        }
+
+        [JSImport("play", "WavePlayer")]
+        public static partial void Play();
+
+        [JSImport("stop", "WavePlayer")]
+        public static partial void Stop();
+
+        [JSExport]
+        public static double[] TakeChunk()
+        {
+            var sampleCountPerChannel = (int)(_sampleSource.WaveFormat.SampleRate * MsPerChunk / 1000f);
+
+            float[] sampleBuffer;
+            {
+                var remainSamples = sampleCountPerChannel * _sampleSource.WaveFormat.Channels;
+                sampleBuffer = new float[remainSamples];
+                var readIndex = 0;
+                while (remainSamples > 0)
+                {
+                    var r = _sampleSource.Read(sampleBuffer, readIndex, sampleBuffer.Length - readIndex);
+                    if (r == 0)
+                    {
+                        Array.Resize(ref sampleBuffer, readIndex);
+                        break;
+                    }
+                    remainSamples -= r;
+                    readIndex += r;
+                }
+            }
+
+            //de-multiplex
+            //re-order buffer
+            //    0 1 2 3 4 5
+            //    L R L R L R
+            // → L L L R R R
+            //    0 2 4 1 3 5
+            //convert float to double for marshal
+
+            var marshalBuf = new double[sampleBuffer.Length];
+            var sampleIndex = 0;
+            for (var i = 0; i < marshalBuf.Length; i += _sampleSource.WaveFormat.Channels)
+            {
+                for (var c = 0; c < _sampleSource.WaveFormat.Channels; c++)
+                {
+                    var srcIdx = i + c;
+                    var dstIdx = sampleCountPerChannel * c + sampleIndex;
+                    marshalBuf[dstIdx] = sampleBuffer[srcIdx];
+                }
+                sampleIndex++;
+            }
+
+            return marshalBuf;
+        }
+
+        [JSImport("init", "WavePlayer")]
+        private static partial void JsInit(int channels, int sampleRate,string className);
+
+        [JSImport("feedChunk", "WavePlayer")]
+        private static partial void JsFeedChunk(double[] channel0, double[] channel2);
+    }
+}

+ 40 - 24
FNZCM/FNZCM.BlazorWasm/Utility/WavePlayerModule.cs

@@ -3,18 +3,19 @@ using NAudio.Flac;
 using NAudio.Wave;
 using NAudio.Wave;
 using System.Runtime.InteropServices.JavaScript;
 using System.Runtime.InteropServices.JavaScript;
 using System.Runtime.Versioning;
 using System.Runtime.Versioning;
+using Newtonsoft.Json;
 
 
 namespace FNZCM.BlazorWasm.Utility
 namespace FNZCM.BlazorWasm.Utility
 {
 {
     [SupportedOSPlatform("browser")]
     [SupportedOSPlatform("browser")]
-    public static partial class WavePlayerModule
+    public static partial class WavePlayerModuleNAudio
     {
     {
         public static bool IsInit { get; private set; }
         public static bool IsInit { get; private set; }
 
 
         private static IWaveProvider _provider;
         private static IWaveProvider _provider;
         private static ISampleProvider _sampleProvider;
         private static ISampleProvider _sampleProvider;
         public static WaveFormat OutputWaveFormat { get; private set; }
         public static WaveFormat OutputWaveFormat { get; private set; }
-        public static int MsPerChunk { get; set; } = 200;
+        public static int MsPerChunk { get; set; } = 150;
 
 
         public static async Task InitFlacAsync(string url)
         public static async Task InitFlacAsync(string url)
         {
         {
@@ -35,8 +36,12 @@ namespace FNZCM.BlazorWasm.Utility
             var flacStream = await rsp.Content.ReadAsStreamAsync();
             var flacStream = await rsp.Content.ReadAsStreamAsync();
             Console.WriteLine("create flac reader");
             Console.WriteLine("create flac reader");
             var reader = new FlacReader(flacStream);
             var reader = new FlacReader(flacStream);
+            Console.WriteLine("format:" + reader.WaveFormat);
+            Console.WriteLine("json:" + JsonConvert.SerializeObject(reader.WaveFormat));
+            Console.WriteLine("create lockAlignReductionStream");
+            var alignReader = new BlockAlignReductionStream(reader);
             Console.WriteLine("init wave player");
             Console.WriteLine("init wave player");
-            await InitAsync(reader);
+            await InitAsync(alignReader);
             Play();
             Play();
         }
         }
 
 
@@ -61,39 +66,50 @@ namespace FNZCM.BlazorWasm.Utility
         public static partial void Stop();
         public static partial void Stop();
 
 
         [JSExport]
         [JSExport]
-        public static bool TakeChunk()
+        public static double[] TakeChunk()
         {
         {
             var sampleCountPerChannel = (int)(OutputWaveFormat.SampleRate * MsPerChunk / 1000f);
             var sampleCountPerChannel = (int)(OutputWaveFormat.SampleRate * MsPerChunk / 1000f);
-            var sampleCount = sampleCountPerChannel * OutputWaveFormat.Channels;
-            var floats = new float[sampleCount];
-            var r = _sampleProvider.Read(floats, 0, floats.Length);
 
 
-            Array.Resize(ref floats, r);
-
-            var channels = new double[OutputWaveFormat.Channels][];
-            for (var i = 0; i < OutputWaveFormat.Channels; i++)
+            float[] sampleBuffer;
             {
             {
-                channels[i] = new double[sampleCountPerChannel];
+                var remainSamples = sampleCountPerChannel * OutputWaveFormat.Channels;
+                sampleBuffer = new float[remainSamples];
+                var readIndex = 0;
+                while (remainSamples > 0)
+                {
+                    var r = _sampleProvider.Read(sampleBuffer, readIndex, sampleBuffer.Length - readIndex);
+                    if (r == 0)
+                    {
+                        Array.Resize(ref sampleBuffer, readIndex);
+                        break;
+                    }
+                    remainSamples -= r;
+                    readIndex += r;
+                }
             }
             }
 
 
             //de-multiplex
             //de-multiplex
-            var ch = 0;
-            var index = 0;
-            foreach (var f in floats)
+            //re-order buffer
+            //    0 1 2 3 4 5
+            //    L R L R L R
+            // → L L L R R R
+            //    0 2 4 1 3 5
+            //convert float to double for marshal
+
+            var marshalBuf = new double[sampleBuffer.Length];
+            var sampleIndex = 0;
+            for (var i = 0; i < marshalBuf.Length; i += OutputWaveFormat.Channels)
             {
             {
-                channels[ch][index] = f;
-                ++ch;
-                if (ch >= OutputWaveFormat.Channels)
+                for (var c = 0; c < OutputWaveFormat.Channels; c++)
                 {
                 {
-                    ch = 0;
-                    ++index;
+                    var srcIdx = i + c;
+                    var dstIdx = sampleCountPerChannel * c + sampleIndex;
+                    marshalBuf[dstIdx] = sampleBuffer[srcIdx];
                 }
                 }
+                sampleIndex++;
             }
             }
 
 
-            //TODO: adapt dym channels
-            JsFeedChunk(channels[0], channels[1]);
-
-            return index > 0;
+            return marshalBuf;
         }
         }
 
 
         [JSImport("init", "WavePlayer")]
         [JSImport("init", "WavePlayer")]

+ 37 - 26
FNZCM/FNZCM.BlazorWasm/wwwroot/lib/fnz/fnz-wave-player-module.js

@@ -1,12 +1,18 @@
 var AudioContext = window.AudioContext || window.webkitAudioContext;
 var AudioContext = window.AudioContext || window.webkitAudioContext;
 const { getAssemblyExports } = await globalThis.getDotnetRuntime(0);
 const { getAssemblyExports } = await globalThis.getDotnetRuntime(0);
-var DotNetInterop = (await getAssemblyExports("FNZCM.BlazorWasm.dll")).FNZCM.BlazorWasm.Utility.WavePlayerModule;
+var nameSpace = (await getAssemblyExports("FNZCM.BlazorWasm.dll")).FNZCM.BlazorWasm.Utility
+var DotNetInterop = nameSpace.WavePlayerModule;
+
+const chunkQueue = 3;
 
 
 var inst;
 var inst;
 
 
-export function init(channels, sampleRate) {
+export function init(channels, sampleRate, className) {
+    if (inst && inst.ctx) {
+        inst.ctx.close();
+    }
 
 
-    if (inst && inst.ctx) inst.ctx.close();
+    if (className) DotNetInterop = nameSpace[className];
 
 
     inst = {};
     inst = {};
     inst.ctx = new AudioContext({ sampleRate: sampleRate });
     inst.ctx = new AudioContext({ sampleRate: sampleRate });
@@ -16,31 +22,30 @@ export function init(channels, sampleRate) {
     inst.isEnd = false;
     inst.isEnd = false;
 }
 }
 
 
-export function play() {
-    if (inst.isPlaying) return;
-
-    if (inst.chunks.length < 2) {
-        DotNetInterop.TakeChunk();
-        DotNetInterop.TakeChunk();
+function feedChunk() {
+    var marshalBuf = DotNetInterop.TakeChunk();
+    if (marshalBuf.length == 0) {
+        inst.isend = true;
+        return;
     }
     }
-    inst.currentSource = inst.chunks.shift();
-    inst.currentSource.start();
-    inst.isPlaying = true;
-}
 
 
-export function feedChunk(channel0, channel1) {
-    
-    //TODO: adapt dym channels
-    
-    const bufferSize = channel0.length;
-    var chunk = inst.ctx.createBuffer(inst.channels, bufferSize, inst.ctx.sampleRate);
+    var sampleCount = marshalBuf.length / inst.channels;
+
+    var audioBuffer = inst.ctx.createBuffer(inst.channels, sampleCount, inst.ctx.sampleRate);
 
 
-    chunk.copyToChannel(new Float32Array(channel0), 0, 0);
-    chunk.copyToChannel(new Float32Array(channel1), 1, 0);
+    for (var i = 0; i < inst.channels; i++) {
+        var chb = marshalBuf.slice(i * sampleCount, (i + 1) * sampleCount);
+        audioBuffer.copyToChannel(
+            new Float32Array(chb),
+            i,
+            0
+        );
+    }
 
 
     var obj = inst.ctx.createBufferSource();
     var obj = inst.ctx.createBufferSource();
-    obj.buffer = chunk;
+    obj.buffer = audioBuffer;
     obj.connect(inst.ctx.destination);
     obj.connect(inst.ctx.destination);
+    obj.loop = false;
     obj.addEventListener("ended", function () {
     obj.addEventListener("ended", function () {
         Continue(inst);
         Continue(inst);
     });
     });
@@ -48,15 +53,22 @@ export function feedChunk(channel0, channel1) {
     inst.chunks.push(obj);
     inst.chunks.push(obj);
 }
 }
 
 
+export function play() {
+    if (inst.isPlaying) return;
+
+    while (inst.chunks.length < chunkQueue && !inst.isEnd) feedChunk();
+    inst.currentSource = inst.chunks.shift();
+    inst.currentSource.start();
+    inst.isPlaying = true;
+}
+
 function Continue() {
 function Continue() {
     if (inst.isPlaying) {
     if (inst.isPlaying) {
         var chunk = inst.chunks.shift();
         var chunk = inst.chunks.shift();
         if (chunk) {
         if (chunk) {
             inst.currentSource = chunk;
             inst.currentSource = chunk;
             inst.currentSource.start();
             inst.currentSource.start();
-            if (inst.chunks.length < 2 && !inst.isEnd) {
-                inst.isEnd = !DotNetInterop.TakeChunk();
-            }
+            while (inst.chunks.length < chunkQueue && !inst.isEnd) feedChunk();
         } else {
         } else {
             inst.isPlaying = false;
             inst.isPlaying = false;
         }
         }
@@ -67,4 +79,3 @@ export function stop() {
     inst.isPlaying = false;
     inst.isPlaying = false;
     if (init.currentSource) inst.currentSource.stop();
     if (init.currentSource) inst.currentSource.stop();
 }
 }
-

+ 25 - 20
FNZCM/FNZCM.ConHost/HostProgram.cs

@@ -1159,7 +1159,6 @@ namespace FNZCM.ConHost
                             break;
                             break;
                     }
                     }
 
 
-                    var range = request.Headers.GetValues("Range");
 
 
                     FileStream fs = null;
                     FileStream fs = null;
                     try
                     try
@@ -1168,7 +1167,13 @@ namespace FNZCM.ConHost
 
 
                         context.Response.Headers.Add("Accept-Ranges", "bytes");
                         context.Response.Headers.Add("Accept-Ranges", "bytes");
 
 
-                        if (range is { Length: > 0 })
+                        var range = request.Headers.GetValues("Range");
+                        if (range == null || range.Length == 0)
+                        {
+                            context.Response.ContentLength64 = fs.Length;
+                            fs.CopyTo(context.Response.OutputStream);
+                        }
+                        else
                         {
                         {
                             var rngParts = range[0].Split(new[] { "bytes=", "-" }, StringSplitOptions.RemoveEmptyEntries);
                             var rngParts = range[0].Split(new[] { "bytes=", "-" }, StringSplitOptions.RemoveEmptyEntries);
                             if (rngParts.Length == 1 && long.TryParse(rngParts[0], out var start))
                             if (rngParts.Length == 1 && long.TryParse(rngParts[0], out var start))
@@ -1179,30 +1184,30 @@ namespace FNZCM.ConHost
                                 context.Response.ContentLength64 = fs.Length - start;
                                 context.Response.ContentLength64 = fs.Length - start;
                                 fs.CopyTo(context.Response.OutputStream);
                                 fs.CopyTo(context.Response.OutputStream);
                             }
                             }
-                            else if (rngParts.Length == 2 && long.TryParse(rngParts[0], out var start2) && long.TryParse(rngParts[1], out var end))
+                            else if (rngParts.Length == 2 && long.TryParse(rngParts[0], out var rangeStart) && long.TryParse(rngParts[1], out var rangeEnd))
                             {
                             {
-                                fs.Position = start2;
-                                var realLen = end - start2 + 1;
+                                var bodyLen = rangeEnd - rangeStart + 1;
                                 context.Response.StatusCode = 206;
                                 context.Response.StatusCode = 206;
-                                context.Response.Headers.Add("Content-Range", $"bytes {start2}-{end}/{fs.Length}");
-                                context.Response.ContentLength64 = realLen;
-                                if (realLen < 4096)
-                                {
-                                    var buf = new byte[realLen];
-                                    fs.Read(buf, 0, buf.Length);
-                                    context.Response.OutputStream.Write(buf, 0, buf.Length);
-                                }
-                                else
+                                context.Response.Headers.Add("Content-Range", $"bytes {rangeStart}-{rangeEnd}/{fs.Length}");
+                                context.Response.ContentLength64 = bodyLen;
+
+                                const int megaBytes = 1024 * 1024;
+                                const int bufferChunk = megaBytes * 16;
+
+                                var remainBytes = bodyLen;
+
+                                fs.Position = rangeStart;
+                                while (remainBytes > 0)
                                 {
                                 {
-                                    fs.CopyTo(context.Response.OutputStream);
+                                    var buf = new byte[remainBytes > bufferChunk ? bufferChunk : remainBytes];
+
+                                    var r = fs.Read(buf, 0, buf.Length);
+                                    context.Response.OutputStream.Write(buf, 0, r);
+
+                                    remainBytes -= r;
                                 }
                                 }
                             }
                             }
                         }
                         }
-                        else
-                        {
-                            context.Response.ContentLength64 = fs.Length;
-                            fs.CopyTo(context.Response.OutputStream);
-                        }
                     }
                     }
                     catch (Exception e)
                     catch (Exception e)
                     {
                     {