diff --git a/RadioBroadcaster.Server/App.config b/RadioBroadcaster.Server/App.config
new file mode 100644
index 0000000..731f6de
--- /dev/null
+++ b/RadioBroadcaster.Server/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RadioBroadcaster.Server/App.xaml b/RadioBroadcaster.Server/App.xaml
new file mode 100644
index 0000000..102de7a
--- /dev/null
+++ b/RadioBroadcaster.Server/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/RadioBroadcaster.Server/App.xaml.cs b/RadioBroadcaster.Server/App.xaml.cs
new file mode 100644
index 0000000..d7f5095
--- /dev/null
+++ b/RadioBroadcaster.Server/App.xaml.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace RadioBroadcaster.Server
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ }
+}
diff --git a/RadioBroadcaster.Server/FileEngine.cs b/RadioBroadcaster.Server/FileEngine.cs
new file mode 100644
index 0000000..b81f22d
--- /dev/null
+++ b/RadioBroadcaster.Server/FileEngine.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+
+namespace RadioBroadcaster.Server
+{
+ static class FileEngine
+ {
+ static string[] WriteBuffer = new string[1024];
+ static int LastWrite = -1;
+ static int LastAddition = -1;
+ static bool IsCurrentlyActive = false;
+ public static string logLocation = "";
+ public static void Write(string TextToWrite)
+ {
+ LastAddition = LastAddition + 1;
+ if(LastAddition > 1023)
+ {
+ LastAddition = 0;
+ }
+ WriteBuffer[LastAddition] = TextToWrite;
+ if (!IsCurrentlyActive)
+ {
+
+ }
+ }
+ static void WriteManager()
+ {
+ FileStream fs = File.OpenWrite(logLocation);
+ fs.Position = fs.Length; // Set position to the end of the stream.
+ while(LastWrite != LastAddition)
+ {
+ DoWrite(fs);
+ }
+ fs.Close();
+ IsCurrentlyActive = false;
+ }
+ static void DoWrite(FileStream fileStream)
+ {
+ LastWrite = LastWrite + 1;
+ if(LastWrite > 1023)
+ {
+ LastWrite = 0;
+ }
+ byte[] utf8Bytes = Encoding.UTF8.GetBytes(WriteBuffer[LastWrite]);
+ fileStream.Write(utf8Bytes, 0, utf8Bytes.Length);
+ }
+ }
+}
diff --git a/RadioBroadcaster.Server/LogManager.cs b/RadioBroadcaster.Server/LogManager.cs
new file mode 100644
index 0000000..9273738
--- /dev/null
+++ b/RadioBroadcaster.Server/LogManager.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+
+namespace RadioBroadcaster.Server
+{
+ static class LogManager
+ {
+ static string[] LogItems = new string[1024];
+ static string[] SerialItems = new string[128];
+ static int currentLogPosition = 0;
+ static int lastLogAddition = 0;
+
+ static int currentSerialPosition = 0;
+ static int lastSerialPosition = 0;
+
+ static string logLocation = System.Reflection.Assembly.GetExecutingAssembly().Location.Remove(System.Reflection.Assembly.GetExecutingAssembly().Location.LastIndexOf("\\")) + "\\outputLog";
+ static string serialLocation = System.Reflection.Assembly.GetExecutingAssembly().Location.Remove(System.Reflection.Assembly.GetExecutingAssembly().Location.LastIndexOf("\\")) + "\\SerialLog";
+
+ static bool LogItemsIsRunning = false;
+ static bool SerialItemsIsRunning = false;
+
+ static bool isDebug = false;
+
+ static bool IsCurrentlyActive = false;
+
+ public static void Init()
+ {
+ WriteLogTimer = new System.Timers.Timer(1000);
+ WriteLogTimer.Elapsed += WriteLogTimer_Elapsed;
+ WriteLogTimer.Start();
+ try
+ {
+ if (DateTime.Now.Subtract(File.GetLastWriteTime(serialLocation)).TotalDays > 14) // As the serial log could contain a massive amount of data, we wipe it after some time
+ {
+ File.WriteAllBytes(serialLocation, new byte[] { 0x00 });
+ }
+ else
+ {
+ FileStream fs = File.OpenRead(serialLocation);
+ if (fs.Length > 10737418240) // > 10GB
+ {
+ fs.Dispose();
+ File.WriteAllBytes(serialLocation, new byte[] { 0x00 });
+ }
+ }
+ }
+ catch
+ {
+ // We don't absolutely need to do anything here
+ }
+
+ }
+
+ private static void WriteLogTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
+ {
+ if (!LogItemsIsRunning)
+ {
+ LogItemsIsRunning = true;
+ WriteManager();
+ LogItemsIsRunning = false;
+ }
+ if (!SerialItemsIsRunning)
+ {
+ SerialItemsIsRunning = true;
+ SerialWriteManager();
+ SerialItemsIsRunning = false;
+ }
+ }
+
+ public static void Write(string TextToWrite)
+ {
+ lastLogAddition = lastLogAddition + 1;
+ if (lastLogAddition > 1023)
+ {
+ lastLogAddition = 0;
+ }
+ LogItems[lastLogAddition] = TextToWrite;
+ if (!IsCurrentlyActive)
+ {
+
+ }
+ }
+ static void WriteManager()
+ {
+ FileStream fs = File.OpenWrite(logLocation);
+ fs.Position = fs.Length; // Set position to the end of the stream.
+ while (currentLogPosition != lastLogAddition)
+ {
+ DoWrite(fs);
+ }
+ fs.Close();
+ IsCurrentlyActive = false;
+ }
+ static void DoWrite(FileStream fileStream)
+ {
+ currentLogPosition = currentLogPosition + 1;
+ if (currentLogPosition > 1023)
+ {
+ currentLogPosition = 0;
+ }
+ byte[] utf8Bytes = Encoding.UTF8.GetBytes(LogItems[currentLogPosition] + Environment.NewLine);
+ fileStream.Write(utf8Bytes, 0, utf8Bytes.Length);
+ LogItems[currentLogPosition] = ""; // Clear some mem
+ }
+ ///
+ /// Logs serial data
+ ///
+ ///
+ public static void WriteSerial(string TextToWrite)
+ {
+ if (isDebug)
+ {
+ lastSerialPosition = lastSerialPosition + 1;
+ if (lastSerialPosition > 127)
+ {
+ lastSerialPosition = 0;
+ }
+ SerialItems[lastSerialPosition] = TextToWrite;
+ }
+ }
+ public static void SerialWriteManager()
+ {
+ FileStream fs = File.OpenWrite(serialLocation);
+ fs.Position = fs.Length; // Set position to the end of the stream.
+ while (currentSerialPosition != lastSerialPosition)
+ {
+ SerialDoWrite(fs);
+ }
+ fs.Close();
+ IsCurrentlyActive = false;
+ }
+ static void SerialDoWrite(FileStream fileStream)
+ {
+ currentSerialPosition = currentSerialPosition + 1;
+ if (currentSerialPosition > 127)
+ {
+ currentSerialPosition = 0;
+ }
+ byte[] utf8Bytes = Encoding.UTF8.GetBytes(SerialItems[currentSerialPosition] + "\t"+ DateTime.Now.ToShortTimeString() + Environment.NewLine);
+ fileStream.Write(utf8Bytes, 0, utf8Bytes.Length);
+ SerialItems[currentSerialPosition] = ""; // Clear some mem
+ }
+ private static System.Timers.Timer WriteLogTimer;
+ }
+}
diff --git a/RadioBroadcaster.Server/MainWindow.xaml b/RadioBroadcaster.Server/MainWindow.xaml
new file mode 100644
index 0000000..9319eb1
--- /dev/null
+++ b/RadioBroadcaster.Server/MainWindow.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/RadioBroadcaster.Server/MainWindow.xaml.cs b/RadioBroadcaster.Server/MainWindow.xaml.cs
new file mode 100644
index 0000000..c4c2b9a
--- /dev/null
+++ b/RadioBroadcaster.Server/MainWindow.xaml.cs
@@ -0,0 +1,255 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+using System.IO;
+
+namespace RadioBroadcaster.Server
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ MediaEngine CurrentMediaEngine;
+ AudioTools audioTools = new AudioTools();
+ // File data
+ const string settingsFile = "settings.dat";
+ string currentRoot = System.Reflection.Assembly.GetExecutingAssembly().Location.Remove(System.Reflection.Assembly.GetExecutingAssembly().Location.LastIndexOf("\\")) + "\\";
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ LogManager.Init();
+ LogManager.Write("Starting software at " + DateTime.Now.ToShortDateString() +" "+ DateTime.Now.ToShortTimeString());
+ CurrentMediaEngine = new MediaEngine();
+ SerialManager.Init();
+ LogManager.Write("Loading Stations list");
+ Stations.LoadStations();
+ // Build UI objects
+ foreach (var item in Stations.sourceStations)
+ {
+ ListBoxItem listItem = new ListBoxItem() { Content = item.InternalName };
+ listItem.Selected += ListItem_Selected;
+ StationsList.Items.Add(listItem);
+ }
+ // Do loading of default settings
+ LogManager.Write("Loading settings");
+ BuildSettings();
+
+ }
+
+ ///
+ /// Creates or loads the settings file
+ ///
+ private void BuildSettings()
+ {
+ if (File.Exists(currentRoot + settingsFile))
+ {
+ string[] settings = File.ReadAllLines(currentRoot + settingsFile);
+ bool stationTag = false;
+ string[] stationsList = new string[0];
+ int stationsInc = 0;
+ foreach (var item in settings)
+ {
+ if (stationTag)
+ {
+ Array.Resize(ref stationsList, stationsList.Length + 1);
+ stationsList[stationsInc] = item;
+ stationsInc++;
+ }
+ else if (item.StartsWith("vol="))
+ {
+ CurrentMediaEngine.SetDefaultVolume = Convert.ToDouble(item.Replace("vol=", ""));
+ LogManager.Write("Volume: " + Convert.ToDouble(item.Replace("vol=", "")));
+ }
+ else if (item.StartsWith("txpow="))
+ {
+ // Not implemented
+ }
+ else if (item.StartsWith("freq="))
+ {
+ // Not implemented
+ }
+ else if (item.StartsWith("Default Station: (comment with // to skip)"))
+ {
+ stationTag = true;
+ }
+ }
+ bool[] validStations = new bool[stationsList.Length];
+ for (int i = 0; i < stationsList.Length; i++)
+ {
+ if (stationsList[i].StartsWith("//"))
+ {
+ validStations[i] = false;
+ }
+ else
+ {
+ validStations[i] = true;
+ }
+ }
+ string SelectedStationName = stationsList[Array.IndexOf(validStations, true)].Trim();
+ LogManager.Write("Station: " + SelectedStationName);
+ foreach (var item in Stations.sourceStations)
+ {
+ if (item.InternalName == SelectedStationName)
+ {
+
+ CurrentMediaEngine.SetPrimaryLocation = new Uri(item.URISource);
+ SerialManager.SetRDSStationName(item.RDS_StationName, true);
+ LogManager.Write("Setting RDS Station Name: " + item.RDS_StationName);
+ SerialManager.SetRDSStationData(item.RDS_Buffers[0]);
+ LogManager.Write("Setting RDS Station Data: " + item.RDS_Buffers[0]);
+ p1Loaded.Content = "Loaded";
+ LogManager.Write("Playing...");
+ CurrentMediaEngine.PlayPrimary();
+ p1Playing.Content = "Playing " + CurrentMediaEngine.SetPrimaryLocation;
+ }
+ }
+ }
+ else // Create a default settings file
+ {
+ LogManager.Write("Settings file did not exist, creating...");
+ string temp = "vol=0.7" + Environment.NewLine
+ + "txpow=115" + Environment.NewLine
+ + "freq=88.10 MHz" + Environment.NewLine
+ + "Default Station: (comment with // to skip)" + Environment.NewLine;
+ foreach (var item in Stations.sourceStations)
+ {
+ temp = temp + item.InternalName + Environment.NewLine;
+ }
+ File.WriteAllText(currentRoot + settingsFile, temp);
+ }
+ }
+
+ private void ListItem_Selected(object sender, RoutedEventArgs e)
+ {
+ ListBoxItem listBoxItem = (ListBoxItem)sender;
+ PlayButton.Content = "Play " + listBoxItem.Content;
+ }
+
+ private void PlayButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (!CurrentMediaEngine.IsPlayingAudio)
+ {
+ CurrentMediaEngine.SetPrimaryLocation = new Uri(Stations.sourceStations[StationsList.SelectedIndex].URISource);
+ SerialManager.SetRDSStationName(Stations.sourceStations[StationsList.SelectedIndex].RDS_StationName, true);
+ SerialManager.SetRDSStationData(Stations.sourceStations[StationsList.SelectedIndex].RDS_Buffers[0]);
+ p1Loaded.Content = "Loaded";
+ CurrentMediaEngine.PlayPrimary();
+ p1Playing.Content = "Playing " + CurrentMediaEngine.SetPrimaryLocation;
+ }
+ else
+ {
+ CurrentMediaEngine.SetSecondaryLocation = new Uri(Stations.sourceStations[StationsList.SelectedIndex].URISource);
+ SerialManager.SetRDSStationName(Stations.sourceStations[StationsList.SelectedIndex].RDS_StationName, true);
+ SerialManager.SetRDSStationData(Stations.sourceStations[StationsList.SelectedIndex].RDS_Buffers[0]);
+ if (CurrentMediaEngine.CurrentPrimaryPlayer == 1)
+ {
+ p2Loaded.Content = "Loaded";
+ }
+ else
+ {
+ p1Loaded.Content = "Loaded";
+ }
+ CurrentMediaEngine.PlaySecondary();
+ if (CurrentMediaEngine.CurrentPrimaryPlayer == 1)
+ {
+ p2playing.Content = "Playing " + CurrentMediaEngine.SetSecondaryLocation;
+ }
+ else
+ {
+ p1Playing.Content = "Playing " + CurrentMediaEngine.SetSecondaryLocation;
+ }
+ audioTools.Crossfade(CurrentMediaEngine);
+ }
+
+ }
+
+ private void TX_Send_Click(object sender, RoutedEventArgs e)
+ {
+ SerialManager.SetTXPower(TX.Value.ToString());
+ }
+
+ private void TX_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+
+ }
+
+ private void LoadStation1_Click(object sender, RoutedEventArgs e)
+ {
+ CurrentMediaEngine.SetPlayer1 = Stations.sourceStations[StationsList.SelectedIndex].URISource;
+ p1Loaded.Content = Stations.sourceStations[StationsList.SelectedIndex].RDS_StationName;
+ }
+
+ private void LoadStation2_Click(object sender, RoutedEventArgs e)
+ {
+ CurrentMediaEngine.SetPlayer2 = Stations.sourceStations[StationsList.SelectedIndex].URISource;
+ p2Loaded.Content = Stations.sourceStations[StationsList.SelectedIndex].RDS_StationName;
+ }
+
+ private void Play_2_Click(object sender, RoutedEventArgs e)
+ {
+ if(CurrentMediaEngine.SetPlayer1 != null)
+ {
+
+ }
+ }
+ }
+ ///
+ /// Various tools for handling audio streams and to add effects
+ ///
+ class AudioTools
+ {
+ ///
+ /// Crossfades the audio between the two tracks
+ ///
+ /// The playback engine in use
+ /// the time (in ms) to wait between each step
+ /// The number of steps to take
+ /// The maximum volume
+ public void Crossfade(MediaEngine playbackEngine, int steptime = 40, int steps = 16, double FullVolume=0.7)
+ {
+ playbackEngine.SecondaryVolume = 0;
+ playbackEngine.SecondaryMuted = false;
+ double volumePerStep = FullVolume / steps;
+ while (steps > 0)
+ {
+ playbackEngine.PrimaryVolume = playbackEngine.PrimaryVolume - volumePerStep;
+ if (playbackEngine.SecondaryVolume > FullVolume - volumePerStep)
+ {
+ playbackEngine.SecondaryVolume = FullVolume;
+ }
+ else
+ {
+ playbackEngine.SecondaryVolume = playbackEngine.SecondaryVolume + volumePerStep;
+ }
+ Thread.Sleep(steptime);
+ steps--;
+ }
+ // Mute and reset volume
+ playbackEngine.PrimaryMuted = true;
+ playbackEngine.PrimaryVolume = FullVolume;
+ playbackEngine.StopPrimary();
+ playbackEngine.SwapPrimary();
+ }
+ ///
+ /// Exposes the underlying audio stream within an m3u encapsulation
+ ///
+ public void m3uDecapsulator()
+ {
+
+ }
+ }
+}
diff --git a/RadioBroadcaster.Server/MediaEngine.cs b/RadioBroadcaster.Server/MediaEngine.cs
new file mode 100644
index 0000000..4d55915
--- /dev/null
+++ b/RadioBroadcaster.Server/MediaEngine.cs
@@ -0,0 +1,379 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Media;
+
+namespace RadioBroadcaster.Server
+{
+ class MediaEngine
+ {
+ MediaPlayer PlayerOne = new MediaPlayer();
+ MediaPlayer PlayerTwo = new MediaPlayer();
+ Uri PlayerOneUri;
+ Uri PlayerTwoUri;
+ private double DefaultVolume = 0.5;
+ ///
+ /// Handlers for if the media either ends or completely stops playing.
+ /// Default interval is 5s between attempts to restart.
+ ///
+ public void AddHandlers()
+ {
+ PlayerOne.MediaEnded += PlayerOne_MediaEnded;
+ PlayerOne.MediaFailed += PlayerOne_MediaFailed;
+
+ PlayerTwo.MediaEnded += PlayerTwo_MediaEnded;
+ PlayerTwo.MediaFailed += PlayerTwo_MediaFailed;
+ }
+
+ private void PlayerTwo_MediaFailed(object sender, ExceptionEventArgs e)
+ {
+ LogManager.Write("Media stream 2 failed at time: " + DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToLongTimeString());
+ PlayerTwo.Close();
+ Thread.Sleep(5000);
+ PlayerTwo.Open(PlayerTwoUri);
+ PlayerTwo.Play();
+ }
+
+ private void PlayerTwo_MediaEnded(object sender, EventArgs e)
+ {
+ LogManager.Write("Media stream 2 ended at time: " + DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToLongTimeString());
+ PlayerTwo.Close();
+ Thread.Sleep(5000);
+ PlayerTwo.Open(PlayerTwoUri);
+ PlayerTwo.Play();
+ }
+
+ private void PlayerOne_MediaFailed(object sender, ExceptionEventArgs e)
+ {
+ LogManager.Write("Media stream 1 failed at time: " + DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToLongTimeString());
+ PlayerOne.Close();
+ Thread.Sleep(5000);
+ PlayerOne.Open(PlayerOneUri);
+ PlayerOne.Play();
+ }
+
+ private void PlayerOne_MediaEnded(object sender, EventArgs e)
+ {
+ LogManager.Write("Media stream 1 ended at time: " + DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToLongTimeString());
+ PlayerOne.Close();
+ Thread.Sleep(5000);
+ PlayerOne.Open(PlayerOneUri);
+ PlayerOne.Play();
+ }
+ #region States
+ private bool One_Playing = false;
+ private bool One_Loaded = false;
+
+ private bool Two_Playing = false;
+ private bool Two_Loaded = false;
+
+ private int CurrentPrimary = 1;
+ #endregion
+ public Uri SetPrimaryLocation
+ {
+ get
+ {
+ if (CurrentPrimary == 1)
+ {
+ return PlayerOne.Source;
+ }
+ else
+ {
+ return PlayerTwo.Source;
+ }
+ }
+ set
+ {
+ if(CurrentPrimary == 1)
+ {
+ PlayerOneUri = value;
+ PlayerOne.Open(value);
+ }
+ else
+ {
+ PlayerTwoUri = value;
+ PlayerTwo.Open(value);
+ }
+ }
+ }
+ public Uri SetSecondaryLocation
+ {
+ get
+ {
+ if (CurrentPrimary == 1)
+ {
+ return PlayerTwo.Source;
+ }
+ else
+ {
+ return PlayerOne.Source;
+ }
+ }
+ set
+ {
+ if (CurrentPrimary == 1)
+ {
+ PlayerTwoUri = value;
+ PlayerTwo.Open(value);
+ }
+ else
+ {
+ PlayerOneUri = value;
+ PlayerOne.Open(value);
+ }
+ }
+ }
+ public void PlayPrimary()
+ {
+ if(CurrentPrimary == 1)
+ {
+ PlayerOne.Play();
+ One_Playing = true;
+ }
+ else
+ {
+ PlayerTwo.Play();
+ Two_Playing = true;
+ }
+ }
+ public void PlaySecondary()
+ {
+ if (CurrentPrimary == 1)
+ {
+ //PlayerTwo.IsMuted = true;
+ PlayerTwo.Play();
+ Two_Playing = true;
+ }
+ else
+ {
+ //PlayerOne.IsMuted = true;
+ PlayerOne.Play();
+ One_Playing = true;
+ }
+ }
+ public string SetPlayer1
+ {
+ get
+ {
+ return PlayerOneUri.AbsoluteUri;
+ }
+ set
+ {
+ PlayerOneUri = new Uri(value);
+ if (One_Playing)
+ {
+ PlayerOne.Open(PlayerOneUri);
+ }
+ }
+ }
+ public string SetPlayer2
+ {
+ get
+ {
+ return PlayerTwoUri.AbsoluteUri;
+ }
+ set
+ {
+ PlayerTwoUri = new Uri(value);
+ if (Two_Playing)
+ {
+ PlayerTwo.Open(PlayerTwoUri);
+ }
+ }
+ }
+ public bool IsPlayingAudio
+ {
+ get
+ {
+ if(One_Playing == false && Two_Playing == false)
+ {
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+ }
+ }
+ public bool IsOnePlayingAudio
+ {
+ get
+ {
+ return One_Playing;
+ }
+ }
+ public bool IsTwoPlayingAudio
+ {
+ get
+ {
+ return Two_Playing;
+ }
+ }
+ public bool IsPrimaryLoaded
+ {
+ get
+ {
+ if(CurrentPrimary == 1)
+ {
+ return One_Loaded;
+ }
+ else
+ {
+ return Two_Loaded;
+ }
+ }
+ }
+ public bool IsSecondaryLoaded
+ {
+ get
+ {
+ if (CurrentPrimary == 1)
+ {
+ return Two_Loaded;
+ }
+ else
+ {
+ return One_Loaded;
+ }
+ }
+ }
+ public int CurrentPrimaryPlayer
+ {
+ get{ return CurrentPrimary; }
+ }
+ public double PrimaryVolume
+ {
+ get
+ {
+ if(CurrentPrimary == 1)
+ {
+ return PlayerOne.Volume;
+ }
+ else
+ {
+ return PlayerTwo.Volume;
+ }
+ }
+ set
+ {
+ if(CurrentPrimary == 1)
+ {
+ PlayerOne.Volume = value;
+ }
+ else
+ {
+ PlayerTwo.Volume = value;
+ }
+ }
+ }
+ public double SecondaryVolume
+ {
+ get
+ {
+ if (CurrentPrimary == 1)
+ {
+ return PlayerTwo.Volume;
+ }
+ else
+ {
+ return PlayerOne.Volume;
+ }
+ }
+ set
+ {
+ if (CurrentPrimary == 1)
+ {
+ PlayerTwo.Volume = value;
+ }
+ else
+ {
+ PlayerOne.Volume = value;
+ }
+ }
+ }
+ public double SetDefaultVolume
+ {
+ set
+ {
+ DefaultVolume = value;
+ PrimaryVolume = value;
+ SecondaryVolume = value;
+ }
+ }
+ public bool SecondaryMuted
+ {
+ get
+ {
+ if (CurrentPrimary == 1)
+ {
+ return PlayerTwo.IsMuted;
+ }
+ else
+ {
+ return PlayerOne.IsMuted;
+ }
+ }
+ set
+ {
+ if(CurrentPrimary == 1)
+ {
+ PlayerTwo.IsMuted = value;
+ }
+ else
+ {
+ PlayerOne.IsMuted = value;
+ }
+ }
+ }
+ public bool PrimaryMuted
+ {
+ get
+ {
+ if (CurrentPrimary == 1)
+ {
+ return PlayerOne.IsMuted;
+ }
+ else
+ {
+ return PlayerTwo.IsMuted;
+ }
+ }
+ set
+ {
+ if (CurrentPrimary == 1)
+ {
+ PlayerOne.IsMuted = value;
+ }
+ else
+ {
+ PlayerTwo.IsMuted = value;
+ }
+ }
+ }
+ public void SwapPrimary()
+ {
+ if(CurrentPrimary == 1)
+ {
+ CurrentPrimary = 2;
+ }
+ else
+ {
+ CurrentPrimary = 1;
+ }
+ }
+ public void StopPrimary()
+ {
+ if(CurrentPrimary == 1)
+ {
+ PlayerOne.Close();
+ }
+ else
+ {
+ PlayerTwo.Close();
+ }
+ }
+ }
+}
diff --git a/RadioBroadcaster.Server/Properties/AssemblyInfo.cs b/RadioBroadcaster.Server/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..42b2156
--- /dev/null
+++ b/RadioBroadcaster.Server/Properties/AssemblyInfo.cs
@@ -0,0 +1,55 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// 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("RadioBroadcaster.Server")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("RadioBroadcaster.Server")]
+[assembly: AssemblyCopyright("Copyright © 2018")]
+[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)]
+
+//In order to begin building localizable applications, set
+//CultureYouAreCodingWith in your .csproj file
+//inside a . For example, if you are using US english
+//in your source files, set the to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// 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")]
diff --git a/RadioBroadcaster.Server/Properties/Resources.Designer.cs b/RadioBroadcaster.Server/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..688477f
--- /dev/null
+++ b/RadioBroadcaster.Server/Properties/Resources.Designer.cs
@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+//
+// 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.
+//
+//------------------------------------------------------------------------------
+
+namespace RadioBroadcaster.Server.Properties
+{
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // 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()
+ {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [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("RadioBroadcaster.Server.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture
+ {
+ get
+ {
+ return resourceCulture;
+ }
+ set
+ {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/RadioBroadcaster.Server/Properties/Resources.resx b/RadioBroadcaster.Server/Properties/Resources.resx
new file mode 100644
index 0000000..af7dbeb
--- /dev/null
+++ b/RadioBroadcaster.Server/Properties/Resources.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/RadioBroadcaster.Server/Properties/Settings.Designer.cs b/RadioBroadcaster.Server/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..dd994c6
--- /dev/null
+++ b/RadioBroadcaster.Server/Properties/Settings.Designer.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+//
+// 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.
+//
+//------------------------------------------------------------------------------
+
+namespace RadioBroadcaster.Server.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;
+ }
+ }
+ }
+}
diff --git a/RadioBroadcaster.Server/Properties/Settings.settings b/RadioBroadcaster.Server/Properties/Settings.settings
new file mode 100644
index 0000000..033d7a5
--- /dev/null
+++ b/RadioBroadcaster.Server/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RadioBroadcaster.Server/RadioBroadcaster.Server.csproj b/RadioBroadcaster.Server/RadioBroadcaster.Server.csproj
new file mode 100644
index 0000000..5259f14
--- /dev/null
+++ b/RadioBroadcaster.Server/RadioBroadcaster.Server.csproj
@@ -0,0 +1,102 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {FE7C4844-0504-4FF0-97A5-AFE1064D058D}
+ WinExe
+ RadioBroadcaster.Server
+ RadioBroadcaster.Server
+ v4.6.1
+ 512
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 4
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+ 4.0
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+ App.xaml
+ Code
+
+
+ MainWindow.xaml
+ Code
+
+
+
+
+ Code
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ Settings.settings
+ True
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RadioBroadcaster.Server/SerialManager.cs b/RadioBroadcaster.Server/SerialManager.cs
new file mode 100644
index 0000000..a337a09
--- /dev/null
+++ b/RadioBroadcaster.Server/SerialManager.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO.Ports;
+using System.IO;
+using System.Threading;
+
+namespace RadioBroadcaster.Server
+{
+ static class SerialManager
+ {
+ static bool _continue;
+ static SerialPort _serialPort;
+ ///
+ /// Data is sent at a predetermined rate. As the arduino can only process one piece of data at a time,
+ /// we add each command to the buffer; which will be sent as soon as possible
+ ///
+ static string[] SendBuffer = new string[0];
+ static int SendBufferSize = 64;
+ static int currentBufferPos = 0; // The current line to send
+ static int lastBufferAddition = -1; // The last position added to within the buffer
+ private static System.Timers.Timer CarouselTimer;
+ private static System.Timers.Timer SendBufferTimer;
+
+ const string FileName = "PortSelection.txt";
+ public static void Init()
+ {
+ Thread readThread = new Thread(Read);
+ SendBuffer = new string[SendBufferSize];
+ _serialPort = new SerialPort();
+ _serialPort.BaudRate = 9600;
+ SendBufferTimer = new System.Timers.Timer(610); // We tick at the same rate as the arduino. There may be clashing
+ // Due to timing differences, but so long as we always take longer than it takes to process data, we
+ // we will ensure data arrives correctly. Make sure this is delayed while serial gets set-up.
+ // Currently, as the carousel works on a 2 second timer, we can send at least 3 packets of data before
+ // a new one is routinely added.
+ SendBufferTimer.Elapsed += SendBufferTimer_Elapsed;
+ CarouselTimer = new System.Timers.Timer(2000);
+ CarouselTimer.Elapsed += CarouselTimer_Elapsed;
+ string[] AvailablePorts;
+ string s = System.Reflection.Assembly.GetExecutingAssembly().Location;
+ try
+ {
+ AvailablePorts = File.ReadAllText(s.Remove(s.LastIndexOf("\\")) + "\\" + FileName).Replace(Environment.NewLine, "\n").Split('\n');
+ }
+ catch
+ {
+ AvailablePorts = SerialPort.GetPortNames();
+ File.WriteAllLines(s.Remove(s.LastIndexOf("\\")) + "\\" + FileName, AvailablePorts);
+ }
+ bool[] removedPorts = new bool[AvailablePorts.Length];
+ for (int i = 0; i < AvailablePorts.Length; i++)
+ {
+ if (AvailablePorts[i].StartsWith("//"))
+ {
+ removedPorts[i] = true;
+ }
+ else
+ {
+ removedPorts[i] = false;
+ }
+ }
+ _serialPort.PortName = AvailablePorts[Array.IndexOf(removedPorts,false)];
+ try
+ {
+ _serialPort.Open();
+ LogManager.Write("Serial port opened successfully");
+ }
+ catch
+ {
+ LogManager.Write("Couldn't access " + _serialPort.PortName + ", loading first port as a default");
+ _serialPort.PortName = SerialPort.GetPortNames()[0];
+ _serialPort.Open();
+ }
+ _continue = true;
+ readThread.Start();
+ SendBufferTimer.Start();
+ Console.Write("Ready to write data to the port");
+ }
+ static bool SendBufferLock = false;
+ static int waitTicks = 0;
+ ///
+ /// Sends data, if there is any, at every timer tick. Allows a queue of commands
+ /// to be executed without clashing.
+ ///
+ ///
+ ///
+ private static void SendBufferTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
+ {
+ if(waitTicks > 5)
+ {
+ if (!SendBufferLock)
+ {
+ SendBufferLock = true;
+ if (SendBuffer[currentBufferPos] != null)
+ {
+ string s = SendBuffer[currentBufferPos].Trim();
+ if (s == "" || s == null)
+ {
+
+ }
+ else
+ {
+ LogManager.WriteSerial("\tOut: " + s);
+ _serialPort.WriteLine(s);
+ SendBuffer[currentBufferPos] = "";
+ if (currentBufferPos >= SendBufferSize - 1)
+ {
+ currentBufferPos = 0;
+ }
+ else
+ {
+ currentBufferPos++;
+ }
+ }
+ }
+ SendBufferLock = false;
+ }
+ }
+ else
+ {
+ waitTicks++;
+ }
+
+ }
+
+ static string currentIteration = "";
+ public static void SetRDSStationName(string setStationName, bool useCarousel = false, bool stopCarousel = false)
+ {
+ if (useCarousel)
+ {
+ LogManager.Write("Starting carousel...");
+ CarouselTimer.Stop();
+ if(setStationName.Last() != ' ') // Make sure there is a space between the first and last char
+ {
+ setStationName = setStationName + " ";
+ }
+ currentIteration = setStationName;
+
+ CarouselTimer.Start();
+ }
+ else
+ {
+ if (setStationName.Length < 9)
+ {
+ Write("RDSName" + setStationName);
+ }
+ else
+ {
+ Write("RDSName" + setStationName.Remove(8));
+ }
+ }
+ if (stopCarousel)
+ {
+
+ }
+
+ }
+ public static void SetRDSStationData(string data)
+ {
+ Write("RDSData" + data);
+ }
+ public static void SetFrequency(string Frequency)
+ {
+ string temp = Frequency.ToLower();
+ if (temp.Contains("mhz"))
+ {
+ temp = temp.Remove(temp.IndexOf("mhz"));
+ }
+ temp = temp.Replace(".", "");
+ temp = temp.Trim();
+ Write("StatFreq" + temp);
+ }
+ public static void SetTXPower(string power)
+ {
+ string s = power.Trim();
+ Write("TXpower" + s);
+ }
+
+ private static void CarouselTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
+ {
+ char one = currentIteration[0];
+ currentIteration = currentIteration.Remove(0, 1) + one;
+ SetRDSStationName(currentIteration);
+ }
+ ///
+ /// Adds the data to the write queue
+ ///
+ ///
+ static void Write(string message)
+ {
+ if(message.Trim() != "")
+ {
+ lastBufferAddition = lastBufferAddition + 1;
+ if (lastBufferAddition > SendBufferSize -1)
+ {
+ lastBufferAddition = 0;
+ }
+ SendBuffer[lastBufferAddition] = message;
+ }
+ }
+ static void Read()
+ {
+ while (_continue)
+ {
+ try
+ {
+ string message = _serialPort.ReadLine();
+ LogManager.WriteSerial(message);
+ if (message == "beat")
+ {
+ // We recieved a heartbeat
+
+ }
+ else if (message.Trim() != "")
+ {
+ Console.WriteLine(message);
+ switch (message.Remove(message.IndexOf(":")))
+ {
+ case "Freq":
+ break;
+ case "TXPower":
+ break;
+ case "InLevel":
+ break;
+ case "ASQ":
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ catch (TimeoutException) { }
+ }
+ }
+ public static bool carouselContinue = false;
+ public static void RDSStationNameCarousel(string StationName, SerialPort serialPort)
+ {
+ string originalName = StationName;
+ string currentIteration = originalName;
+ carouselContinue = true;
+ while (carouselContinue)
+ {
+ char one = currentIteration[0];
+ currentIteration = currentIteration.Remove(0, 1) + one;
+ if (currentIteration.Length < 9)
+ {
+ serialPort.WriteLine("RDSName" + currentIteration);
+ }
+ else
+ {
+ serialPort.WriteLine("RDSName" + currentIteration.Remove(8));
+ }
+ Thread.Sleep(1000);
+ }
+ }
+ }
+}
diff --git a/RadioBroadcaster.Server/Stations.cs b/RadioBroadcaster.Server/Stations.cs
new file mode 100644
index 0000000..b167bf6
--- /dev/null
+++ b/RadioBroadcaster.Server/Stations.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RadioBroadcaster.Server
+{
+ public static class Stations
+ {
+ public static List sourceStations = new List();
+ public static void LoadStations()
+ {
+ sourceStations.Add(new SourceStation()
+ {
+ InternalName = "Sound",
+ InternalID = 0,
+ URISource = "http://tunein-icecast.mediaworks.nz/sound_128kbps",
+ RDS_StationName = "The Sound - Rebroadcast",
+ RDS_Buffers = new string[] { "The Sound FM - ", "Rebroadcast" }
+ });
+ sourceStations.Add(new SourceStation()
+ {
+ InternalName = "MoreFM",
+ InternalID = 0,
+ URISource = "http://tunein-icecast.mediaworks.nz/more_128kbps",
+ RDS_StationName = "MoreFM - Rebroadcast",
+ RDS_Buffers = new string[] { "MoreFM - ", "Rebroadcast" }
+ });
+ sourceStations.Add(new SourceStation()
+ {
+ InternalName = "P4",
+ InternalID = 0,
+ URISource = "http://http-live.sr.se/p4stockholm-aac-96",
+ RDS_StationName = "P4 Stockholm",
+ RDS_Buffers = new string[] { "P4 Stockholm ", "Rebroadcast" }
+ });
+ sourceStations.Add(new SourceStation()
+ {
+ InternalName = "Radio Hauraki",
+ InternalID = 0,
+ URISource = "https://ais-nzme.streamguys1.com/nz_009/playlist.m3u8",
+ RDS_StationName = "Radio Hauraki",
+ RDS_Buffers = new string[] { "Radio Hauraki", "Rebroadcast" }
+ });
+ sourceStations.Add(new SourceStation()
+ {
+ InternalName = "Rock",
+ InternalID = 0,
+ URISource = "http://tunein-icecast.mediaworks.nz/rock_128kbps",
+ RDS_StationName = "The Rock - Rebroadcast",
+ RDS_Buffers = new string[] { "The Rock FM - ", "Rebroadcast" }
+ });
+ }
+
+ }
+ public class SourceStation : IEquatable
+ {
+ public string InternalName { get; set; }
+ public int InternalID { get; set; }
+
+ public string URISource { get; set; }
+
+ public string RequestedFrequency { get; set; }
+ public int BroadcastPower { get; set; }
+ public string RDS_StationName { get; set; }
+ ///
+ /// RDS Buffer. Cannot exceed 32 chars per segment
+ ///
+ public string[] RDS_Buffers { get; set; }
+
+ public bool Equals(SourceStation otherStation)
+ {
+ if (this.InternalName != otherStation.InternalName)
+ {
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+ }
+ }
+}
diff --git a/RadioBroadcaster.sln b/RadioBroadcaster.sln
new file mode 100644
index 0000000..8edc435
--- /dev/null
+++ b/RadioBroadcaster.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27428.2043
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RadioBroadcaster.Server", "RadioBroadcaster.Server\RadioBroadcaster.Server.csproj", "{FE7C4844-0504-4FF0-97A5-AFE1064D058D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {FE7C4844-0504-4FF0-97A5-AFE1064D058D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FE7C4844-0504-4FF0-97A5-AFE1064D058D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FE7C4844-0504-4FF0-97A5-AFE1064D058D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FE7C4844-0504-4FF0-97A5-AFE1064D058D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {A1F4495C-B9DD-4AE5-B631-029D8F167948}
+ EndGlobalSection
+EndGlobal