Windows application with mock hardware layer

This commit is contained in:
Mark van Renswoude 2021-02-19 17:42:37 +01:00
parent f42dd4d02b
commit 072e73df88
29 changed files with 12530 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.vs/
bin/
obj/
*.user

24
LICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

6
Windows/App.config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

214
Windows/Forms/SettingsForm.Designer.cs generated Normal file
View File

@ -0,0 +1,214 @@

namespace MassiveKnob.Forms
{
partial class SettingsForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SettingsForm));
this.NotifyIcon = new System.Windows.Forms.NotifyIcon(this.components);
this.NotifyIconMenu = new System.Windows.Forms.ContextMenuStrip(this.components);
this.SettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.QuitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.CommunicationGroupbox = new System.Windows.Forms.GroupBox();
this.SerialPortStatusLabel = new System.Windows.Forms.Label();
this.SerialPortCombobox = new System.Windows.Forms.ComboBox();
this.SerialPortLabel = new System.Windows.Forms.Label();
this.DevicesGroupbox = new System.Windows.Forms.GroupBox();
this.DevicesPanel = new System.Windows.Forms.Panel();
this.DeviceCountUnknownLabel = new System.Windows.Forms.Label();
this.CloseButton = new System.Windows.Forms.Button();
this.NotifyIconMenu.SuspendLayout();
this.CommunicationGroupbox.SuspendLayout();
this.DevicesGroupbox.SuspendLayout();
this.DevicesPanel.SuspendLayout();
this.SuspendLayout();
//
// NotifyIcon
//
this.NotifyIcon.ContextMenuStrip = this.NotifyIconMenu;
this.NotifyIcon.Icon = ((System.Drawing.Icon)(resources.GetObject("NotifyIcon.Icon")));
this.NotifyIcon.Text = "Massive Knob";
this.NotifyIcon.Visible = true;
this.NotifyIcon.DoubleClick += new System.EventHandler(this.NotifyIcon_DoubleClick);
//
// NotifyIconMenu
//
this.NotifyIconMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.SettingsToolStripMenuItem,
this.QuitToolStripMenuItem});
this.NotifyIconMenu.Name = "NotifyIconMenu";
this.NotifyIconMenu.Size = new System.Drawing.Size(121, 48);
//
// SettingsToolStripMenuItem
//
this.SettingsToolStripMenuItem.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
this.SettingsToolStripMenuItem.Name = "SettingsToolStripMenuItem";
this.SettingsToolStripMenuItem.Size = new System.Drawing.Size(120, 22);
this.SettingsToolStripMenuItem.Text = "&Settings";
this.SettingsToolStripMenuItem.Click += new System.EventHandler(this.SettingsToolStripMenuItem_Click);
//
// QuitToolStripMenuItem
//
this.QuitToolStripMenuItem.Name = "QuitToolStripMenuItem";
this.QuitToolStripMenuItem.Size = new System.Drawing.Size(120, 22);
this.QuitToolStripMenuItem.Text = "&Quit";
this.QuitToolStripMenuItem.Click += new System.EventHandler(this.QuitToolStripMenuItem_Click);
//
// CommunicationGroupbox
//
this.CommunicationGroupbox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.CommunicationGroupbox.Controls.Add(this.SerialPortStatusLabel);
this.CommunicationGroupbox.Controls.Add(this.SerialPortCombobox);
this.CommunicationGroupbox.Controls.Add(this.SerialPortLabel);
this.CommunicationGroupbox.Location = new System.Drawing.Point(12, 12);
this.CommunicationGroupbox.Name = "CommunicationGroupbox";
this.CommunicationGroupbox.Size = new System.Drawing.Size(455, 52);
this.CommunicationGroupbox.TabIndex = 1;
this.CommunicationGroupbox.TabStop = false;
this.CommunicationGroupbox.Text = " Communication ";
//
// SerialPortStatusLabel
//
this.SerialPortStatusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.SerialPortStatusLabel.AutoEllipsis = true;
this.SerialPortStatusLabel.Location = new System.Drawing.Point(261, 22);
this.SerialPortStatusLabel.Name = "SerialPortStatusLabel";
this.SerialPortStatusLabel.Size = new System.Drawing.Size(188, 18);
this.SerialPortStatusLabel.TabIndex = 2;
this.SerialPortStatusLabel.Text = "[runtime]";
//
// SerialPortCombobox
//
this.SerialPortCombobox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.SerialPortCombobox.FormattingEnabled = true;
this.SerialPortCombobox.Location = new System.Drawing.Point(107, 19);
this.SerialPortCombobox.Name = "SerialPortCombobox";
this.SerialPortCombobox.Size = new System.Drawing.Size(148, 21);
this.SerialPortCombobox.TabIndex = 1;
this.SerialPortCombobox.SelectedIndexChanged += new System.EventHandler(this.SerialPortCombobox_SelectedIndexChanged);
//
// SerialPortLabel
//
this.SerialPortLabel.AutoSize = true;
this.SerialPortLabel.Location = new System.Drawing.Point(10, 22);
this.SerialPortLabel.Name = "SerialPortLabel";
this.SerialPortLabel.Size = new System.Drawing.Size(54, 13);
this.SerialPortLabel.TabIndex = 0;
this.SerialPortLabel.Text = "Serial port";
//
// DevicesGroupbox
//
this.DevicesGroupbox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.DevicesGroupbox.Controls.Add(this.DevicesPanel);
this.DevicesGroupbox.Location = new System.Drawing.Point(12, 70);
this.DevicesGroupbox.Name = "DevicesGroupbox";
this.DevicesGroupbox.Size = new System.Drawing.Size(455, 57);
this.DevicesGroupbox.TabIndex = 2;
this.DevicesGroupbox.TabStop = false;
this.DevicesGroupbox.Text = " Audio devices ";
//
// DevicesPanel
//
this.DevicesPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.DevicesPanel.Controls.Add(this.DeviceCountUnknownLabel);
this.DevicesPanel.Location = new System.Drawing.Point(13, 19);
this.DevicesPanel.Name = "DevicesPanel";
this.DevicesPanel.Size = new System.Drawing.Size(436, 32);
this.DevicesPanel.TabIndex = 1;
//
// DeviceCountUnknownLabel
//
this.DeviceCountUnknownLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.DeviceCountUnknownLabel.Location = new System.Drawing.Point(0, 0);
this.DeviceCountUnknownLabel.Name = "DeviceCountUnknownLabel";
this.DeviceCountUnknownLabel.Size = new System.Drawing.Size(436, 32);
this.DeviceCountUnknownLabel.TabIndex = 1;
this.DeviceCountUnknownLabel.Text = "Insert Massive Knob to continue...";
this.DeviceCountUnknownLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// CloseButton
//
this.CloseButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.CloseButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.CloseButton.Location = new System.Drawing.Point(392, 133);
this.CloseButton.Name = "CloseButton";
this.CloseButton.Size = new System.Drawing.Size(75, 23);
this.CloseButton.TabIndex = 3;
this.CloseButton.Text = "Close";
this.CloseButton.UseVisualStyleBackColor = true;
this.CloseButton.Click += new System.EventHandler(this.CloseButton_Click);
//
// SettingsForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.CloseButton;
this.ClientSize = new System.Drawing.Size(479, 168);
this.Controls.Add(this.CloseButton);
this.Controls.Add(this.DevicesGroupbox);
this.Controls.Add(this.CommunicationGroupbox);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "SettingsForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Massive Knob - Settings";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.SettingsForm_FormClosing);
this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.SettingsForm_FormClosed);
this.NotifyIconMenu.ResumeLayout(false);
this.CommunicationGroupbox.ResumeLayout(false);
this.CommunicationGroupbox.PerformLayout();
this.DevicesGroupbox.ResumeLayout(false);
this.DevicesPanel.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.NotifyIcon NotifyIcon;
private System.Windows.Forms.ContextMenuStrip NotifyIconMenu;
private System.Windows.Forms.ToolStripMenuItem QuitToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem SettingsToolStripMenuItem;
private System.Windows.Forms.GroupBox CommunicationGroupbox;
private System.Windows.Forms.Label SerialPortStatusLabel;
private System.Windows.Forms.ComboBox SerialPortCombobox;
private System.Windows.Forms.Label SerialPortLabel;
private System.Windows.Forms.GroupBox DevicesGroupbox;
private System.Windows.Forms.Button CloseButton;
private System.Windows.Forms.Panel DevicesPanel;
private System.Windows.Forms.Label DeviceCountUnknownLabel;
}
}

