diff --git a/src/AudioBand.sln b/src/AudioBand.sln index 452fd330..6c19b6c3 100644 --- a/src/AudioBand.sln +++ b/src/AudioBand.sln @@ -27,6 +27,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Audio Sources", "Audio Sour EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E79F26F3-C51F-4375-BF1C-4090FAB8D16D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Win10AudioSource", "Win10AudioSource\Win10AudioSource.csproj", "{A709B842-7F5A-4C81-8A2E-80396996EF79}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -137,6 +139,18 @@ Global {4E9EEE47-23F6-45B7-9DDB-A34731D6AE4A}.Release|x64.Build.0 = Release|x64 {4E9EEE47-23F6-45B7-9DDB-A34731D6AE4A}.Test|Any CPU.ActiveCfg = Release|x64 {4E9EEE47-23F6-45B7-9DDB-A34731D6AE4A}.Test|x64.ActiveCfg = Release|x64 + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Debug|x64.ActiveCfg = Debug|Any CPU + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Debug|x64.Build.0 = Debug|Any CPU + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Release|Any CPU.Build.0 = Release|Any CPU + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Release|x64.ActiveCfg = Release|Any CPU + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Release|x64.Build.0 = Release|Any CPU + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Test|Any CPU.ActiveCfg = Release|Any CPU + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Test|Any CPU.Build.0 = Release|Any CPU + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Test|x64.ActiveCfg = Release|Any CPU + {A709B842-7F5A-4C81-8A2E-80396996EF79}.Test|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -150,6 +164,7 @@ Global {741DB79C-921D-4D91-85F1-CD10C746F46E} = {31BA7036-81E7-4D02-90CA-753832D950C5} {D3F92C3E-E546-4A6B-ADA2-FACD95E229F7} = {AD1A45A7-F5E2-4657-BA09-99F8A5421FF0} {D8E1D3E5-D0AB-43C4-8AF6-60C14C5C6843} = {AD1A45A7-F5E2-4657-BA09-99F8A5421FF0} + {A709B842-7F5A-4C81-8A2E-80396996EF79} = {31BA7036-81E7-4D02-90CA-753832D950C5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7C4FC539-D568-422E-8445-A0313878CB1F} diff --git a/src/Win10AudioSource/AudioSource.manifest b/src/Win10AudioSource/AudioSource.manifest new file mode 100644 index 00000000..8d405b6b --- /dev/null +++ b/src/Win10AudioSource/AudioSource.manifest @@ -0,0 +1 @@ +AudioSource = "Win10AudioSource.dll" \ No newline at end of file diff --git a/src/Win10AudioSource/Properties/AssemblyInfo.cs b/src/Win10AudioSource/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..fb4d8332 --- /dev/null +++ b/src/Win10AudioSource/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +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("Win10AudioSource")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Win10AudioSource")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[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("a709b842-7f5a-4c81-8a2e-80396996ef79")] + +// 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/src/Win10AudioSource/Win10AudioSource.cs b/src/Win10AudioSource/Win10AudioSource.cs new file mode 100644 index 00000000..17a14862 --- /dev/null +++ b/src/Win10AudioSource/Win10AudioSource.cs @@ -0,0 +1,306 @@ +using System; +using System.Drawing; +using System.IO; +using System.Threading.Tasks; +using AudioBand.AudioSource; +using Windows.Foundation.Metadata; +using Windows.Media; +using Windows.Media.Control; +using Windows.Storage.Streams; + +namespace Win10AudioSource +{ + public class Win10AudioSource : IAudioSource + { + private GlobalSystemMediaTransportControlsSessionManager _mtcManager; + private GlobalSystemMediaTransportControlsSession _currentSession; + + public event EventHandler SettingChanged + { + add { } + remove { } + } + + public event EventHandler TrackInfoChanged; + + public event EventHandler IsPlayingChanged; + + public event EventHandler TrackProgressChanged; + + public event EventHandler VolumeChanged + { + add { } + remove { } + } + + public event EventHandler ShuffleChanged; + + public event EventHandler RepeatModeChanged; + + public string Name { get; } = "Windows 10"; + + public IAudioSourceLogger Logger { get; set; } + + public async Task ActivateAsync() + { + if (!ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7)) + { + Logger.Info("Audio source only available on windows 10 1809 and later"); + return; + } + + _mtcManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync(); + await UpdateSession(_mtcManager.GetCurrentSession()); + _mtcManager.CurrentSessionChanged += MtcManagerOnCurrentSessionChanged; + } + + public Task DeactivateAsync() + { + UnsubscribeFromSession(); + _mtcManager.CurrentSessionChanged -= MtcManagerOnCurrentSessionChanged; + _mtcManager = null; + + return Task.CompletedTask; + } + + public async Task PlayTrackAsync() + { + if (_currentSession == null) + { + return; + } + + await _currentSession.TryPlayAsync(); + } + + public async Task PauseTrackAsync() + { + if (_currentSession == null) + { + return; + } + + await _currentSession.TryPauseAsync(); + } + + public async Task PreviousTrackAsync() + { + if (_currentSession == null) + { + return; + } + + await _currentSession.TrySkipPreviousAsync(); + } + + public async Task NextTrackAsync() + { + if (_currentSession == null) + { + return; + } + + await _currentSession.TrySkipNextAsync(); + } + + public Task SetVolumeAsync(float newVolume) + { + return Task.CompletedTask; + } + + public async Task SetPlaybackProgressAsync(TimeSpan newProgress) + { + if (_currentSession == null) + { + return; + } + + await _currentSession.TryChangePlaybackPositionAsync((long)newProgress.TotalSeconds); + } + + public async Task SetShuffleAsync(bool shuffleOn) + { + if (_currentSession == null) + { + return; + } + + await _currentSession.TryChangeShuffleActiveAsync(shuffleOn); + } + + public async Task SetRepeatModeAsync(RepeatMode newRepeatMode) + { + if (_currentSession == null) + { + return; + } + + await _currentSession.TryChangeAutoRepeatModeAsync(ToWindowsRepeatMode(newRepeatMode)); + } + + private async void MtcManagerOnCurrentSessionChanged(GlobalSystemMediaTransportControlsSessionManager sender, CurrentSessionChangedEventArgs args) + { + await UpdateSession(sender.GetCurrentSession()); + } + + private void CurrentSessionOnTimelinePropertiesChanged(GlobalSystemMediaTransportControlsSession sender, TimelinePropertiesChangedEventArgs args) + { + UpdateTimelineProperties(); + } + + private void CurrentSessionOnPlaybackInfoChanged(GlobalSystemMediaTransportControlsSession sender, PlaybackInfoChangedEventArgs args) + { + UpdatePlaybackProperties(); + } + + private async void CurrentSessionOnMediaPropertiesChanged(GlobalSystemMediaTransportControlsSession sender, MediaPropertiesChangedEventArgs args) + { + await UpdateMediaProperties(); + } + + private async Task UpdateSession(GlobalSystemMediaTransportControlsSession newSession) + { + UnsubscribeFromSession(); + _currentSession = newSession; + + await UpdateMediaProperties(); + UpdateTimelineProperties(); + UpdatePlaybackProperties(); + SubscribeToSession(); + } + + private void UnsubscribeFromSession() + { + if (_currentSession == null) + { + return; + } + + _currentSession.MediaPropertiesChanged -= CurrentSessionOnMediaPropertiesChanged; + _currentSession.PlaybackInfoChanged -= CurrentSessionOnPlaybackInfoChanged; + _currentSession.TimelinePropertiesChanged -= CurrentSessionOnTimelinePropertiesChanged; + } + + private void SubscribeToSession() + { + if (_currentSession == null) + { + return; + } + + _currentSession.MediaPropertiesChanged += CurrentSessionOnMediaPropertiesChanged; + _currentSession.PlaybackInfoChanged += CurrentSessionOnPlaybackInfoChanged; + _currentSession.TimelinePropertiesChanged += CurrentSessionOnTimelinePropertiesChanged; + } + + private async Task UpdateMediaProperties() + { + if (_currentSession == null) + { + return; + } + + var mediaProperties = await _currentSession.TryGetMediaPropertiesAsync(); + var albumArt = await GetAlbumArt(mediaProperties.Thumbnail); + + TrackInfoChanged?.Invoke(this, new TrackInfoChangedEventArgs + { + Album = mediaProperties.AlbumTitle, + AlbumArt = albumArt, + Artist = mediaProperties.Artist, + TrackName = mediaProperties.Title, + TrackLength = _currentSession.GetTimelineProperties().EndTime, + }); + } + + private void UpdateTimelineProperties() + { + if (_currentSession == null) + { + return; + } + + var timelineProperties = _currentSession.GetTimelineProperties(); + TrackProgressChanged?.Invoke(this, timelineProperties.Position); + } + + private void UpdatePlaybackProperties() + { + if (_currentSession == null) + { + ClearState(); + return; + } + + var playbackInfo = _currentSession.GetPlaybackInfo(); + + // We'll just make every other state count as paused. + var isPlaying = playbackInfo.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing; + IsPlayingChanged?.Invoke(this, isPlaying); + + ShuffleChanged?.Invoke(this, playbackInfo.IsShuffleActive.GetValueOrDefault()); + RepeatModeChanged?.Invoke(this, ToAudioBandRepeatMode(playbackInfo.AutoRepeatMode.GetValueOrDefault())); + } + + private RepeatMode ToAudioBandRepeatMode(MediaPlaybackAutoRepeatMode repeatMode) + { + switch (repeatMode) + { + case MediaPlaybackAutoRepeatMode.List: + return RepeatMode.RepeatContext; + case MediaPlaybackAutoRepeatMode.None: + return RepeatMode.Off; + case MediaPlaybackAutoRepeatMode.Track: + return RepeatMode.RepeatTrack; + } + + return RepeatMode.Off; + } + + private MediaPlaybackAutoRepeatMode ToWindowsRepeatMode(RepeatMode repeatMode) + { + switch (repeatMode) + { + case RepeatMode.Off: + return MediaPlaybackAutoRepeatMode.None; + case RepeatMode.RepeatContext: + return MediaPlaybackAutoRepeatMode.List; + case RepeatMode.RepeatTrack: + return MediaPlaybackAutoRepeatMode.Track; + } + + return MediaPlaybackAutoRepeatMode.None; + } + + private async Task GetAlbumArt(IRandomAccessStreamReference stream) + { + if (stream == null) + { + return null; + } + + try + { + var read = await stream.OpenReadAsync(); + using (var netStream = read.AsStreamForRead()) + { + return Image.FromStream(netStream); + } + } + catch (Exception e) + { + Logger.Error(e); + return null; + } + } + + private void ClearState() + { + TrackInfoChanged?.Invoke(this, new TrackInfoChangedEventArgs()); + IsPlayingChanged?.Invoke(this, false); + TrackProgressChanged?.Invoke(this, TimeSpan.Zero); + ShuffleChanged?.Invoke(this, false); + RepeatModeChanged?.Invoke(this, RepeatMode.Off); + } + } +} diff --git a/src/Win10AudioSource/Win10AudioSource.csproj b/src/Win10AudioSource/Win10AudioSource.csproj new file mode 100644 index 00000000..196eb090 --- /dev/null +++ b/src/Win10AudioSource/Win10AudioSource.csproj @@ -0,0 +1,73 @@ + + + + + Debug + AnyCPU + {A709B842-7F5A-4C81-8A2E-80396996EF79} + Library + Properties + Win10AudioSource + Win10AudioSource + v4.7 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + ..\AudioBandRules.ruleset + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + PreserveNewest + + + + + {30f2bfea-788a-494d-88e7-f2070528ebea} + AudioBand.AudioSource + + + + + 10.0.17763.1000-preview + + + 1.1.118 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + if NOT "$(Configuration)" == "Release" ( + (robocopy "$(TargetDir)\" "$(SolutionDir)AudioBand\$(OutDir)AudioSources\Windows10\\" /xf AudioBand.AudioSource.*) ^& IF %25ERRORLEVEL%25 LEQ 1 exit 0 +) ELSE ( + (robocopy "$(TargetDir)\" "$(TargetDir)Windows10\\" /xf AudioBand.AudioSource.*) ^& IF %25ERRORLEVEL%25 LEQ 1 exit 0 +) + + \ No newline at end of file