From 3a6cd300ee2707b75c4b0c366bce0c4fae68dfa8 Mon Sep 17 00:00:00 2001 From: Mike Oliphant Date: Thu, 13 Feb 2025 15:35:07 -0800 Subject: [PATCH] deep in api refactor --- StompboxAPI/APIClient.cs | 185 ++++++ StompboxAPI/NativeAPI.cs | 11 +- StompboxAPI/StompboxAPI.csproj | 1 + StompboxAPI/StompboxProcessor.cs | 32 + StompboxAPIBase/StompboxAPIBase.projitems | 1 + StompboxAPIBase/StompboxClient.cs | 241 +++++++ StompboxRemoteClient/NetworkInterface.cs | 151 +++++ StompboxRemoteClient/PluginFactory.cs | 101 +++ StompboxRemoteClient/ProtocolClient.cs | 620 ++++++++++++++++++ StompboxRemoteClient/RemoteClient.cs | 205 ++++++ .../StompboxRemoteClient.csproj | 11 + StompboxUI.sln | 27 + 12 files changed, 1585 insertions(+), 1 deletion(-) create mode 100644 StompboxAPI/APIClient.cs create mode 100644 StompboxAPIBase/StompboxClient.cs create mode 100644 StompboxRemoteClient/NetworkInterface.cs create mode 100644 StompboxRemoteClient/PluginFactory.cs create mode 100644 StompboxRemoteClient/ProtocolClient.cs create mode 100644 StompboxRemoteClient/RemoteClient.cs create mode 100644 StompboxRemoteClient/StompboxRemoteClient.csproj diff --git a/StompboxAPI/APIClient.cs b/StompboxAPI/APIClient.cs new file mode 100644 index 0000000..83487db --- /dev/null +++ b/StompboxAPI/APIClient.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace StompboxAPI +{ + public class APIClient : StompboxClient + { + StompboxProcessor processor; + bool needPresetLoad = false; + + public override bool NeedUIReload + { + get + { + if (needPresetLoad && !processor.IsPresetLoading()) + { + UpdateProgram(); + + needPresetLoad = false; + + return true; + } + + return base.NeedUIReload; + } + + set => base.NeedUIReload = value; + } + + public override int CurrentPresetIndex + { + get => base.CurrentPresetIndex; + + set + { + base.CurrentPresetIndex = value; + + needPresetLoad = true; + } + } + + public APIClient() + : base() + { + if (!Directory.Exists(PluginPath)) + { + Directory.CreateDirectory(PluginPath); + } + + processor = new StompboxProcessor(PluginPath, dawMode: true); + + InClientMode = false; + + //processorWrapper.SetMidiCallback(HandleMidi); + + UpdateProgram(); + } + + public void StartServer() + { + processor.StartServer(); + } + + public override void UpdatePresets() + { + needPresetLoad = false; + + base.UpdatePresets(); + + //SetPresetNames(new List(processorWrapper.GetPresets().Trim().Split(' '))); + + //SuppressCommandUpdates = true; + //SetSelectedPreset(processorWrapper.GetCurrentPreset()); + //SuppressCommandUpdates = false; + } + + public override void UpdateProgram() + { + base.UpdateProgram(); + + UpdateUI(); + } + + public override IEnumerable GetAllPluginNames() + { + return processor.GetAllPlugins(); + } + + public override IAudioPlugin GetPluginDefinition(string pluginName) + { + return PluginFactory.GetPluginDefinition(pluginName); + } + + public override IAudioPlugin CreatePlugin(string pluginName, string pluginID) + { + return processor.CreatePlugin(pluginID); + } + + public override void UpdateUI() + { + LoadChainEffects(InputPlugins, processor.GetInputChainPlugins()); + LoadChainEffects(FxLoopPlugins, processor.GetFxLoopPlugins()); + LoadChainEffects(OutputPlugins, processor.GetOutputChainPlugins()); + + base.UpdateUI(); + } + + void LoadChainEffects(List chain, IEnumerable plugins) + { + chain.Clear(); + + foreach (string pluginName in plugins) + { + chain.Add(PluginFactory.CreatePlugin(pluginName)); + } + } + + protected override IAudioPlugin CreateSlotPlugin(string slotName, string defaultPlugin) + { + string pluginID = processor.GetPluginSlot(slotName); + + return PluginFactory.CreatePlugin(pluginID); + } + + public override void Init(double sampleRate) + { + processor.Init(sampleRate); + } + + // public String GetProgramState() + // { + //#if !STOMPBOXREMOTE + // String settingsString = processorWrapper.DumpSettings(); + // String programString = processorWrapper.DumpProgram(); + + // return settingsString + programString; + //#else + // return null; + //#endif + // } + + public unsafe void Process(double* input, double* output, uint bufferSize) + { + processor.Process(input, output, bufferSize); + } + + long samplePos = 0; + + public unsafe void SimulateAudio() + { + int bufferSize = 1024; + int sampleRate = 44100; + + int sleepMS = sampleRate / 1000; + + Init(sampleRate); + + double* inBuf = (double*)Marshal.AllocHGlobal(bufferSize * sizeof(double)); + double* outBuf = (double*)Marshal.AllocHGlobal(bufferSize * sizeof(double)); + + while (true) + { + if (StopSimulateAudio) + break; + + for (int i = 0; i < bufferSize; i++) + { + inBuf[i] = 0; + + inBuf[i] += Math.Sin(((double)samplePos / (double)sampleRate) * 440 * Math.PI * 2) * 0.25f; + + samplePos++; + } + + processor.Process(inBuf, outBuf, (uint)bufferSize); + + Thread.Sleep(sleepMS); + } + } + } +} diff --git a/StompboxAPI/NativeAPI.cs b/StompboxAPI/NativeAPI.cs index 271939b..35aca37 100644 --- a/StompboxAPI/NativeAPI.cs +++ b/StompboxAPI/NativeAPI.cs @@ -24,11 +24,14 @@ class NativeApi public static extern void DeleteProcessor(IntPtr processor); [DllImport(STOMPBOX_LIB_NAME)] - public static extern void Init(IntPtr processor, double sampleRate); + public static extern void InitProcessor(IntPtr processor, double sampleRate); [DllImport(STOMPBOX_LIB_NAME)] public static extern void StartServer(IntPtr processor); + [DllImport(STOMPBOX_LIB_NAME)] + public static unsafe extern void Process(IntPtr processor, double* input, double* output, uint bufferSize); + [DllImport(STOMPBOX_LIB_NAME)] [return: MarshalAs(UnmanagedType.LPWStr)] public static extern string GetDataPath(IntPtr processor); @@ -36,6 +39,12 @@ class NativeApi [DllImport(STOMPBOX_LIB_NAME)] public static extern IntPtr GetAllPlugins(IntPtr processor); + [DllImport(STOMPBOX_LIB_NAME)] + public static extern IntPtr GetPluginSlot(IntPtr processor, [MarshalAs(UnmanagedType.LPWStr)] string slotName); + + [DllImport(STOMPBOX_LIB_NAME)] + public static extern IntPtr GetChainPlugins(IntPtr processor, [MarshalAs(UnmanagedType.LPStr)] string chainName); + [DllImport(STOMPBOX_LIB_NAME)] public static extern IntPtr CreatePlugin(IntPtr processor, [MarshalAs(UnmanagedType.LPStr)] string id); diff --git a/StompboxAPI/StompboxAPI.csproj b/StompboxAPI/StompboxAPI.csproj index 79c5495..f58c8af 100644 --- a/StompboxAPI/StompboxAPI.csproj +++ b/StompboxAPI/StompboxAPI.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + True diff --git a/StompboxAPI/StompboxProcessor.cs b/StompboxAPI/StompboxProcessor.cs index df79165..ea0a81d 100644 --- a/StompboxAPI/StompboxProcessor.cs +++ b/StompboxAPI/StompboxProcessor.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; @@ -17,6 +19,11 @@ public StompboxProcessor(string dataPath, bool dawMode) nativeProcessor = NativeApi.CreateProcessor(dataPath, dawMode); } + public void Init(double sampleRate) + { + NativeApi.InitProcessor(nativeProcessor, sampleRate); + } + public UnmanagedAudioPlugin CreatePlugin(string id) { IntPtr nativePlugin = NativeApi.CreatePlugin(nativeProcessor, id); @@ -35,5 +42,30 @@ public List GetAllPlugins() { return NativeApi.GetListFromStringVector(NativeApi.GetAllPlugins(nativeProcessor)); } + + public string GetPluginSlot(string slotName) + { + return Marshal.PtrToStringAnsi(NativeApi.GetPluginSlot(nativeProcessor, slotName)); + } + + public List GetInputChainPlugins() + { + return NativeApi.GetListFromStringVector(NativeApi.GetChainPlugins(nativeProcessor, "InputChain")); + } + + public List GetFxLoopPlugins() + { + return NativeApi.GetListFromStringVector(NativeApi.GetChainPlugins(nativeProcessor, "FxLoop")); + } + + public List GetOutputChainPlugins() + { + return NativeApi.GetListFromStringVector(NativeApi.GetChainPlugins(nativeProcessor, "OutputChain")); + } + + public unsafe void Process(double* input, double* output, uint bufferSize) + { + NativeApi.Process(nativeProcessor, input, output, bufferSize); + } } } diff --git a/StompboxAPIBase/StompboxAPIBase.projitems b/StompboxAPIBase/StompboxAPIBase.projitems index b8e2499..b79d23f 100644 --- a/StompboxAPIBase/StompboxAPIBase.projitems +++ b/StompboxAPIBase/StompboxAPIBase.projitems @@ -10,5 +10,6 @@ + \ No newline at end of file diff --git a/StompboxAPIBase/StompboxClient.cs b/StompboxAPIBase/StompboxClient.cs new file mode 100644 index 0000000..118d63d --- /dev/null +++ b/StompboxAPIBase/StompboxClient.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; + +namespace StompboxAPI +{ + public class MidiCCMapEntry + { + public int CCNumber { get; set; } + public string PluginName { get; set; } + public string PluginParameter { get; set; } + } + + public class StompboxClient + { + public static StompboxClient Instance { get; private set; } + + public static Action DebugAction { get; set; } + + public Action MidiCallback { get; set; } + + public double BPM { get; set; } + public bool InClientMode { get; protected set; } + public bool AllowMidiMapping { get; set; } + public string PluginPath { get; set; } + public List InputPlugins { get; set; } + public List FxLoopPlugins { get; set; } + public List OutputPlugins { get; set; } + public IEnumerable AllActivePlugins + { + get + { + yield return Tuner; + + yield return InputGain; + + if (Amp != null) + yield return Amp; + + foreach (IAudioPlugin plugin in InputPlugins) + yield return plugin; + + if (Tonestack != null) + yield return Tonestack; + + foreach (IAudioPlugin plugin in FxLoopPlugins) + yield return plugin; + + if (Cabinet != null) + yield return Cabinet; + + foreach (IAudioPlugin plugin in OutputPlugins) + yield return plugin; + + yield return AudioPlayer; + + yield return AudioRecorder; + + yield return MasterVolume; + } + } + public IAudioPlugin Tuner { get; private set; } + public IAudioPlugin InputGain { get; private set; } + public IAudioPlugin MasterVolume { get; private set; } + public IAudioPlugin Amp { get; private set; } + public IAudioPlugin Tonestack { get; private set; } + public IAudioPlugin Cabinet { get; private set; } + public IAudioPlugin AudioPlayer { get; private set; } + public IAudioPlugin AudioRecorder { get; private set; } + public float MaxDSPLoad { get; private set; } + public float MinDSPLoad { get; private set; } + public List MidiCCMap { get; private set; } = new List(); + public int MidiModeCC { get; set; } = -1; + public Dictionary MidiStompCCMap { get; private set; } = new Dictionary(); + public bool StopSimulateAudio { get; set; } + public List PresetNames { get; private set; } + + public virtual int CurrentPresetIndex { get; set; } + + public virtual bool NeedUIReload { get; set; } + + bool needUIReload = false; + + public StompboxClient() + { + Instance = this; + + Debug("Creating StompboxClient."); + + AllowMidiMapping = true; + BPM = 120; + + PluginPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "stompbox"); + + + PresetNames = new List(); + + InputPlugins = new List(); + FxLoopPlugins = new List(); + OutputPlugins = new List(); + } + + + public virtual void UpdatePresets() + { + } + + public virtual void UpdateProgram() + { + UpdatePresets(); + } + + + public virtual IEnumerable GetAllPluginNames() + { + return null; + } + + public IEnumerable GetAllPluginDefinitions() + { + foreach (string name in GetAllPluginNames()) + { + yield return GetPluginDefinition(name); + } + } + + public IEnumerable GetAllUserPluginDefinitions() + { + foreach (string name in GetAllPluginNames()) + { + IAudioPlugin plugin = GetPluginDefinition(name); + + if (plugin.IsUserSelectable) + yield return plugin; + } + } + + public virtual IAudioPlugin GetPluginDefinition(string pluginName) + { + return null; + } + + public IAudioPlugin CreatePlugin(string id) + { + return CreateSlotPlugin(id, id); + } + + public virtual IAudioPlugin CreatePlugin(string pluginName, string pluginID) + { + return null; + } + + public void Debug(string debugStr) + { + if (DebugAction != null) + DebugAction(debugStr); + } + + + public void ReportDSPLoad(float maxDSPLoad, float minDSPLoad) + { + MaxDSPLoad = maxDSPLoad; + MinDSPLoad = minDSPLoad; + } + + public void SetInputChain(List plugins) + { + InputPlugins = plugins; + } + + public void SetFxLoop(List plugins) + { + FxLoopPlugins = plugins; + } + + + public void SetOutputChain(List plugins) + { + OutputPlugins = plugins; + } + + public void SetPresetNames(List presetNames) + { + PresetNames.Clear(); + + foreach (string presetName in presetNames) + { + PresetNames.Add(presetName); + } + } + + public void SetSelectedPreset(string presetName) + { + Debug("Set selected preset: " + presetName); + + CurrentPresetIndex = PresetNames.IndexOf(presetName); + } + + public virtual void UpdateUI() + { + Debug("*** Update UI"); + + Tuner = CreatePlugin("Tuner"); + + InputGain = CreatePlugin("Input"); + + Amp = CreateSlotPlugin("Amp", "NAM"); + + Tonestack = CreateSlotPlugin("Tonestack", "EQ-7"); + + Cabinet = CreateSlotPlugin("Cabinet", "Cabinet"); + + MasterVolume = CreatePlugin("Master"); + + AudioPlayer = CreatePlugin("AudioFilePlayer"); + + if (StompboxClient.Instance.InClientMode) + AudioRecorder = CreatePlugin("AudioFileRecorder"); + + NeedUIReload = true; + } + + protected virtual IAudioPlugin CreateSlotPlugin(string slotName, string defaultPlugin) + { + return null; + } + + public virtual void Init(double sampleRate) + { + } + + void HandleMidi(int midiCommand, int midiData1, int midiData2) + { + if (MidiCallback != null) + MidiCallback(midiCommand, midiData1, midiData2); + } + + } +} diff --git a/StompboxRemoteClient/NetworkInterface.cs b/StompboxRemoteClient/NetworkInterface.cs new file mode 100644 index 0000000..6d40b97 --- /dev/null +++ b/StompboxRemoteClient/NetworkInterface.cs @@ -0,0 +1,151 @@ +using System; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace StompboxAPI +{ + public class NetworkClient + { + public bool Running { get; private set; } + public bool Connected { get; private set; } + public string LastError { get; private set; } + + TcpClient client; + NetworkStream stream; + Thread runThread = null; + + public Action LineHandler { get; set; } + public Action DebugAction { get; set; } + + Action resultCallback = null; + + public NetworkClient() + { + } + + public bool Start(string serverName, int port, Action resultCallback) + { + this.resultCallback = resultCallback; + + try + { + Debug("** Connecting to " + serverName + ":" + port); + + client = new TcpClient(); + + var connectionTask = client.ConnectAsync(serverName, port).ContinueWith(task => + { + return task.IsFaulted ? null : client; + }, TaskContinuationOptions.ExecuteSynchronously); + var timeoutTask = Task.Delay(5000) + .ContinueWith(task => null, TaskContinuationOptions.ExecuteSynchronously); + var resultTask = Task.WhenAny(connectionTask, timeoutTask).Unwrap(); + + var resultTcpClient = resultTask.GetAwaiter().GetResult(); + + if (client.Connected) + { + stream = client.GetStream(); + + Connected = true; + + Debug("** Connected!"); + + runThread = new Thread(RunServer); + runThread.Start(); + } + + resultCallback(client.Connected); + + return client.Connected; + } + catch (Exception ex) + { + LastError = ex.ToString(); + + Debug("** Connect failed with: " + LastError); + + resultCallback(false); + } + + return false; + } + + void Debug(string debugStr) + { + if (DebugAction != null) + DebugAction(debugStr); + } + + public void Stop() + { + client.Close(); + + if (runThread != null) + runThread.Join(); + } + + public void SendData(String data) + { + if (!Running) + return; + + try + { + Byte[] byteData = System.Text.Encoding.ASCII.GetBytes(data); + + stream.Write(byteData, 0, data.Length); + } + catch + { + } + } + + void RunServer() + { + try + { + using (StreamReader reader = new StreamReader(stream)) + { + Running = true; + + resultCallback(true); // Do this here to make sure we don't send any messages before we are ready to get the response + + do + { + string line = reader.ReadLine(); + + if (String.IsNullOrEmpty(line)) + { + if (client.Client.Poll(0, SelectMode.SelectRead)) + { + byte[] buff = new byte[1]; + + if (client.Client.Receive(buff, SocketFlags.Peek) == 0) + { + break; + } + } + } + else + { + if (LineHandler != null) + LineHandler(line); + } + } + while (true); + } + } + catch (Exception ex) + { + } + + Debug("** Connection ended"); + + Running = false; + Connected = false; + } + } +} diff --git a/StompboxRemoteClient/PluginFactory.cs b/StompboxRemoteClient/PluginFactory.cs new file mode 100644 index 0000000..fbcc0c1 --- /dev/null +++ b/StompboxRemoteClient/PluginFactory.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; + +namespace StompboxAPI +{ + public class PluginFactory + { + Dictionary loadedPlugins = new Dictionary(); + + StompboxClient StompboxClient; + + public PluginFactory(StompboxClient StompboxClient) + { + this.StompboxClient = StompboxClient; + } + + public void ClearPlugins() + { + loadedPlugins.Clear(); + } + + public IAudioPlugin GetPlugin(string id) + { + if (!loadedPlugins.ContainsKey(id)) + return null; + + return loadedPlugins[id]; + } + + public IAudioPlugin GetPluginDefinition(string name) + { + if (loadedPlugins.ContainsKey(name)) + return loadedPlugins[name]; + + return StompboxClient.CreatePlugin(name, name); + } + + public IAudioPlugin CreateNewPlugin(string name) + { + return CreatePlugin(name, null); + } + + public IAudioPlugin CreatePlugin(string id) + { + if (id == null) + return null; + + string name = id; + string[] idName = id.Split('_'); + + if (idName.Length > 1) + { + name = idName[0]; + } + + return CreatePlugin(name, id); + } + + public IAudioPlugin CreatePlugin(string name, string id) + { + IAudioPlugin newPlugin = null; + + if (id == null) + { + id = name; + int number = 1; + + while (loadedPlugins.ContainsKey(id)) + { + number++; + + id = name + "_" + number; + } + } + else + { + if (loadedPlugins.ContainsKey(id)) + return loadedPlugins[id]; + } + + newPlugin = StompboxClient.CreatePlugin(name, id); + + loadedPlugins[id] = newPlugin; + + if (newPlugin != null) + { + StompboxClient.Debug("New plugin: " + newPlugin.Name + "[" + newPlugin.ID + "]"); + } + + return newPlugin; + } + + public void ReleasePlugin(IAudioPlugin plugin) + { + if (loadedPlugins.ContainsKey(plugin.ID)) + { + loadedPlugins.Remove(plugin.ID); + } + } + } +} diff --git a/StompboxRemoteClient/ProtocolClient.cs b/StompboxRemoteClient/ProtocolClient.cs new file mode 100644 index 0000000..5e3b860 --- /dev/null +++ b/StompboxRemoteClient/ProtocolClient.cs @@ -0,0 +1,620 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; + +namespace StompboxAPI +{ + public class BPMSync + { + public static List Timings; + + public string Name { get; set; } + public int Numerator { get; set; } + public int Denomenator { get; set; } + + static BPMSync() + { + Timings = new List + { + new BPMSync + { + Name = "Custom", + Numerator = 0, + Denomenator = 0 + }, + new BPMSync + { + Name = "Half Note", + Numerator = 2, + Denomenator = 1 + }, + new BPMSync + { + Name = "Dotted 1/4 Note", + Numerator = 3, + Denomenator = 2 + }, + new BPMSync + { + Name = "1/4 Note", + Numerator = 1, + Denomenator = 1 + }, + new BPMSync + { + Name = "Dotted 1/8th", + Numerator = 3, + Denomenator = 4 + }, + new BPMSync + { + Name = "Triplet of Half", + Numerator = 2, + Denomenator = 3 + }, + new BPMSync + { + Name = "1/8th Note", + Numerator = 1, + Denomenator = 2 + }, + new BPMSync + { + Name = "Dotted 1/16th", + Numerator = 3, + Denomenator = 8 + }, + new BPMSync + { + Name = "Triplet of Quarter", + Numerator = 1, + Denomenator = 3 + }, + new BPMSync + { + Name = "16th Note", + Numerator = 1, + Denomenator = 4 + } + }; + } + } + + public class ProtocolClient + { + public List PluginNames { get; private set; } + + Dictionary pluginDefs = new Dictionary(); + + StompboxClient StompboxClient; + + public ProtocolClient(StompboxClient StompboxClient) + { + this.StompboxClient = StompboxClient; + + PluginNames = new List(); + + // Force invariant locale so we don't have issues parsing decimal places in locales that use ',' + var culture = CultureInfo.InvariantCulture; + CultureInfo.DefaultThreadCurrentCulture = culture; + CultureInfo.DefaultThreadCurrentUICulture = culture; + } + + public IAudioPlugin GetPluginDefinition(string pluginName) + { + if (pluginDefs.ContainsKey(pluginName)) + return pluginDefs[pluginName]; + + return null; + } + + public IAudioPlugin CreateNewPlugin(string pluginName, string pluginID) + { + if (!pluginDefs.ContainsKey(pluginName)) + { + throw new InvalidOperationException(); + } + + IAudioPlugin plugin = new AudioPluginBase { ID = pluginID, Name = pluginName }; + + if (plugin.Parameters.Count == 0) + { + if (pluginDefs.ContainsKey(plugin.Name)) + { + IAudioPlugin pluginDef = pluginDefs[plugin.Name]; + + plugin.Description = pluginDef.Description; + plugin.BackgroundColor = pluginDef.BackgroundColor; + plugin.ForegroundColor = pluginDef.ForegroundColor; + plugin.IsUserSelectable = pluginDef.IsUserSelectable; + plugin.StompboxClient = StompboxClient; + + foreach (PluginParameter paramDef in pluginDef.Parameters) + { + PluginParameter parameter = paramDef.ShallowCopy(); + parameter.Plugin = plugin; + + StompboxClient.SuppressCommandUpdates = true; + parameter.Value = parameter.DefaultValue; + StompboxClient.SuppressCommandUpdates = false; + + plugin.Parameters.Add(parameter); + + if ((parameter.ParameterType == EParameterType.Enum) || (paramDef.ParameterType == EParameterType.File)) + plugin.EnumParameter = parameter; + + if (parameter.CanSyncToHostBPM) + { + parameter.UpdateBPMSync = delegate + { + StompboxClient.SuppressCommandUpdates = true; + + if ((parameter.HostBPMSyncNumerator != 0) && (parameter.HostBPMSyncDenominator != 0)) + { + parameter.Value = ((60.0 / StompboxClient.BPM) * ((double)parameter.HostBPMSyncNumerator / (double)parameter.HostBPMSyncDenominator)) * 1000; + } + else + { + parameter.Value = parameter.DefaultValue; + } + + StompboxClient.SuppressCommandUpdates = false; + }; + } + } + } + } + + return plugin; + } + + public void ReleasePlugin(IAudioPlugin plugin) + { + StompboxClient.PluginFactory.ReleasePlugin(plugin); + + StompboxClient.SendCommand("ReleasePlugin " + plugin.ID); + } + + char[] split = { ' ' }; + + public void HandleCommand(string cmd) + { + StompboxClient.Debug("** CMD: " + cmd); + + try + { + string[] cmdWords = Regex.Matches(cmd, @"(['\""])(?.+?)\1|(?[^ ]+)") + .Cast() + .Select(m => m.Groups["value"].Value) + .ToArray(); + + + //Regex.Matches(cmd, @"[\""].+?[\""]|[^ ]+") + //.Cast() + //.Select(m => m.Value) + //.ToArray(); + + if (cmdWords.Length > 0) + { + switch (cmdWords[0]) + { + case "Presets": + int numPresets = cmdWords.Length - 1; + + if (numPresets > 0) + { + List presets = new List(); + + for (int i = 0; i < numPresets; i++) + { + presets.Add(cmdWords[i + 1]); + } + + presets.Sort(); + + StompboxClient.SetPresetNames(presets); + + } + break; + + case "SetPreset": + if (cmdWords.Length > 1) + { + StompboxClient.MidiCCMap.Clear(); + + StompboxClient.SuppressCommandUpdates = true; + StompboxClient.SetSelectedPreset(cmdWords[1]); + StompboxClient.SuppressCommandUpdates = false; + } + break; + + case "SetChain": + if (cmdWords.Length > 1) + { + List plugins = new List(); + + int numPlugins = cmdWords.Length - 2; + + for (int i = 0; i < numPlugins; i++) + { + string pluginID = cmdWords[i + 2]; + + IAudioPlugin plugin = StompboxClient.PluginFactory.CreatePlugin(pluginID); + + plugins.Add(plugin); + } + + switch (cmdWords[1]) + { + case "Input": + StompboxClient.SetInputChain(plugins); + break; + case "FxLoop": + StompboxClient.SetFxLoop(plugins); + break; + case "Output": + StompboxClient.SetOutputChain(plugins); + break; + } + } + + break; + + case "SetPluginSlot": + if (cmdWords.Length > 2) + { + StompboxClient.SetSlotPlugin(cmdWords[1], cmdWords[2]); + } + break; + + case "SetParam": + if (cmdWords.Length > 3) + { + IAudioPlugin plugin = StompboxClient.PluginFactory.CreatePlugin(cmdWords[1]); + + if (plugin != null) + { + if (cmdWords[2] == "Enabled") + { + int enabled = 0; + + int.TryParse(cmdWords[3], out enabled); + + StompboxClient.SuppressCommandUpdates = true; + plugin.Enabled = (enabled == 1); + StompboxClient.SuppressCommandUpdates = false; + } + else + { + foreach (PluginParameter parameter in plugin.Parameters) + { + if (parameter.Name == cmdWords[2]) + { + double value = parameter.DefaultValue; + + if (parameter.CanSyncToHostBPM) + { + parameter.HostBPMSyncNumerator = parameter.HostBPMSyncDenominator = 0; + } + + if (parameter.ParameterType == EParameterType.Enum) + { + int pos = 0; + + foreach (string enumValue in parameter.EnumValues) + { + if (enumValue == cmdWords[3]) + { + value = pos; + + break; + } + + pos++; + } + } + else if (parameter.ParameterType == EParameterType.File) + { + int pos = 0; + + foreach (string enumValue in parameter.EnumValues) + { + if (enumValue == cmdWords[3]) + { + value = pos; + + break; + } + + pos++; + } + } + else if (parameter.CanSyncToHostBPM && (cmdWords[3].IndexOf('/') != -1)) + { + string[] numDenom = cmdWords[3].Split('/'); + + int numerator = 0; + int denominator = 0; + + if (numDenom.Length > 1) + { + int.TryParse(numDenom[0], out numerator); + int.TryParse(numDenom[1], out denominator); + + parameter.HostBPMSyncNumerator = numerator; + parameter.HostBPMSyncDenominator = denominator; + + if ((numerator != 0) && (denominator != 0)) + { + value = ((60.0 / StompboxClient.BPM) * ((double)parameter.HostBPMSyncNumerator / (double)parameter.HostBPMSyncDenominator)) * 1000; + } + } + } + else + { + double.TryParse(cmdWords[3], out value); + } + + StompboxClient.SuppressCommandUpdates = true; + parameter.Value = value; + StompboxClient.SuppressCommandUpdates = false; + + break; + } + } + } + } + } + break; + + case "SetOutput": + if (cmdWords.Length > 2) + { + IAudioPlugin plugin = StompboxClient.PluginFactory.GetPlugin(cmdWords[1]); + + if (plugin != null) + { + double outputValue = 0; + + if (double.TryParse(cmdWords[2], out outputValue)) + { + plugin.OutputValue = outputValue; + } + } + + break; + } + break; + + case "DSPLoad": + if (cmdWords.Length > 2) + { + float maxLoad = 0; + float.TryParse(cmdWords[1], out maxLoad); + + float minLoad = 0; + float.TryParse(cmdWords[2], out minLoad); + + + StompboxClient.ReportDSPLoad(maxLoad, minLoad); + } + break; + + case "PluginConfig": + if (cmdWords.Length > 1) + { + string pluginName = cmdWords[1]; + + AudioPluginBase pluginDef = new AudioPluginBase { Name = pluginName }; + pluginDefs[pluginName] = pluginDef; + + int numProps = (cmdWords.Length - 2) / 2; + + for (int prop = 0; prop < numProps; prop++) + { + string propName = cmdWords[(prop * 2) + 2]; + string propValue = cmdWords[(prop * 2) + 3]; + + switch (propName) + { + case "BackgroundColor": + pluginDef.BackgroundColor = propValue; + break; + case "ForegroundColor": + pluginDef.ForegroundColor = propValue; + break; + case "IsUserSelectable": + int isSelectable = 0; + + int.TryParse(propValue, out isSelectable); + + pluginDef.IsUserSelectable = (isSelectable == 1); + break; + + case "Description": + pluginDef.Description = propValue; + break; + } + } + + if (pluginDef.IsUserSelectable) + PluginNames.Add(pluginDef.Name); + + } + + break; + case "ParameterConfig": + if (cmdWords.Length > 2) + { + if (pluginDefs.ContainsKey(cmdWords[1])) + { + IAudioPlugin pluginDef = pluginDefs[cmdWords[1]]; + + PluginParameter newParameter = new PluginParameter() { Name = cmdWords[2] }; + + int numProps = (cmdWords.Length - 3) / 2; + + for (int prop = 0; prop < numProps; prop++) + { + string propName = cmdWords[(prop * 2) + 3]; + string propValue = cmdWords[(prop * 2) + 4]; + + switch (propName) + { + case "Type": + EParameterType paramType; + Enum.TryParse(propValue, out paramType); + newParameter.ParameterType = paramType; + break; + case "MinValue": + double minValue = 0; + Double.TryParse(propValue, out minValue); + newParameter.MinValue = minValue; + break; + case "MaxValue": + double maxValue = 0; + Double.TryParse(propValue, out maxValue); + newParameter.MaxValue = maxValue; + break; + case "RangePower": + double rangePower = 0; + Double.TryParse(propValue, out rangePower); + newParameter.RangePower = rangePower; + break; + case "DefaultValue": + double defaultValue = 0; + Double.TryParse(propValue, out defaultValue); + newParameter.DefaultValue = defaultValue; + break; + case "ValueFormat": + newParameter.ValueFormat = propValue; + break; + case "CanSyncToHostBPM": + int canSyncToHostBPM = 0; + int.TryParse(propValue, out canSyncToHostBPM); + newParameter.CanSyncToHostBPM = (canSyncToHostBPM == 1); + break; + case "IsAdvanced": + int isAdvanced = 0; + int.TryParse(propValue, out isAdvanced); + newParameter.IsAdvanced = (isAdvanced == 1); + break; + case "Description": + newParameter.Description = propValue; + break; + } + } + pluginDef.Parameters.Add(newParameter); + } + } + break; + + case "ParameterEnumValues": + if (cmdWords.Length > 2) + { + if (pluginDefs.ContainsKey(cmdWords[1])) + { + IAudioPlugin pluginDef = pluginDefs[cmdWords[1]]; + + foreach (PluginParameter parameter in pluginDef.Parameters) + { + if (parameter.Name == cmdWords[2]) + { + int numEnums = cmdWords.Length - 3; + + parameter.EnumValues = new string[numEnums]; + + for (int i = 0; i < numEnums; i++) + { + parameter.EnumValues[i] = cmdWords[i + 3]; + } + + break; + } + } + } + } + break; + + case "ParameterFileTree": + if (cmdWords.Length > 3) + { + if (pluginDefs.ContainsKey(cmdWords[1])) + { + IAudioPlugin pluginDef = pluginDefs[cmdWords[1]]; + + foreach (PluginParameter parameter in pluginDef.Parameters) + { + if (parameter.Name == cmdWords[2]) + { + parameter.FilePath = cmdWords[3]; + + int numEnums = cmdWords.Length - 4; + + parameter.EnumValues = new string[numEnums]; + + for (int i = 0; i < numEnums; i++) + { + parameter.EnumValues[i] = cmdWords[i + 4]; + } + + break; + } + } + } + } + break; + + case "MapController": + if (cmdWords.Length > 3) + { + StompboxClient.Instance.MidiCCMap.Add(new MidiCCMapEntry() + { + CCNumber = int.Parse(cmdWords[1]), + PluginName = cmdWords[2], + PluginParameter = cmdWords[3] + }); + } + break; + + case "MapModeController": + if (cmdWords.Length > 1) + { + StompboxClient.MidiModeCC = int.Parse(cmdWords[1]); + } + break; + + case "MapStompController": + if (cmdWords.Length > 2) + { + int stomp = int.Parse(cmdWords[1]); + int cc = int.Parse(cmdWords[2]); + + StompboxClient.MidiStompCCMap[cc] = stomp; + } + break; + + case "EndProgram": + StompboxClient.UpdateUI(); + break; + + case "Ok": + break; + + default: // Unrecognized command + break; + } + } + } + catch (Exception ex) + { + // In case we get an exception after setting it + StompboxClient.SuppressCommandUpdates = false; + + StompboxClient.Debug("Exception: " + ex.ToString()); + } + } + } +} diff --git a/StompboxRemoteClient/RemoteClient.cs b/StompboxRemoteClient/RemoteClient.cs new file mode 100644 index 0000000..d493205 --- /dev/null +++ b/StompboxRemoteClient/RemoteClient.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace StompboxAPI +{ + public class RemoteClient : StompboxClient + { + public PluginFactory PluginFactory { get; set; } + public bool SuppressCommandUpdates { get; set; } + + Dictionary slotPlugins = new Dictionary(); + + NetworkClient networkClient; + ProtocolClient protocolClient; + + public bool Connected + { + get + { + return networkClient.Connected; + } + } + + public override int CurrentPresetIndex + { + get => base.CurrentPresetIndex; + + set + { + base.CurrentPresetIndex = value; + + if (!SuppressCommandUpdates) + { + SendCommand("LoadPreset " + PresetNames[currentPresetIndex]); + } + } + } + + public RemoteClient() + : base() + { + PluginFactory = new PluginFactory(this); + + InClientMode = true; + + protocolClient = new ProtocolClient(this); + + networkClient = new NetworkClient(); + networkClient.LineHandler = HandleCommand; + networkClient.DebugAction = delegate (string debug) + { + Debug(debug); + }; + } + + Action connectCallback; + + public void Connect(string serverName, int port, Action connectCallback) + { + if (InClientMode) + { + this.connectCallback = connectCallback; + + Debug("Connect to server: " + serverName); + + networkClient.Start(serverName, 24639, ConnectCallback); + } + } + + public void Disconnect() + { + if (InClientMode) + { + networkClient.Stop(); + } + } + + void ConnectCallback(bool result) + { + if (result) + { + Debug("Connected"); + + networkClient.LineHandler = HandleCommand; + + RequestConfigDump(); + + connectCallback(true); + } + else + { + Debug("Connect failed"); + + connectCallback(false); + } + } + + public override void UpdatePresets() + { + base.UpdatePresets(); + + SendCommand("List Presets"); + } + + public void RequestConfigDump() + { + //Thread.Sleep(500); + + SendCommand("Dump Config"); + SendCommand("List Presets"); + + if (InClientMode) + SendCommand("PluginOutputOn"); + + UpdateProgram(); + } + + public override IEnumerable GetAllPluginNames() + { + return protocolClient.PluginNames; + } + + public override IAudioPlugin GetPluginDefinition(string pluginName) + { + return protocolClient.GetPluginDefinition(pluginName); + } + + public override IAudioPlugin CreatePlugin(string pluginName, string pluginID) + { + return protocolClient.CreateNewPlugin(pluginName, pluginID); + } + + public void ReleasePlugin(IAudioPlugin plugin) + { + protocolClient.ReleasePlugin(plugin); + } + + string[] lineSeparator = new string[] { "\r", "\n" }; + + public void HandleCommand(string commandStr) + { + if (InClientMode) + { + string[] commands = commandStr.Split(lineSeparator, 0); + + foreach (string command in commands) + { + if (!string.IsNullOrWhiteSpace(command)) + { + protocolClient.HandleCommand(command); + } + } + } + } + + public void SendCommand(string command) + { + Debug("Send command: " + command); + + if (SuppressCommandUpdates) + { + Debug("*** Command suppressed"); + } + else + { + if (networkClient != null) + { + if (!networkClient.Connected) + { + Debug("*** Network client not connected"); + } + else + { + Debug("Actullay sending"); + networkClient.SendData(command + "\r\n"); + } + } + } + } + + public void SetSlotPlugin(string slotName, string pluginID) + { + slotPlugins[slotName] = pluginID; + } + + protected override IAudioPlugin CreateSlotPlugin(string slotName, string defaultPlugin) + { + string pluginID = null; + + slotPlugins.TryGetValue(slotName, out pluginID); + + if (pluginID == null) + { + pluginID = defaultPlugin; + + string cmd = "SetPluginSlot " + slotName + " " + pluginID; + + SendCommand(cmd); + } + + return PluginFactory.CreatePlugin(pluginID); + } + } +} diff --git a/StompboxRemoteClient/StompboxRemoteClient.csproj b/StompboxRemoteClient/StompboxRemoteClient.csproj new file mode 100644 index 0000000..fc69937 --- /dev/null +++ b/StompboxRemoteClient/StompboxRemoteClient.csproj @@ -0,0 +1,11 @@ + + + + net8.0 + enable + enable + + + + + diff --git a/StompboxUI.sln b/StompboxUI.sln index e59aa42..9895282 100644 --- a/StompboxUI.sln +++ b/StompboxUI.sln @@ -46,6 +46,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StompboxAPI", "StompboxAPI\ EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StompboxAPIBase", "StompboxAPIBase\StompboxAPIBase.shproj", "{EF794465-7976-4AD3-B2CC-DEA7A85A80AE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StompboxRemoteClient", "StompboxRemoteClient\StompboxRemoteClient.csproj", "{5B8B23B6-09DC-4FC6-90FC-033D1145374E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -447,6 +449,30 @@ Global {FA850CE0-FF06-4D05-B582-CA4F4613E416}.RelWithDebInfo|x64.Build.0 = Debug|Any CPU {FA850CE0-FF06-4D05-B582-CA4F4613E416}.RelWithDebInfo|x86.ActiveCfg = Debug|Any CPU {FA850CE0-FF06-4D05-B582-CA4F4613E416}.RelWithDebInfo|x86.Build.0 = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Debug|x64.Build.0 = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Debug|x86.Build.0 = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.MinSizeRel|Any CPU.ActiveCfg = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.MinSizeRel|Any CPU.Build.0 = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.MinSizeRel|x64.ActiveCfg = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.MinSizeRel|x64.Build.0 = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.MinSizeRel|x86.ActiveCfg = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.MinSizeRel|x86.Build.0 = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Release|Any CPU.Build.0 = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Release|x64.ActiveCfg = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Release|x64.Build.0 = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Release|x86.ActiveCfg = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.Release|x86.Build.0 = Release|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.RelWithDebInfo|Any CPU.ActiveCfg = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.RelWithDebInfo|Any CPU.Build.0 = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.RelWithDebInfo|x64.ActiveCfg = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.RelWithDebInfo|x64.Build.0 = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.RelWithDebInfo|x86.ActiveCfg = Debug|Any CPU + {5B8B23B6-09DC-4FC6-90FC-033D1145374E}.RelWithDebInfo|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -463,6 +489,7 @@ Global StompboxShared\StompboxShared.projitems*{1ff3b5c3-44e2-464a-a15f-55ad4b41cea0}*SharedItemsImports = 5 StompboxShared\StompboxShared.projitems*{428f3d8a-d221-4f75-b1d6-f409b91fb7df}*SharedItemsImports = 5 StompboxShared\StompboxShared.projitems*{49305251-662e-459a-9adf-9866ceb6c4d2}*SharedItemsImports = 5 + StompboxAPIBase\StompboxAPIBase.projitems*{5b8b23b6-09dc-4fc6-90fc-033d1145374e}*SharedItemsImports = 5 Dependencies\UILayout\UILayout\UILayout.projitems*{7da2b397-a39f-4e5b-96ed-f206329982c9}*SharedItemsImports = 13 Dependencies\UILayout\UILayout.MonoGame\UILayout.MonoGame.projitems*{8f0c191b-da0c-4af6-9e0b-c325d48a1a42}*SharedItemsImports = 5 Dependencies\UILayout\UILayout\UILayout.projitems*{8f0c191b-da0c-4af6-9e0b-c325d48a1a42}*SharedItemsImports = 5