View File

@ -0,0 +1,363 @@
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using MassiveKnob.Hardware;
using MassiveKnob.Settings;
using MassiveKnob.UserControls;
using Nito.AsyncEx;
namespace MassiveKnob.Forms
{
public partial class SettingsForm : Form, IMassiveKnobHardwareObserver
{
private readonly IAudioDeviceManager audioDeviceManager;
private readonly IMassiveKnobHardwareFactory massiveKnobHardwareFactory;
private readonly List<KnobDeviceControl> knobDeviceControls = new List<KnobDeviceControl>();
private bool loading = true;
private IMassiveKnobHardware hardware;
private IAudioDevice[] devices;
private Settings.Settings settings;
private readonly AsyncLock saveSettingsLock = new AsyncLock();
private readonly AsyncLock setVolumeLock = new AsyncLock();
private bool startupVisibleCalled;
private bool closing;
public SettingsForm(IAudioDeviceManagerFactory audioDeviceManagerFactory, IMassiveKnobHardwareFactory massiveKnobHardwareFactory)
{
audioDeviceManager = audioDeviceManagerFactory.Create();
this.massiveKnobHardwareFactory = massiveKnobHardwareFactory;
InitializeComponent();
SerialPortStatusLabel.Text = Strings.StatusNotConnected;
Task.Run(async () =>
{
await LoadSettings();
await Task.WhenAll(
LoadSerialPorts(),
LoadAudioDevices()
);
loading = false;
await Connect();
}).ContinueWith(t =>
{
if (t.IsFaulted && t.Exception != null)
SafeCall(() => throw t.Exception);
});
}
private void SafeCall(Action action)
{
if (InvokeRequired)
Invoke(action);
else
action();
}
private Task LoadSerialPorts()
{
var portNames = SerialPort.GetPortNames();
SafeCall(() =>
{
SerialPortCombobox.BeginUpdate();
try
{
SerialPortCombobox.Items.Clear();
foreach (var portName in portNames)
{
var itemIndex = SerialPortCombobox.Items.Add(portName);
if (portName == settings.SerialPort)
SerialPortCombobox.SelectedIndex = itemIndex;
}
}
finally
{
SerialPortCombobox.EndUpdate();
}
});
return Task.CompletedTask;
}
private async Task LoadSettings()
{
var newSettings = await SettingsJsonSerializer.Deserialize();
SafeCall(() => SetSettings(newSettings));
}
private void SaveSettings()
{
if (settings == null)
return;
Task.Run(async () =>
{
using (await saveSettingsLock.LockAsync())
{
await SettingsJsonSerializer.Serialize(settings);
}
});
}
private async Task Connect()
{
string serialPort = null;
SafeCall(() =>
{
SerialPortStatusLabel.Text = Strings.StatusConnecting;
serialPort = (string)SerialPortCombobox.SelectedItem;
});
if (string.IsNullOrEmpty(serialPort))
return;
if (hardware != null)
{
hardware.DetachObserver(this);
await hardware.Disconnect();
}
hardware = massiveKnobHardwareFactory.Create(serialPort);
hardware.AttachObserver(this);
await hardware.TryConnect();
}
private async Task LoadAudioDevices()
{
var newDevices = await audioDeviceManager.GetDevices();
SafeCall(() => SetDevices(newDevices));
}
private void SetSettings(Settings.Settings value)
{
if (value == null)
return;
SerialPortCombobox.SelectedItem = value.SerialPort;
// No need to update the knob device user controls, as they are not loaded yet
settings = value;
}
private void SetDevices(IEnumerable<IAudioDevice> value)
{
devices = value.ToArray();
foreach (var knobDeviceControl in knobDeviceControls)
knobDeviceControl.SetDevices(devices);
}
private void SetKnobCount(int count)
{
if (count == knobDeviceControls.Count)
return;
SuspendLayout();
try
{
DeviceCountUnknownLabel.Visible = count == 0;
if (knobDeviceControls.Count > count)
{
for (var i = count; i < knobDeviceControls.Count; i++)
knobDeviceControls[i].Dispose();
knobDeviceControls.RemoveRange(count, knobDeviceControls.Count - count);
}
for (var i = knobDeviceControls.Count; i < count; i++)
{
var knobDeviceControl = new KnobDeviceControl
{
Left = 0,
Width = DevicesPanel.Width
};
knobDeviceControl.Top = i * knobDeviceControl.Height;
knobDeviceControl.Parent = DevicesPanel;
knobDeviceControl.SetKnobIndex(i);
knobDeviceControl.SetDevices(devices);
if (i < settings.Knobs.Count)
knobDeviceControl.SetDeviceId(settings.Knobs[i].DeviceId);
knobDeviceControl.OnDeviceChanged += (sender, args) =>
{
while (settings.Knobs.Count - 1 < args.KnobIndex)
settings.Knobs.Add(new Settings.Settings.KnobSettings());
settings.Knobs[args.KnobIndex].DeviceId = args.DeviceId;
SaveSettings();
};
knobDeviceControls.Add(knobDeviceControl);
}
var expectedHeight = knobDeviceControls.Count > 0
? knobDeviceControls[0].Height * count
: DeviceCountUnknownLabel.Height;
if (expectedHeight == DevicesPanel.Height)
return;
var diff = expectedHeight - DevicesPanel.Height;
Height += diff;
Top -= diff / 2;
}
finally
{
ResumeLayout();
}
}
protected override void SetVisibleCore(bool value)
{
// Prevent the form from showing at startup
if (!startupVisibleCalled)
startupVisibleCalled = true;
else
base.SetVisibleCore(value);
}
private void Settings()
{
Show();
}
private void Quit()
{
closing = true;
Close();
}
public void Connected(int knobCount)
{
SafeCall(() =>
{
SerialPortStatusLabel.Text = Strings.StatusConnected;
SetKnobCount(knobCount);
});
}
public void Disconnected()
{
SafeCall(() =>
{
SerialPortStatusLabel.Text = Strings.StatusNotConnected;
});
}
public void VolumeChanged(int knob, int volume)
{
if (knob >= settings.Knobs.Count)
return;
if (!settings.Knobs[knob].DeviceId.HasValue)
return;
var deviceId = settings.Knobs[knob].DeviceId.Value;
Task.Run(async () =>
{
using (await setVolumeLock.LockAsync())
{
var device = await audioDeviceManager.GetDeviceById(deviceId);
if (device != null)
await device.SetVolume(volume);
}
});
}
private void SettingsForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (closing)
return;
Hide();
e.Cancel = true;
}
private void SettingsForm_FormClosed(object sender, FormClosedEventArgs e)
{
foreach (var knobDeviceControl in knobDeviceControls)
knobDeviceControl.Dispose();
knobDeviceControls.Clear();
hardware?.DetachObserver(this);
hardware?.Disconnect().GetAwaiter().GetResult();
audioDeviceManager?.Dispose();
}
private void NotifyIcon_DoubleClick(object sender, EventArgs e)
{
Settings();
}
private void QuitToolStripMenuItem_Click(object sender, EventArgs e)
{
Quit();
}
private void SettingsToolStripMenuItem_Click(object sender, EventArgs e)
{
Settings();
}
private void CloseButton_Click(object sender, EventArgs e)
{
Close();
}
private void SerialPortCombobox_SelectedIndexChanged(object sender, EventArgs e)
{
if (loading || (string)SerialPortCombobox.SelectedItem == settings.SerialPort)
return;
settings.SerialPort = (string) SerialPortCombobox.SelectedItem;
SaveSettings();
Task.Run(Connect);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MassiveKnob.Hardware
{
public abstract class AbstractMassiveKnobHardware : IMassiveKnobHardware
{
protected ObserverProxy Observers = new ObserverProxy();
public void AttachObserver(IMassiveKnobHardwareObserver observer)
{
Observers.AttachObserver(observer);
}
public void DetachObserver(IMassiveKnobHardwareObserver observer)
{
Observers.DetachObserver(observer);
}
public abstract Task TryConnect();
public abstract Task Disconnect();
public class ObserverProxy : IMassiveKnobHardwareObserver
{
private readonly List<IMassiveKnobHardwareObserver> observers = new List<IMassiveKnobHardwareObserver>();
public void AttachObserver(IMassiveKnobHardwareObserver observer)
{
observers.Add(observer);
}
public void DetachObserver(IMassiveKnobHardwareObserver observer)
{
observers.Remove(observer);
}
public void Connected(int knobCount)
{
foreach (var observer in observers)
observer.Connected(knobCount);
}
public void Disconnected()
{
foreach (var observer in observers)
observer.Disconnected();
}
public void VolumeChanged(int knob, int volume)
{
foreach (var observer in observers)
observer.VolumeChanged(knob, volume);
}
}
}
}

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudioSwitcher.AudioApi;
using AudioSwitcher.AudioApi.CoreAudio;
namespace MassiveKnob.Hardware
{
public class CoreAudioDeviceManager : IAudioDeviceManager
{
private readonly Lazy<CoreAudioController> audioController = new Lazy<CoreAudioController>();
private List<IAudioDevice> devices;
public void Dispose()
{
if (audioController.IsValueCreated)
audioController.Value.Dispose();
}
public async Task<IEnumerable<IAudioDevice>> GetDevices()
{
return devices ?? (devices = (await audioController.Value.GetPlaybackDevicesAsync())
.Select(device => new AudioDevice(device) as IAudioDevice)
.ToList());
}
public Task<IAudioDevice> GetDeviceById(Guid deviceId)
{
return Task.FromResult(devices?.FirstOrDefault(device => device.Id == deviceId));
}
private class AudioDevice : IAudioDevice
{
private readonly IDevice device;
public Guid Id { get; }
public string DisplayName { get; }
public AudioDevice(IDevice device)
{
this.device = device;
Id = device.Id;
string displayFormat;
if ((device.State & DeviceState.Disabled) != 0)
displayFormat = Strings.DeviceDisplayNameDisabled;
else if ((device.State & DeviceState.Unplugged) != 0)
displayFormat = Strings.DeviceDisplayNameUnplugged;
else if ((device.State & DeviceState.NotPresent) != 0)
displayFormat = Strings.DeviceDisplayNameNotPresent;
else if ((device.State & DeviceState.Active) == 0)
displayFormat = Strings.DeviceDisplayNameInactive;
else
displayFormat = Strings.DeviceDisplayNameActive;
DisplayName = string.Format(displayFormat, device.FullName);
}
public Task SetVolume(int volume)
{
return device.SetVolumeAsync(volume);
}
}
}
public class CoreAudioDeviceManagerFactory : IAudioDeviceManagerFactory
{
public IAudioDeviceManager Create()
{
return new CoreAudioDeviceManager();
}
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MassiveKnob.Hardware
{
public interface IAudioDevice
{
Guid Id { get; }
string DisplayName { get; }
Task SetVolume(int volume);
}
public interface IAudioDeviceManager : IDisposable
{
Task<IEnumerable<IAudioDevice>> GetDevices();
Task<IAudioDevice> GetDeviceById(Guid deviceId);
}
public interface IAudioDeviceManagerFactory
{
IAudioDeviceManager Create();
}
}

View File

@ -0,0 +1,30 @@
using System.Threading.Tasks;
namespace MassiveKnob.Hardware
{
public interface IMassiveKnobHardwareObserver
{
void Connected(int knobCount);
void Disconnected();
void VolumeChanged(int knob, int volume);
// void ButtonPress(int index); -- for switching the active device, TBD
}
public interface IMassiveKnobHardware
{
void AttachObserver(IMassiveKnobHardwareObserver observer);
void DetachObserver(IMassiveKnobHardwareObserver observer);
Task TryConnect();
Task Disconnect();
// Task SetActiveKnob(int knob); -- for providing LED feedback when switching the active device, TBD
}
public interface IMassiveKnobHardwareFactory
{
IMassiveKnobHardware Create(string serialPort);
}
}

View File

@ -0,0 +1,70 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MassiveKnob.Hardware
{
public class MockMassiveKnobHardware : AbstractMassiveKnobHardware
{
private readonly int knobCount;
private readonly TimeSpan volumeChangeInterval;
private readonly int maxVolume;
private Timer changeVolumeTimer;
private readonly Random random = new Random();
public MockMassiveKnobHardware(int knobCount, TimeSpan volumeChangeInterval, int maxVolume)
{
this.knobCount = knobCount;
this.volumeChangeInterval = volumeChangeInterval;
this.maxVolume = maxVolume;
}
public override async Task TryConnect()
{
if (changeVolumeTimer != null)
return;
await Task.Delay(2000);
Observers.Connected(knobCount);
changeVolumeTimer = new Timer(
state =>
{
Observers.VolumeChanged(random.Next(0, knobCount), random.Next(0, maxVolume));
},
null,
volumeChangeInterval,
volumeChangeInterval);
}
public override Task Disconnect()
{
changeVolumeTimer?.Dispose();
return Task.CompletedTask;
}
}
public class MockMassiveKnobHardwareFactory : IMassiveKnobHardwareFactory
{
private readonly int knobCount;
private readonly TimeSpan volumeChangeInterval;
private readonly int maxVolume;
public MockMassiveKnobHardwareFactory(int knobCount, TimeSpan volumeChangeInterval, int maxVolume)
{
this.knobCount = knobCount;
this.volumeChangeInterval = volumeChangeInterval;
this.maxVolume = maxVolume;
}
public IMassiveKnobHardware Create(string serialPort)
{
return new MockMassiveKnobHardware(knobCount, volumeChangeInterval, maxVolume);
}
}
}

BIN
Windows/MainIcon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

134
Windows/MassiveKnob.csproj Normal file
View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{73130EC7-49B3-40AD-8367-1095C0F41905}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>MassiveKnob</RootNamespace>
<AssemblyName>MassiveKnob</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>MainIcon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<StartupObject>MassiveKnob.Program</StartupObject>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Forms\SettingsForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Forms\SettingsForm.Designer.cs">
<DependentUpon>SettingsForm.cs</DependentUpon>
</Compile>
<Compile Include="Hardware\AbstractMassiveKnobHardware.cs" />
<Compile Include="Hardware\CoreAudioDeviceManager.cs" />
<Compile Include="Hardware\IAudioDeviceManager.cs" />
<Compile Include="Hardware\IMassiveKnobHardware.cs" />
<Compile Include="Hardware\MockMassiveKnobHardware.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Settings\Settings.cs" />
<Compile Include="Settings\SettingsJsonSerializer.cs" />
<Compile Include="Strings.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Strings.resx</DependentUpon>
</Compile>
<Compile Include="UserControls\KnobDeviceControl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="UserControls\KnobDeviceControl.Designer.cs">
<DependentUpon>KnobDeviceControl.cs</DependentUpon>
</Compile>
<EmbeddedResource Include="Forms\SettingsForm.resx">
<DependentUpon>SettingsForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<EmbeddedResource Include="Strings.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AudioSwitcher.AudioApi">
<Version>4.0.0-alpha5</Version>
</PackageReference>
<PackageReference Include="AudioSwitcher.AudioApi.CoreAudio">
<Version>4.0.0-alpha5</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.3</Version>
</PackageReference>
<PackageReference Include="Nito.AsyncEx">
<Version>5.1.0</Version>
</PackageReference>
<PackageReference Include="SimpleInjector">
<Version>5.2.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Content Include="MainIcon.ico" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

25
Windows/MassiveKnob.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31005.135
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob", "MassiveKnob.csproj", "{73130EC7-49B3-40AD-8367-1095C0F41905}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{73130EC7-49B3-40AD-8367-1095C0F41905}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{73130EC7-49B3-40AD-8367-1095C0F41905}.Debug|Any CPU.Build.0 = Debug|Any CPU
{73130EC7-49B3-40AD-8367-1095C0F41905}.Release|Any CPU.ActiveCfg = Release|Any CPU
{73130EC7-49B3-40AD-8367-1095C0F41905}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3063D7FD-6457-45B0-9ED8-620BDF605773}
EndGlobalSection
EndGlobal

43
Windows/Program.cs Normal file
View File

@ -0,0 +1,43 @@
using System;
using System.Windows.Forms;
using MassiveKnob.Forms;
using MassiveKnob.Hardware;
using SimpleInjector;
using SimpleInjector.Diagnostics;
namespace MassiveKnob
{
public static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
public static void Main()
{
var container = BuildContainer();
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(container.GetInstance<SettingsForm>());
}
private static Container BuildContainer()
{
var container = new Container();
container.Options.EnableAutoVerification = false;
container.Register<SettingsForm>();
container.GetRegistration(typeof(SettingsForm))?.Registration
.SuppressDiagnosticWarning(DiagnosticType.DisposableTransientComponent, "Windows Form implements IDisposable");
container.Register<IAudioDeviceManagerFactory, CoreAudioDeviceManagerFactory>();
// For testing without the hardware:
container.Register<IMassiveKnobHardwareFactory>(() => new MockMassiveKnobHardwareFactory(3, TimeSpan.FromSeconds(1), 25));
return container;
}
}
}

View File

@ -0,0 +1,35 @@
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MassiveKnob")]
[assembly: AssemblyDescription("Physical controls for Windows audio devices")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MassiveKnob")]
[assembly: AssemblyCopyright("Copyright © 2021 Mark van Renswoude")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("73130ec7-49b3-40ad-8367-1095c0f41905")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

70
Windows/Properties/Resources.Designer.cs generated Normal file
View File

@ -0,0 +1,70 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace MassiveKnob.Properties
{
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MassiveKnob.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

29
Windows/Properties/Settings.Designer.cs generated Normal file
View File

@ -0,0 +1,29 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace MassiveKnob.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
return defaultInstance;
}
}
}
}

View File

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

4708
Windows/Resources/Icon.ai Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
namespace MassiveKnob.Settings
{
public class Settings
{
public string SerialPort { get; set; }
public List<KnobSettings> Knobs { get; set; }
public static Settings Default()
{
return new Settings
{
Knobs = new List<KnobSettings>()
};
}
public class KnobSettings
{
public Guid? DeviceId { get; set; }
}
}
}

View File

@ -0,0 +1,106 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace MassiveKnob.Settings
{
public static class SettingsJsonSerializer
{
public static string GetDefaultFilename()
{
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob");
Directory.CreateDirectory(path);
return Path.Combine(path, "Settings.json");
}
public static Task Serialize(Settings settings)
{
return Serialize(settings, GetDefaultFilename());
}
public static async Task Serialize(Settings settings, string filename)
{
var serializedSettings = SerializedSettings.FromSettings(settings);
var json = JsonConvert.SerializeObject(serializedSettings);
using (var stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read, 4096, true))
using (var streamWriter = new StreamWriter(stream, Encoding.UTF8))
{
await streamWriter.WriteAsync(json);
await streamWriter.FlushAsync();
}
}
public static Task<Settings> Deserialize()
{
return Deserialize(GetDefaultFilename());
}
public static async Task<Settings> Deserialize(string filename)
{
if (!File.Exists(filename))
return Settings.Default();
string json;
using (var stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, true))
using (var streamReader = new StreamReader(stream, Encoding.UTF8))
{
json = await streamReader.ReadToEndAsync();
}
if (string.IsNullOrEmpty(json))
return Settings.Default();
var serializedSettings = JsonConvert.DeserializeObject<SerializedSettings>(json);
return serializedSettings.ToSettings();
}
private class SerializedSettings
{
// ReSharper disable MemberCanBePrivate.Local - used for JSON serialization
public string SerialPort;
public SerializedKnobSettings[] Knobs;
// ReSharper restore MemberCanBePrivate.Local
public static SerializedSettings FromSettings(Settings settings)
{
return new SerializedSettings
{
SerialPort = settings.SerialPort,
Knobs = settings.Knobs.Select(knob => new SerializedKnobSettings
{
DeviceId = knob.DeviceId
}).ToArray()
};
}
public Settings ToSettings()
{
return new Settings
{
SerialPort = SerialPort,
Knobs = Knobs.Select(knob => new Settings.KnobSettings
{
DeviceId = knob.DeviceId
}).ToList()
};
}
public class SerializedKnobSettings
{
public Guid? DeviceId;
}
}
}
}

