From 9f8ddc828d4ed719eebdf0413065e8b825d52e6f Mon Sep 17 00:00:00 2001 From: VoidX Date: Sun, 19 Jan 2025 23:05:24 +0100 Subject: [PATCH] CavernPipe done --- Cavern/Channels/ChannelPrototype.cs | 40 +++++- CavernSamples/CavernPipeClient/Program.cs | 13 +- CavernSamples/CavernPipeServer/App.xaml | 1 - .../CavernPipeServer/CavernPipeRenderer.cs | 47 ++++++- .../CavernPipeServer/CavernPipeServer.csproj | 7 +- .../CavernPipeServer.csproj.user | 3 + .../CavernPipeServer/ChannelMeters.cs | 121 ++++++++++++++++++ .../CavernPipeServer/MainWindow.xaml | 12 +- .../CavernPipeServer/MainWindow.xaml.cs | 51 +++++++- CavernSamples/CavernPipeServer/PipeHandler.cs | 41 +++++- .../Resources/MainWindowStrings.hu-HU.xaml | 5 + .../Resources/MainWindowStrings.xaml | 5 + .../ThreadSafeChannelMeters.cs | 32 +++++ .../Elements/DriverRenderTarget.cs | 3 +- .../CavernPipe Bitstream.md | 52 ++++++++ 15 files changed, 415 insertions(+), 18 deletions(-) create mode 100644 CavernSamples/CavernPipeServer/ChannelMeters.cs create mode 100644 CavernSamples/CavernPipeServer/ThreadSafeChannelMeters.cs create mode 100644 docs/Format bitstream definitions/CavernPipe Bitstream.md diff --git a/Cavern/Channels/ChannelPrototype.cs b/Cavern/Channels/ChannelPrototype.cs index c26be9e7..034c4929 100644 --- a/Cavern/Channels/ChannelPrototype.cs +++ b/Cavern/Channels/ChannelPrototype.cs @@ -146,10 +146,27 @@ public static ChannelPrototype[] GetAlternative(ReferenceChannel[] source) { } /// - /// Convert a to the name of the channels. + /// Convert a to the name of the channel, if its position is known. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetName(Channel source) => GetName(GetReference(source)); + + /// + /// Convert a to the name of the channel. /// public static string GetName(ReferenceChannel source) => Mapping[(int)source].Name; + /// + /// Convert a mapping of s to the names of the channels. + /// + public static string[] GetNames(Channel[] source) { + string[] result = new string[source.Length]; + for (int i = 0; i < source.Length; i++) { + result[i] = GetName(source[i]); + } + return result; + } + /// /// Convert a mapping of s to the names of the channels. /// @@ -161,6 +178,27 @@ public static string[] GetNames(ReferenceChannel[] source) { return result; } + /// + /// Get which channel this placement represents. + /// + public static ReferenceChannel GetReference(Channel source) { + if (source.LFE) { + return ReferenceChannel.ScreenLFE; + } + for (int i = 0; i < Mapping.Length; i++) { + if (Math.Abs(Mapping[i].X - source.X) < .05f && Math.Abs(Mapping[i].Y - source.Y) < .05f) { + return (ReferenceChannel)i; + } + } + Vector3 cubicalPos = source.CubicalPos; + for (int i = 0; i < AlternativePositions.Length; i++) { + if (AlternativePositions[i] == cubicalPos) { + return (ReferenceChannel)i; + } + } + return ReferenceChannel.Unknown; + } + /// /// Convert a mapping of s to channel name initials. /// diff --git a/CavernSamples/CavernPipeClient/Program.cs b/CavernSamples/CavernPipeClient/Program.cs index 61f38e02..a9e3013d 100644 --- a/CavernSamples/CavernPipeClient/Program.cs +++ b/CavernSamples/CavernPipeClient/Program.cs @@ -28,23 +28,26 @@ // Sending the file or part to the pipe using FileStream reader = File.OpenRead(args[0]); -long sent = 0; +long sent = 0, + received = 0; float[] writeBuffer = []; byte[] sendBuffer = new byte[1024 * 1024], receiveBuffer = []; -while (sent < reader.Length) { +while (received < target.Length) { int toSend = reader.Read(sendBuffer, 0, sendBuffer.Length); pipe.Write(BitConverter.GetBytes(toSend)); pipe.Write(sendBuffer, 0, toSend); sent += toSend; // If there is incoming data, write it to file - int toReceive = pipe.ReadInt32(); + int toReceive = pipe.ReadInt32(), + samples = toReceive / sizeof(float); if (receiveBuffer.Length < toReceive) { receiveBuffer = new byte[toReceive]; - writeBuffer = new float[toReceive / sizeof(float)]; + writeBuffer = new float[samples]; } pipe.ReadAll(receiveBuffer, 0, toReceive); Buffer.BlockCopy(receiveBuffer, 0, writeBuffer, 0, toReceive); - target.WriteBlock(writeBuffer, 0, toReceive / sizeof(float)); + target.WriteBlock(writeBuffer, 0, samples); + received += samples / target.ChannelCount; } \ No newline at end of file diff --git a/CavernSamples/CavernPipeServer/App.xaml b/CavernSamples/CavernPipeServer/App.xaml index 21fd2b5c..3ecf8716 100644 --- a/CavernSamples/CavernPipeServer/App.xaml +++ b/CavernSamples/CavernPipeServer/App.xaml @@ -1,7 +1,6 @@  diff --git a/CavernSamples/CavernPipeServer/CavernPipeRenderer.cs b/CavernSamples/CavernPipeServer/CavernPipeRenderer.cs index cee4a9ed..8234cad7 100644 --- a/CavernSamples/CavernPipeServer/CavernPipeRenderer.cs +++ b/CavernSamples/CavernPipeServer/CavernPipeRenderer.cs @@ -6,12 +6,28 @@ using Cavern.Format; using Cavern.Format.Renderers; using Cavern.Format.Utilities; +using Cavern.Utilities; namespace CavernPipeServer { /// /// Handles rendering of incoming audio content and special protocol additions/transformations. /// public class CavernPipeRenderer : IDisposable { + /// + /// Rendering of new content has started, the are updated from the latest Cavern user files. + /// + public event Action OnRenderingStarted; + + /// + /// Provides per-channel metering data. Channel gains are ratios between -50 and 0 dB FS. + /// + public delegate void OnMetersAvailable(float[] meters); + + /// + /// New output data was rendered, audio meters can be updated. Channel gains are ratios between -50 and 0 dB FS. + /// + public event OnMetersAvailable MetersAvailable; + /// /// Protocol message decoder. /// @@ -55,8 +71,15 @@ void RenderThread() { Renderer renderer = reader.GetRenderer(); Listener listener = new Listener { SampleRate = reader.SampleRate, - UpdateRate = Protocol.UpdateRate + UpdateRate = Protocol.UpdateRate, + AudioQuality = QualityModes.Perfect, }; + OnRenderingStarted?.Invoke(); + + float[] reRender = null; + if (Listener.Channels.Length != Protocol.OutputChannels) { + reRender = new float[Protocol.OutputChannels * Protocol.UpdateRate]; + } listener.AttachSources(renderer.Objects); // When this writer is used without writing a header, it's a BitDepth converter from float to anything, and can dump to streams. @@ -64,11 +87,31 @@ void RenderThread() { while (Input != null) { float[] render = listener.Render(); - streamDumper.WriteBlock(render, 0, render.LongLength); + UpdateMeters(render); + if (reRender == null) { + streamDumper.WriteBlock(render, 0, render.LongLength); + } else { + Array.Clear(reRender); + WaveformUtils.Downmix(render, reRender, Protocol.OutputChannels); + streamDumper.WriteBlock(reRender, 0, reRender.LongLength); + } } } catch { Dispose(); } } + + /// + /// Send the event with the rendered channel names and their last rendered gains. + /// + /// Channel gains are ratios between -50 and 0 dB FS. + void UpdateMeters(float[] audioOut) { + float[] result = new float[Listener.Channels.Length]; + for (int i = 0; i < result.Length; i++) { + float channelGain = QMath.GainToDb(WaveformUtils.GetRMS(audioOut, i, result.Length)); + result[i] = QMath.Clamp01(QMath.LerpInverse(-50, 0, channelGain)); + } + MetersAvailable?.Invoke(result); + } } } \ No newline at end of file diff --git a/CavernSamples/CavernPipeServer/CavernPipeServer.csproj b/CavernSamples/CavernPipeServer/CavernPipeServer.csproj index 4b745b82..ffdc7f04 100644 --- a/CavernSamples/CavernPipeServer/CavernPipeServer.csproj +++ b/CavernSamples/CavernPipeServer/CavernPipeServer.csproj @@ -1,6 +1,6 @@  - Exe + WinExe net8.0-windows disable true @@ -23,6 +23,11 @@ <_DeploymentManifestIconFile Remove="..\Icon.ico" /> + + + MSBuild:Compile + + diff --git a/CavernSamples/CavernPipeServer/CavernPipeServer.csproj.user b/CavernSamples/CavernPipeServer/CavernPipeServer.csproj.user index 00444098..0221e6cf 100644 --- a/CavernSamples/CavernPipeServer/CavernPipeServer.csproj.user +++ b/CavernSamples/CavernPipeServer/CavernPipeServer.csproj.user @@ -7,6 +7,9 @@ + + Designer + Designer diff --git a/CavernSamples/CavernPipeServer/ChannelMeters.cs b/CavernSamples/CavernPipeServer/ChannelMeters.cs new file mode 100644 index 00000000..3cd8092f --- /dev/null +++ b/CavernSamples/CavernPipeServer/ChannelMeters.cs @@ -0,0 +1,121 @@ +using System; +using System.Windows; +using System.Windows.Controls; + +using Cavern; +using Cavern.Channels; + +namespace CavernPipeServer { + /// + /// Display channel output meters on a . + /// + /// Add meters as children of this control + /// Reference channel name display + /// Reference meter display + public class ChannelMeters(Panel canvas, TextBlock labelProto, ProgressBar barProto) { + /// + /// Meters are displayed on this control. + /// + protected Panel canvas = canvas; + + /// + /// Display the last gain updates of the channels on these controls. + /// + (TextBlock, ProgressBar)[] displays; + + /// + /// To prevent slowdowns caused by too many UI updates, collect the peaks over longer time intervals and update with said peaks. + /// + protected float[] movingPeaks; + + /// + /// When to update the meters. + /// + DateTime updateAt; + + /// + /// Create the UI elements for displaying meter values later. The first call to this function is always after the 's creation, + /// so the are what will be rendered. + /// + public virtual void Enable() { + string[] channels = ChannelPrototype.GetNames(Listener.Channels); + displays = new (TextBlock, ProgressBar)[channels.Length]; + movingPeaks = new float[channels.Length]; + for (int i = 0; i < channels.Length; i++) { + double marginTop = labelProto.Margin.Top + (labelProto.Height + 5) * i; + TextBlock channelName = new TextBlock { + Margin = new Thickness(labelProto.Margin.Left, marginTop, labelProto.Margin.Right, 0), + HorizontalAlignment = labelProto.HorizontalAlignment, + VerticalAlignment = labelProto.VerticalAlignment, + Text = channels[i] + }; + ProgressBar progressBar = new ProgressBar { + Margin = new Thickness(barProto.Margin.Left, marginTop, barProto.Margin.Right, 0), + HorizontalAlignment = barProto.HorizontalAlignment, + VerticalAlignment = barProto.VerticalAlignment, + Width = barProto.Width, + Height = barProto.Height, + Maximum = 1 + }; + displays[i] = (channelName, progressBar); + canvas.Children.Add(channelName); + canvas.Children.Add(progressBar); + } + } + + /// + /// Update the displayed meter values of each channel if they exist. + /// + public void Update(float[] meters) { + if (displays == null) { + return; + } + + for (int i = 0; i < movingPeaks.Length; i++) { + if (movingPeaks[i] < meters[i]) { + movingPeaks[i] = meters[i]; + } + } + + if (updateAt < DateTime.Now) { + UpdateUI(movingPeaks); + Array.Clear(movingPeaks); + updateAt = DateTime.Now + updateInterval; + } + } + + /// + /// Remove the channel output meters from the UI. + /// + public virtual void Disable() { + if (displays == null) { + return; + } + + for (int i = 0; i < displays.Length; i++) { + canvas.Children.Remove(displays[i].Item1); + canvas.Children.Remove(displays[i].Item2); + } + displays = null; + } + + /// + /// The part of that requires a dispatcher. + /// + /// + protected virtual void UpdateUI(float[] meters) { + if (displays == null) { + return; + } + + for (int i = 0, c = Math.Min(displays.Length, meters.Length); i < c; i++) { + displays[i].Item2.Value = meters[i]; + } + } + + /// + /// How often to update the meters. + /// + static readonly TimeSpan updateInterval = TimeSpan.FromSeconds(.05); + } +} \ No newline at end of file diff --git a/CavernSamples/CavernPipeServer/MainWindow.xaml b/CavernSamples/CavernPipeServer/MainWindow.xaml index 640df734..a3391c6c 100644 --- a/CavernSamples/CavernPipeServer/MainWindow.xaml +++ b/CavernSamples/CavernPipeServer/MainWindow.xaml @@ -4,8 +4,14 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" - Title="CavernPipe Server" - Visibility="Hidden" ShowInTaskbar="False" Width="600" Height="300"> - + Title="CavernPipe Server" Visibility="Hidden" ShowInTaskbar="False" ResizeMode="NoResize" Background="#696969" + Width="300" Height="300"> + + + + + + + \ No newline at end of file diff --git a/CavernSamples/CavernPipeServer/MainWindow.xaml.cs b/CavernSamples/CavernPipeServer/MainWindow.xaml.cs index 47d8ccc1..ec6c8c6d 100644 --- a/CavernSamples/CavernPipeServer/MainWindow.xaml.cs +++ b/CavernSamples/CavernPipeServer/MainWindow.xaml.cs @@ -3,9 +3,10 @@ using System.Drawing; using System.Windows; using System.Windows.Forms; +using System.Windows.Media; using Application = System.Windows.Application; -using MessageBox = System.Windows.MessageBox; +using Color = System.Windows.Media.Color; namespace CavernPipeServer { /// @@ -25,13 +26,23 @@ public partial class MainWindow : Window { /// /// Network connection with watchdog. /// - readonly PipeHandler handler = new PipeHandler(); + readonly PipeHandler handler; + + /// + /// Displays real-time renderer channel gains. + /// + readonly ThreadSafeChannelMeters meters; /// /// Language string access. /// readonly ResourceDictionary language = Consts.Language.GetMainWindowStrings(); + /// + /// Closing the application is in progress. + /// + bool exiting; + /// /// Main status/configuration window and background operation handler. /// @@ -51,6 +62,13 @@ public MainWindow() { ContextMenuStrip = contextMenu, }; icon.DoubleClick += Open; + + meters = new ThreadSafeChannelMeters(canvas, chProto, vmProto); + handler = new PipeHandler(); + handler.OnRenderingStarted += meters.Enable; + handler.StatusChanged += OnServerStatusChange; + handler.MetersAvailable += meters.Update; + OnServerStatusChange(); } /// @@ -84,9 +102,38 @@ void Restart(object _, EventArgs __) { /// Close the CavernPipe server. /// void Exit(object _, EventArgs e) { + exiting = true; handler.Dispose(); icon.Dispose(); Application.Current.Shutdown(); } + + /// + /// Update the UI to reflect the server's status. + /// + void OnServerStatusChange() { + if (exiting) { + return; + } + + canvas.Dispatcher.Invoke(() => { + Color statusColor; + if (handler.IsConnected) { + statusColor = Color.FromArgb(255, 0, 147, 191); + status.Content = (string)language["SProc"]; + } else if (handler.Running) { + statusColor = Color.FromArgb(255, 0, 255, 0); + status.Content = (string)language["SWait"]; + } else { + statusColor = Color.FromArgb(255, 255, 0, 0); + status.Content = (string)language["SNoSv"]; + } + status.Background = new SolidColorBrush(statusColor); + }); + + if (!handler.IsConnected) { + meters.Disable(); + } + } } } \ No newline at end of file diff --git a/CavernSamples/CavernPipeServer/PipeHandler.cs b/CavernSamples/CavernPipeServer/PipeHandler.cs index 81dcbd30..4a42702d 100644 --- a/CavernSamples/CavernPipeServer/PipeHandler.cs +++ b/CavernSamples/CavernPipeServer/PipeHandler.cs @@ -12,10 +12,44 @@ namespace CavernPipeServer { /// Handles the network communication of CavernPipe. A watchdog for a self-created named pipe called "CavernPipe". /// public class PipeHandler : IDisposable { + /// + /// Rendering of new content has started, the are updated from the latest Cavern user files. + /// + public event Action OnRenderingStarted; + + /// + /// Either the or status have changed. + /// + public event Action StatusChanged; + + /// + /// Allows subscribing to . + /// + public event CavernPipeRenderer.OnMetersAvailable MetersAvailable; + /// /// The network connection is kept alive. /// - public bool Running { get; private set; } + public bool Running { + get => running; + set { + running = value; + StatusChanged?.Invoke(); + } + } + bool running; + + /// + /// A client is connected to the server. + /// + public bool IsConnected { + get => isConnected; + set { + isConnected = value; + StatusChanged?.Invoke(); + } + } + bool isConnected; /// /// Used for providing thread safety. @@ -85,7 +119,10 @@ async void ThreadProc() { try { TryStartServer(); await server.WaitForConnectionAsync(canceler.Token); + IsConnected = true; using CavernPipeRenderer renderer = new CavernPipeRenderer(server); + renderer.OnRenderingStarted += OnRenderingStarted; + renderer.MetersAvailable += MetersAvailable; byte[] inBuffer = [], outBuffer = []; while (Running) { @@ -115,6 +152,8 @@ async void ThreadProc() { server.Flush(); } } + + IsConnected = false; lock (locker) { server.Dispose(); server = null; diff --git a/CavernSamples/CavernPipeServer/Resources/MainWindowStrings.hu-HU.xaml b/CavernSamples/CavernPipeServer/Resources/MainWindowStrings.hu-HU.xaml index 2befc164..18eda784 100644 --- a/CavernSamples/CavernPipeServer/Resources/MainWindowStrings.hu-HU.xaml +++ b/CavernSamples/CavernPipeServer/Resources/MainWindowStrings.hu-HU.xaml @@ -6,6 +6,11 @@ Szerver újraindítása Kilépés + + Feldolgozás... + Várakozás médialejátszóra... + A CavernPipe szerver nem fut. + Hiba A CavernPipe szerver már fut, és készen áll a lejátszásra. diff --git a/CavernSamples/CavernPipeServer/Resources/MainWindowStrings.xaml b/CavernSamples/CavernPipeServer/Resources/MainWindowStrings.xaml index bb8a50fa..959540d2 100644 --- a/CavernSamples/CavernPipeServer/Resources/MainWindowStrings.xaml +++ b/CavernSamples/CavernPipeServer/Resources/MainWindowStrings.xaml @@ -6,6 +6,11 @@ Restart server Exit + + Processing... + Waiting for media player... + The CavernPipe server isn't running. + Error The CavernPipe server is already running and ready for playback. diff --git a/CavernSamples/CavernPipeServer/ThreadSafeChannelMeters.cs b/CavernSamples/CavernPipeServer/ThreadSafeChannelMeters.cs new file mode 100644 index 00000000..ab36c204 --- /dev/null +++ b/CavernSamples/CavernPipeServer/ThreadSafeChannelMeters.cs @@ -0,0 +1,32 @@ +using System.Windows.Controls; + +namespace CavernPipeServer { + /// + /// Multithreaded version of , for use outside dispatchers. + /// + /// Add meters as children of this control + /// Reference channel name display + /// Reference meter display + public class ThreadSafeChannelMeters(Panel canvas, TextBlock labelProto, ProgressBar barProto) : ChannelMeters(canvas, labelProto, barProto) { + /// + public override void Enable() { + lock (canvas) { + canvas.Dispatcher.Invoke(base.Enable); + } + } + + /// + public override void Disable() { + lock (canvas) { + canvas.Dispatcher.Invoke(base.Disable); + } + } + + /// + protected override void UpdateUI(float[] meters) { + lock (canvas) { + canvas.Dispatcher.Invoke(base.UpdateUI, meters); + } + } + } +} \ No newline at end of file diff --git a/CavernSamples/CavernizeGUI/Elements/DriverRenderTarget.cs b/CavernSamples/CavernizeGUI/Elements/DriverRenderTarget.cs index 35431bc2..5d94a9a5 100644 --- a/CavernSamples/CavernizeGUI/Elements/DriverRenderTarget.cs +++ b/CavernSamples/CavernizeGUI/Elements/DriverRenderTarget.cs @@ -1,6 +1,5 @@ using Cavern; using Cavern.Channels; -using Cavern.Format.Renderers; namespace CavernizeGUI.Elements { /// @@ -19,7 +18,7 @@ static ReferenceChannel[] GetChannels() { new Listener(); ReferenceChannel[] result = new ReferenceChannel[Listener.Channels.Length]; for (int i = 0; i < result.Length; i++) { - result[i] = Renderer.ChannelFromPosition(Listener.Channels[i].CubicalPos); + result[i] = ChannelPrototype.GetReference(Listener.Channels[i]); } return result; } diff --git a/docs/Format bitstream definitions/CavernPipe Bitstream.md b/docs/Format bitstream definitions/CavernPipe Bitstream.md new file mode 100644 index 00000000..192649c1 --- /dev/null +++ b/docs/Format bitstream definitions/CavernPipe Bitstream.md @@ -0,0 +1,52 @@ +# CavernPipe Bitstream Structure +CavernPipe is a solution for inter-process rendering of any supported bitstream +with named pipes. When the user has CavernPipe installed and it's running, the +CavernPipe named pipe is available for a single consuming process. The protocol +of CavernPipe is very minimal and easy to implement. + +## Initial setup +The user can configure the system layout in the Cavern Driver, downloadable from +the [Cavern website](https://cavern.sbence.hu). The playback software only needs +to know the channel count of the user's layout to be able to provide correct +output, everything else will be handled by CavernPipe. The channel count is the +first line in `%appdata%\Cavern\Save.dat`. If this file doesn't exist, it's 6. + +## Handshake +After connection, the client has to define the format in which it expects the +rendered data. These are the 8 bytes required before rendering can begin. +|------|-------|------| +| Byte | Type | Data | +|------|-------| +| 0 | Byte | [Bit depth](https://cavern.sbence.hu/cavern/doc.php?if=api/Cavern/Format/BitDepth/index) | +| 1 | Byte | Mandatory frames to process. Before CavernPipe replies, it will render at least `mandatory frames * update rate * channel count` samples. If there aren't enough data sent to the server to render that much, a deadlock happens. | +| 2-3 | Int16 | Output channel count, the number of available system output channels. If it doesn't match with the user's actual channel count, no problem, CavernPipe will handle it, but _no rendering or mapping shall happen in any media player with Cavern-rendered samples_. | +| 4-7 | Int32 | Update rate: samples per channel for each rendered frame. | +|------|-------|------| + +### Proper E-AC-3 handshake +For optimal rendering of Enhanced AC-3, the update rate shall be 64 samples. A +single frame of E-AC-3 data is always 1536 samples, so if you'd like to render +E-AC-3 + JOC with Cavern, set the mandatory frames to 24, and the update rate to +64. This is the recommended rendering detail in the JOC specification. If you +know the frame border, just send a single frame and wait for the reply just +before that frame should be played. + +CavernPipe also supports caching, multiple frames can be sent in advance, which +will be rendered in the background, and replies will always contain at least the +mandatory frames asked for. Excess samples have to be cached client-side. On +seek, break the connection and reconnect to CavernPipe, that clears the cache. + +## Rendering +Communication after the handshake is very straightforward: send a single 32-bit +integer, which is the number of bytes in the available bitstream data, then send +the available bitstream. It can be more than what's initially needed as defined +in the mandatory frames, but in this case, it's very likely that they will only +get rendered in the next reply. If data is cached, sending a single 0 integer +will send back every currently processed data, at least the mandatory frames. +This is how to flush the CavernPipe. Warning: if there isn't enough data for the +mandatory frames in the cache, a deadlock happens. + +The result of CavernPipe will follow the same format: a 32-bit integer arrives +with the data length, and the bytes of the rendered interlaced PCM stream will +follow. If the bit depth matches the system output, a simple buffer copy is +enough to provide the correct output.