144
Windows/Strings.Designer.cs generated Normal file
View File

@ -0,0 +1,144 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace MassiveKnob {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Strings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Strings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MassiveKnob.Strings", typeof(Strings).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to {0}.
/// </summary>
internal static string DeviceDisplayNameActive {
get {
return ResourceManager.GetString("DeviceDisplayNameActive", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} (Disabled).
/// </summary>
internal static string DeviceDisplayNameDisabled {
get {
return ResourceManager.GetString("DeviceDisplayNameDisabled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} (Inactive).
/// </summary>
internal static string DeviceDisplayNameInactive {
get {
return ResourceManager.GetString("DeviceDisplayNameInactive", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} (Not present).
/// </summary>
internal static string DeviceDisplayNameNotPresent {
get {
return ResourceManager.GetString("DeviceDisplayNameNotPresent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} (Unplugged).
/// </summary>
internal static string DeviceDisplayNameUnplugged {
get {
return ResourceManager.GetString("DeviceDisplayNameUnplugged", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Knob {0}.
/// </summary>
internal static string KnobIndex {
get {
return ResourceManager.GetString("KnobIndex", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connected.
/// </summary>
internal static string StatusConnected {
get {
return ResourceManager.GetString("StatusConnected", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connecting....
/// </summary>
internal static string StatusConnecting {
get {
return ResourceManager.GetString("StatusConnecting", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Not connected.
/// </summary>
internal static string StatusNotConnected {
get {
return ResourceManager.GetString("StatusNotConnected", resourceCulture);
}
}
}
}

147
Windows/Strings.resx Normal file
View File

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="StatusConnected" xml:space="preserve">
<value>Connected</value>
</data>
<data name="StatusConnecting" xml:space="preserve">
<value>Connecting...</value>
</data>
<data name="StatusNotConnected" xml:space="preserve">
<value>Not connected</value>
</data>
<data name="KnobIndex" xml:space="preserve">
<value>Knob {0}</value>
</data>
<data name="DeviceDisplayNameActive" xml:space="preserve">
<value>{0}</value>
</data>
<data name="DeviceDisplayNameDisabled" xml:space="preserve">
<value>{0} (Disabled)</value>
</data>
<data name="DeviceDisplayNameInactive" xml:space="preserve">
<value>{0} (Inactive)</value>
</data>
<data name="DeviceDisplayNameNotPresent" xml:space="preserve">
<value>{0} (Not present)</value>
</data>
<data name="DeviceDisplayNameUnplugged" xml:space="preserve">
<value>{0} (Unplugged)</value>
</data>
</root>

View File

@ -0,0 +1,90 @@

namespace MassiveKnob.UserControls
{
partial class KnobDeviceControl
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.DeviceCombobox = new System.Windows.Forms.ComboBox();
this.KnobIndexLabel = new System.Windows.Forms.Label();
this.DeviceLabel = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// DeviceCombobox
//
this.DeviceCombobox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.DeviceCombobox.DropDownHeight = 300;
this.DeviceCombobox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.DeviceCombobox.FormattingEnabled = true;
this.DeviceCombobox.IntegralHeight = false;
this.DeviceCombobox.ItemHeight = 13;
this.DeviceCombobox.Location = new System.Drawing.Point(59, 24);
this.DeviceCombobox.Name = "DeviceCombobox";
this.DeviceCombobox.Size = new System.Drawing.Size(286, 21);
this.DeviceCombobox.TabIndex = 0;
this.DeviceCombobox.SelectedIndexChanged += new System.EventHandler(this.DeviceCombobox_SelectedIndexChanged);
//
// KnobIndexLabel
//
this.KnobIndexLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.KnobIndexLabel.Location = new System.Drawing.Point(3, 8);
this.KnobIndexLabel.Name = "KnobIndexLabel";
this.KnobIndexLabel.Size = new System.Drawing.Size(342, 13);
this.KnobIndexLabel.TabIndex = 1;
this.KnobIndexLabel.Text = "Knob X";
//
// DeviceLabel
//
this.DeviceLabel.AutoSize = true;
this.DeviceLabel.Location = new System.Drawing.Point(3, 27);
this.DeviceLabel.Name = "DeviceLabel";
this.DeviceLabel.Size = new System.Drawing.Size(41, 13);
this.DeviceLabel.TabIndex = 2;
this.DeviceLabel.Text = "Device";
//
// KnobDeviceControl
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.DeviceLabel);
this.Controls.Add(this.KnobIndexLabel);
this.Controls.Add(this.DeviceCombobox);
this.Name = "KnobDeviceControl";
this.Size = new System.Drawing.Size(351, 56);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ComboBox DeviceCombobox;
private System.Windows.Forms.Label KnobIndexLabel;
private System.Windows.Forms.Label DeviceLabel;
}
}

View File

@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using MassiveKnob.Hardware;
namespace MassiveKnob.UserControls
{
public partial class KnobDeviceControl : UserControl
{
private int knobIndex;
private Guid? deviceId;
public event KnobDeviceChangedEventHandler OnDeviceChanged;
public KnobDeviceControl()
{
InitializeComponent();
DeviceCombobox.DisplayMember = @"DisplayName";
}
public void SetKnobIndex(int index)
{
knobIndex = index;
KnobIndexLabel.Text = string.Format(Strings.KnobIndex, index + 1);
}
public void SetDeviceId(Guid? value)
{
deviceId = value;
if (DeviceCombobox.Items.Count > 0)
DeviceCombobox.SelectedItem = value.HasValue ? new DeviceItem(value.Value) : null;
}
public void SetDevices(IEnumerable<IAudioDevice> devices)
{
DeviceCombobox.BeginUpdate();
try
{
DeviceCombobox.Items.Clear();
if (devices == null)
return;
var sortedDevices = devices.OrderBy(d => d.DisplayName);
foreach (var device in sortedDevices)
{
var itemIndex = DeviceCombobox.Items.Add(
new DeviceItem(device.Id)
{
DisplayName = device.DisplayName
});
if (deviceId.HasValue && deviceId.Value == device.Id)
DeviceCombobox.SelectedIndex = itemIndex;
}
}
finally
{
DeviceCombobox.EndUpdate();
}
}
private void DeviceCombobox_SelectedIndexChanged(object sender, EventArgs e)
{
OnDeviceChanged?.Invoke(this, new KnobDeviceChangedEventArgs
{
KnobIndex = knobIndex,
DeviceId = ((DeviceItem)DeviceCombobox.SelectedItem)?.DeviceId
});
}
private class DeviceItem : IEquatable<DeviceItem>
{
// ReSharper disable UnusedAutoPropertyAccessor.Local - used by ComboBox
public Guid DeviceId { get; }
public string DisplayName { get; set; }
// ReSharper restore UnusedAutoPropertyAccessor.Local
public DeviceItem(Guid deviceId)
{
DeviceId = deviceId;
}
public bool Equals(DeviceItem other)
{
if (other == null) return false;
return ReferenceEquals(this, other) || DeviceId.Equals(other.DeviceId);
}
public override bool Equals(object obj)
{
if (obj == null) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is DeviceItem deviceItem && Equals(deviceItem);
}
public override int GetHashCode()
{
return DeviceId.GetHashCode();
}
}
}
public class KnobDeviceChangedEventArgs : EventArgs
{
public int KnobIndex { get; set; }
public Guid? DeviceId { get; set; }
}
public delegate void KnobDeviceChangedEventHandler(object sender, KnobDeviceChangedEventArgs e);
}