12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361 |
- using System.ComponentModel;
- using System.ComponentModel.DataAnnotations;
- using System.Diagnostics;
- using System.Diagnostics.Eventing.Reader;
- using System.Drawing.Drawing2D;
- using System.Reflection;
- using System.Text;
- using System.Threading.Channels;
- using Bmp.Core.Common.EventBus;
- using Bmp.Core.Common.Utility;
- using Bmp.Core.Common.Win32;
- using Bmp.Core.Lite.Metadata;
- using Bmp.Core.Lite.Playback.Inputs;
- using Bmp.Core.Lite.Playback.MiddleWare;
- using Bmp.Core.Lite.Playback.Outputs;
- using Bmp.Core.Playback.Inputs;
- using Bmp.WinForms.SaveLoad;
- using Bmp.WinForms.SaveLoad.Models;
- using EnumsNET;
- using NAudio.Wave;
- using Newtonsoft.Json.Linq;
- using AsioOut = Bmp.Core.Lite.Playback.Outputs.NAudioASIO.AsioOut;
- namespace Bmp.WinForms
- {
- internal partial class MainForm : Form
- {
- private const string EMOJI_X = "❌";
- private const string EMOJI_WARN = "⚠";
- private const string EMOJI_PLAY_BIG = "▶";
- private const string EMOJI_PLAY_SMALL = "⏵";
- private const string EMOJI_LOADING = "⏳";
- private static readonly IReadOnlyCollection<string> SupportedDropTypes = new[] { DataFormats.FileDrop, DataFormats.Text };
- private static readonly Random Rng = new();
- private readonly Channel<string> _pendingAddToList = Channel.CreateUnbounded<string>();
- private readonly ILogger<MainForm> _logger;
- private readonly IEventBus _eventBus;
- private readonly SaveLoadService _saveLoadService;
- private bool _isRunning;
- private ListViewItem? _currentListViewItem;
- private IOutputDeviceInfo? _selectedOutputDevice;
- private WaveStream? _inputSource;
- private VisualizeDataMiddleWare? _visualizeDataMiddleWrap;
- private IWavePlayer? _outputDevice;
- private bool _nativeDsd;
- private UIPlaybackState _playbackState;
- private bool _trackBarHolding;
- public MainForm(IEventBus eventBus, SaveLoadService saveLoadService, ILogger<MainForm> logger)
- {
- InitializeComponent();
- _eventBus = eventBus;
- _saveLoadService = saveLoadService;
- _logger = logger;
- Text = Const.AppTitle;
- var eIcon = IconExtractor.GetMainIcon();
- if (null != eIcon) Icon = eIcon;
- UpdatePlaylistModeButton();
- }
- // ----------------- playback control -----------------
- private async Task LoadItemAsync(ListViewItem item)
- {
- MainPanel.Enabled = false;
- await StopAsync();
- _playbackState = UIPlaybackState.Loading;
- _currentListViewItem = item;
- _currentListViewItem.EnsureVisible();
- _currentListViewItem.SubItems[StateColumnHeader.Index].Text = EMOJI_LOADING;
- var finalState = EMOJI_PLAY_BIG;
- var balloonShow = false;
- ToolTipIcon? balloonIcon = null;
- string? balloonTitle = null;
- string? balloonContent = null;
- var flagPlayOk = false;
- try
- {
- try
- {
- await ReloadSource();
- }
- catch (Exception e)
- {
- finalState = EMOJI_X;
- item.ToolTipText =
- item.Name
- + Environment.NewLine
- + "无法加载文件"
- + Environment.NewLine
- + e.Message;
- _playbackState = UIPlaybackState.Error;
- //TODO: 消息机制 错误 无法加载媒体
- return;
- }
- try
- {
- MetaDataWrap? meta = null;
- if (_inputSource is IHaveMetaData hmd) meta = hmd.MetaData;
- meta ??= await Task.Run(() => InputSourceProvider.ReadMeta(item.Name));
- var fileName = Path.GetFileNameWithoutExtension(item.Name);
- if (Uri.TryCreate(item.Name, UriKind.Absolute, out var url))
- {
- fileName = Path.GetFileNameWithoutExtension(url.LocalPath);
- }
- item.SubItems[TitleColumnHeader.Index].Text = meta.Title ?? fileName;
- if (!string.IsNullOrEmpty(meta.Artist)) item.SubItems[TitleColumnHeader.Index].Text += $" - {meta.Artist}";
- if (meta.Album != null && meta.TrackNo.HasValue)
- {
- item.SubItems[TitleColumnHeader.Index].Text = $"{meta.Album} #{meta.TrackNo:00} - {item.SubItems[TitleColumnHeader.Index].Text}";
- }
- item.SubItems[DurColumnHeader.Index].Text = meta.Duration.ToString("hh\\:mm\\:ss");
- var localVarPath = Uri.UnescapeDataString(item.Name);
- item.ToolTipText = localVarPath;
- if (meta.RawTags != null)
- {
- foreach (var pair in meta.RawTags.OrderBy(p => p.Key))
- {
- item.ToolTipText += $"{Environment.NewLine}【{pair.Key}】{pair.Value.Trim()}";
- }
- }
- }
- catch (Exception e)
- {
- finalState = EMOJI_WARN;
- item.ToolTipText =
- item.Name
- + Environment.NewLine
- + "无法获取元数据"
- + Environment.NewLine
- + e.Message;
- //TODO: 消息机制 警告 无法获取元数据
- }
- Play();
- flagPlayOk = true;
- }
- catch (Exception ex)
- {
- finalState = EMOJI_X;
- _playbackState = UIPlaybackState.Error;
- Console.WriteLine(ex);
- //TODO: 消息机制 错误 播放失败
- }
- finally
- {
- MainPanel.Enabled = true;
- if (flagPlayOk == false)
- {
- item.SubItems[StateColumnHeader.Index].Text = finalState;
- if (balloonShow)
- {
- SettingButtonToolTip.ToolTipIcon = balloonIcon ?? ToolTipIcon.None;
- SettingButtonToolTip.ToolTipTitle = balloonTitle;
- if (balloonContent != null)
- {
- SettingButtonToolTip.Show(balloonContent, SettingButton);
- SettingButtonToolTip.Show(balloonContent, SettingButton);
- }
- }
- else
- {
- SettingButtonToolTip.Hide(SettingButton);
- }
- }
- }
- }
- private async Task ReloadSource()
- {
- _inputSource = await Task.Run(() => InputSourceProvider.CreateWaveStream(_currentListViewItem!.Name, _saveLoadService.State.DecodeDsdToPcm == true));
- }
- private void DeInitOutputDevice()
- {
- var local = _outputDevice;
- _outputDevice = null;
- if (local != null)
- {
- local.PlaybackStopped -= _outputDevice_PlaybackStopped;
- local.Stop();
- if (local is AsioOut ao)
- {
- //try reset device
- if (_nativeDsd) ao.SetNativeDsd(false);
- ao.Driver.SetSampleRate(44100);
- }
- local.Dispose();
- }
- }
- private bool ReInitOutputDevice()
- {
- DeInitOutputDevice();
- var finalState = EMOJI_PLAY_BIG;
- var balloonShow = false;
- ToolTipIcon? balloonIcon = null;
- string? balloonTitle = null;
- string? balloonContent = null;
- try
- {
- if (_inputSource is DsdAudioStream)
- {
- _nativeDsd = true;
- if (_saveLoadService.State.SelectedDsdAsioOutputDeviceId == null)
- {
- finalState = EMOJI_X;
- balloonShow = true;
- balloonIcon = ToolTipIcon.Error;
- balloonTitle = "未指定 DSD 输出设备";
- balloonContent = "在设置菜单选择 DSD 输出设备";
- _playbackState = UIPlaybackState.Error;
- return false;
- }
- var allDevices = OutputDeviceProvider.GetAllSupportedDevices();
- var selectedDevice = allDevices.FirstOrDefault(p => p.Id == _saveLoadService.State.SelectedDsdAsioOutputDeviceId);
- if (selectedDevice == null)
- {
- //TODO: 消息机制 错误 找不到指定的输出设备
- balloonShow = true;
- balloonIcon = ToolTipIcon.Warning;
- balloonTitle = "找不到指定的 DSD 输出设备";
- balloonContent = "在设置菜单选择 DSD 输出设备";
- _playbackState = UIPlaybackState.Error;
- return false;
- }
- _selectedOutputDevice = selectedDevice;
- }
- else
- {
- _nativeDsd = false;
- if (_saveLoadService.State.SelectedPcmOutputDeviceId == null)
- {
- //TODO: 消息机制 错误 未指定输出设备
- finalState = EMOJI_X;
- balloonShow = true;
- balloonIcon = ToolTipIcon.Error;
- balloonTitle = "未指定 PCM 输出设备";
- balloonContent = "在设置菜单选择 PCM 输出设备";
- _playbackState = UIPlaybackState.Error;
- return false;
- }
- var allDevices = OutputDeviceProvider.GetAllSupportedDevices();
- var selectedDevice = allDevices.FirstOrDefault(p => p.Id == _saveLoadService.State.SelectedPcmOutputDeviceId);
- if (selectedDevice == null)
- {
- //TODO: 消息机制 错误 找不到指定的输出设备
- balloonShow = true;
- balloonIcon = ToolTipIcon.Warning;
- balloonTitle = "找不到指定的 PCM 输出设备";
- balloonContent = "在设置菜单选择 PCM 输出设备";
- _playbackState = UIPlaybackState.Error;
- return false;
- }
- _selectedOutputDevice = selectedDevice;
- }
- _visualizeDataMiddleWrap = new VisualizeDataMiddleWare(_inputSource!);
- _visualizeDataMiddleWrap.DataTransferred += _visualizeDataMiddleWrap_DataTransferred;
- IWavePlayer od;
- try
- {
- od = OutputDeviceProvider.CreateWavePlayer(_selectedOutputDevice, _nativeDsd);
- }
- catch (Exception e)
- {
- //TODO: 消息机制 错误 无法打开设备
- finalState = EMOJI_X;
- balloonShow = true;
- balloonIcon = ToolTipIcon.Error;
- balloonTitle = "无法启动播放";
- balloonContent = "无法打开输出设备" + Environment.NewLine + e.Message;
- _playbackState = UIPlaybackState.Error;
- return false;
- }
- try
- {
- od.Init(_visualizeDataMiddleWrap);
- }
- catch (Exception e)
- {
- //TODO: 消息机制 错误 无法初始化设备
- finalState = EMOJI_X;
- balloonShow = true;
- balloonIcon = ToolTipIcon.Error;
- balloonTitle = "无法启动播放";
- balloonContent = "无法初始化输出设备" + Environment.NewLine + e.Message;
- return false;
- }
- _outputDevice = od;
- _outputDevice.PlaybackStopped += _outputDevice_PlaybackStopped;
- }
- finally
- {
- _currentListViewItem!.SubItems[StateColumnHeader.Index].Text = finalState;
- if (balloonShow)
- {
- SettingButtonToolTip.ToolTipIcon = balloonIcon ?? ToolTipIcon.None;
- SettingButtonToolTip.ToolTipTitle = balloonTitle;
- if (balloonContent != null)
- {
- SettingButtonToolTip.Show(balloonContent, SettingButton);
- SettingButtonToolTip.Show(balloonContent, SettingButton);
- }
- }
- else
- {
- SettingButtonToolTip.Hide(SettingButton);
- }
- }
- return true;
- }
- private async Task StopAsync()
- {
- _playbackState = UIPlaybackState.Stopped;
- DeInitOutputDevice();
- if (_inputSource != null) await _inputSource.DisposeAsync();
- _inputSource = null;
- if (_currentListViewItem != null)
- {
- var subItem = _currentListViewItem.SubItems[StateColumnHeader.Index];
- if (subItem.Text == EMOJI_PLAY_BIG) subItem.Text = "";
- }
- }
- private void Play()
- {
- if (_inputSource == null) return;
- if (ReInitOutputDevice() == false) return;
- _outputDevice!.Play();
- _playbackState = UIPlaybackState.Playing;
- }
- private void Pause()
- {
- _playbackState = UIPlaybackState.Paused;
- DeInitOutputDevice();
- }
- private async Task TrackPrevAsync()
- {
- var itemsCount = MainListView.Items.Count;
- if (itemsCount == 0)
- {
- //列表为空 下一个鬼,直接停止
- await StopAsync();
- return;
- }
- var playlistMode = _saveLoadService.State.PlaylistMode;
- int? nextIndex; // 下一项,null表示停止
- //当前项已消失时
- // 随机 go random brr
- // 其他 回到第一项
- if (_currentListViewItem?.ListView == null)
- {
- if (itemsCount > 2 && playlistMode == PlaylistMode.Random)
- nextIndex = Rng.Next(itemsCount);
- else
- nextIndex = 0;
- }
- else
- {
- var currentIndex = _currentListViewItem.Index;
- switch (playlistMode)
- {
- default:
- case PlaylistMode.Normal:
- nextIndex = currentIndex - 1;
- // 在第一项往前,无反应
- if (nextIndex.Value < 0) return;
- break;
- case PlaylistMode.LoopList:
- nextIndex = currentIndex - 1;
- // 最后一项播放完,跳到最后一项
- if (nextIndex.Value < 0) nextIndex = itemsCount - 1;
- break;
- case PlaylistMode.LoopTrack:
- //保持不变
- nextIndex = currentIndex;
- break;
- case PlaylistMode.Random:
- //go random brr
- nextIndex = Rng.Next(itemsCount);
- break;
- case PlaylistMode.Once:
- //无条件停止
- nextIndex = null;
- break;
- }
- }
- if (nextIndex.HasValue)
- {
- await LoadItemAsync(MainListView.Items[nextIndex.Value]);
- }
- else
- {
- await StopAsync();
- }
- }
- private async Task TrackNextAsync()
- {
- var itemsCount = MainListView.Items.Count;
- if (itemsCount == 0)
- {
- //列表为空 下一个鬼,直接停止
- await StopAsync();
- return;
- }
- int? nextIndex; // 下一项,null表示停止
- var playlistMode = _saveLoadService.State.PlaylistMode;
- //当前项已消失时
- // 随机 go random brr
- // 其他 回到第一项
- if (_currentListViewItem?.ListView == null)
- {
- if (itemsCount > 2 && playlistMode == PlaylistMode.Random)
- nextIndex = Rng.Next(itemsCount);
- else
- nextIndex = 0;
- }
- else
- {
- var currentIndex = _currentListViewItem.Index;
- switch (playlistMode)
- {
- default:
- case PlaylistMode.Normal:
- nextIndex = currentIndex + 1;
- // 最后一项播放完,停止
- if (nextIndex.Value > itemsCount - 1) nextIndex = null;
- break;
- case PlaylistMode.LoopList:
- nextIndex = currentIndex + 1;
- // 最后一项播放完,回到第一项
- if (nextIndex.Value > itemsCount - 1) nextIndex = 0;
- break;
- case PlaylistMode.LoopTrack:
- //保持不变
- nextIndex = currentIndex;
- break;
- case PlaylistMode.Random:
- //go random brr
- nextIndex = Rng.Next(itemsCount);
- break;
- case PlaylistMode.Once:
- //无条件停止
- nextIndex = null;
- break;
- }
- }
- if (nextIndex.HasValue)
- {
- await LoadItemAsync(MainListView.Items[nextIndex.Value]);
- }
- else
- {
- await StopAsync();
- }
- }
- // ----------------- playback event -----------------
- private void _visualizeDataMiddleWrap_DataTransferred(object? sender, VisualizeDataEventArgs e)
- {
- //TODO: 视觉效果
- }
- private async void _outputDevice_PlaybackStopped(object? sender, StoppedEventArgs e)
- {
- if (_playbackState == UIPlaybackState.Seeking) return;
- if (_playbackState == UIPlaybackState.Paused) return;
- if (e.Exception == null)
- {
- await TrackNextAsync();
- }
- else
- {
- //TODO: 消息机制 错误 因为发生错误而停止
- _playbackState = UIPlaybackState.Error;
- }
- }
- // ----------------- UI func -----------------
- private void SaveState()
- {
- _saveLoadService.State.FormPosition = Location;
- _saveLoadService.State.FormSize = Size;
- _saveLoadService.State.Playlist = MainListView.Items.Cast<ListViewItem>().Select(p => new SaveLoadPlaylistItem
- {
- Path = p.Name,
- Title = p.SubItems[TitleColumnHeader.Index].Text,
- Duration = p.SubItems[DurColumnHeader.Index].Text,
- ToolTip = p.ToolTipText
- }).ToArray();
- _saveLoadService.Save();
- }
- private async void StartProcessPendingAddQueue()
- {
- var reader = _pendingAddToList.Reader;
- while (_isRunning)
- {
- var inputPath1 = await reader.ReadAsync();
- foreach (var path in InputSourceProvider.ExpandPaths(inputPath1))
- {
- var item = new ListViewItem();
- item.Name = path;
- item.ToolTipText = Uri.UnescapeDataString(path);
- var localVarPath = path;
- if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri) && uri.IsFile == false) localVarPath = uri.ToString();
- item.Text = Uri.UnescapeDataString(Path.GetFileName(localVarPath));
- if (string.IsNullOrWhiteSpace(item.Text)) item.Text = localVarPath;
- for (var i = 0; i < MainListView.Columns.Count - 1; i++) item.SubItems.Add("");
- MainListView.Items.Add(item);
- }
- }
- }
- private void AddDebugContent(int i)
- {
- //TODO: UI playlist group by Album, but move sort ???
- var group = new ListViewGroup
- {
- Header = "Grrr" + i,
- Subtitle = "Srrr" + i,
- TitleImageIndex = 0
- };
- MainListView.Groups.Add(group);
- MainListView.Items.Add(new ListViewItem { Group = group, Text = "Brrr1", });
- MainListView.Items.Add(new ListViewItem { Group = group, Text = "Brrr2", });
- MainListView.Items.Add(new ListViewItem { Group = group, Text = "Brrr3", });
- }
- private void ShowSettingContextMenu(int? expandIndex = null)
- {
- var ctx = new ContextMenuStrip();
- var topItem = ctx.Items.Add(" ---------------- 设置 ---------------- ");
- topItem.Alignment = ToolStripItemAlignment.Right;
- topItem.TextAlign = ContentAlignment.MiddleCenter;
- topItem.Enabled = false;
- ctx.Items.Add("-");
- var allSupportedDevices = OutputDeviceProvider.GetAllSupportedDevices();
- var selectedPcmDevice = allSupportedDevices.FirstOrDefault(p => p.Id == _saveLoadService.State.SelectedPcmOutputDeviceId);
- var selectedDsdDevice = allSupportedDevices.FirstOrDefault(p => p.Id == _saveLoadService.State.SelectedDsdAsioOutputDeviceId);
- var pcmOutputSelect = new ToolStripMenuItem($"PCM 输出{(selectedPcmDevice == null ? "(未选择)" : "")}");
- ctx.Items.Add(pcmOutputSelect);
- var dsdOutputSelect = new ToolStripMenuItem($"DSD 输出{(selectedDsdDevice == null ? "(未选择)" : "")}");
- ctx.Items.Add(dsdOutputSelect);
- var dsdToPcm = new ToolStripMenuItem("软解码成PCM");
- if (_saveLoadService.State.DecodeDsdToPcm) dsdToPcm.CheckState = CheckState.Indeterminate;
- dsdOutputSelect.DropDownItems.Add(dsdToPcm);
- dsdToPcm.Click += delegate
- {
- _saveLoadService.State.DecodeDsdToPcm = true;
- _saveLoadService.State.SelectedDsdAsioOutputDeviceId = null;
- ShowSettingContextMenu();
- };
- foreach (var deviceInfo in allSupportedDevices)
- {
- var pcmDeviceItem = new ToolStripMenuItem(deviceInfo.DisplayName);
- pcmOutputSelect.DropDownItems.Add(pcmDeviceItem);
- if (selectedPcmDevice == deviceInfo) pcmDeviceItem.CheckState = CheckState.Indeterminate;
- pcmDeviceItem.Click += delegate
- {
- _saveLoadService.State.SelectedPcmOutputDeviceId = deviceInfo.Id;
- _saveLoadService.State.OutputDeviceLatency = null;
- ShowSettingContextMenu();
- };
- if (deviceInfo.Type == OutputType.ASIO)
- {
- var dsdDeviceItem = new ToolStripMenuItem(deviceInfo.DisplayName);
- dsdOutputSelect.DropDownItems.Add(dsdDeviceItem);
- if (selectedDsdDevice == deviceInfo) dsdDeviceItem.CheckState = CheckState.Indeterminate;
- dsdDeviceItem.Click += delegate
- {
- _saveLoadService.State.SelectedDsdAsioOutputDeviceId = deviceInfo.Id;
- _saveLoadService.State.DecodeDsdToPcm = false;
- ShowSettingContextMenu();
- };
- }
- }
- ctx.Items.Add("-");
- if (selectedPcmDevice == null)
- {
- ctx.Items.Add("(未选择 PCM 输出设备)").Enabled = false;
- }
- else
- {
- if (selectedPcmDevice.CanSetLatency)
- {
- const int mulCount = 4;
- var defaultLatency = selectedPcmDevice.Latency;
- var selectedLatency = _saveLoadService.State.OutputDeviceLatency ?? defaultLatency;
- var latencyMenu = new ToolStripMenuItem("PCM 输出缓冲大小:" + (_saveLoadService.State.OutputDeviceLatency ?? defaultLatency));
- ctx.Items.Add(latencyMenu);
- void SetLatency(int latency)
- {
- _saveLoadService.State.OutputDeviceLatency = latency;
- ShowSettingContextMenu();
- //ShowSettingContextMenu(ctx.Items.IndexOf(latencyMenu));
- }
- for (var i = 0; i < mulCount; i++)
- {
- var latency = defaultLatency * (i + 1);
- var latencyItem = new ToolStripMenuItem(latency.ToString());
- latencyMenu.DropDownItems.Add(latencyItem);
- if (selectedLatency == latency) latencyItem.CheckState = CheckState.Indeterminate;
- latencyItem.Click += delegate { SetLatency(latency); };
- }
- }
- if (selectedPcmDevice.HasControlPanel)
- {
- var toolStripItem = ctx.Items.Add("调出 PCM 设置面板");
- if (_playbackState == UIPlaybackState.Playing)
- {
- toolStripItem.Enabled = false;
- toolStripItem.Text += " (播放时不可用)";
- }
- toolStripItem.Click += delegate
- {
- try
- {
- selectedPcmDevice.ShowControlPanel();
- }
- catch (Exception e)
- {
- //TODO: 消息机制 错误 调出ASIO设置面板
- MessageBox.Show("无法调出 PCM 设置面板" + Environment.NewLine + e.Message, null, MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, 0, false);
- }
- };
- }
- }
- if (selectedDsdDevice == null)
- {
- if (_saveLoadService.State.DecodeDsdToPcm == false)
- {
- ctx.Items.Add("(未选择 DSD 输出设备)").Enabled = false;
- }
- }
- else if (selectedDsdDevice.HasControlPanel)
- {
- var toolStripItem = ctx.Items.Add("调出 DSD 设置面板");
- if (_playbackState == UIPlaybackState.Playing)
- {
- toolStripItem.Enabled = false;
- toolStripItem.Text += " (播放时不可用)";
- }
- toolStripItem.Click += delegate
- {
- try
- {
- selectedDsdDevice.ShowControlPanel();
- }
- catch (Exception e)
- {
- //TODO: 消息机制 错误 调出ASIO设置面板
- MessageBox.Show("无法调出 DSD 设置面板" + Environment.NewLine + e.Message, null, MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, 0, false);
- }
- };
- }
- ctx.Items.Add("-");
- ctx.Items.Add("立即保存状态").Click += delegate { SaveState(); };
- if (expandIndex.HasValue) ctx.Opened += delegate
- {
- if (ctx.Items[expandIndex.Value] is ToolStripMenuItem menuItem) menuItem.ShowDropDown();
- };
- ctx.Show(SettingButton, Point.Empty);
- #if DEBUG
- ctx.Items.Add("-");
- var dbgItem = new ToolStripMenuItem("调试");
- ctx.Items.Add(dbgItem);
- dbgItem.DropDownItems.Add("清除选中 PCM 设备").Click += delegate { if (_saveLoadService is { State: { } }) _saveLoadService.State.SelectedPcmOutputDeviceId = null; };
- dbgItem.DropDownItems.Add("清除选中 DSD 设备").Click += delegate { if (_saveLoadService is { State: { } }) _saveLoadService.State.SelectedDsdAsioOutputDeviceId = null; };
- #endif
- }
- private void UpdatePlaylistModeButton()
- {
- var value = _saveLoadService.State.PlaylistMode;
- var type = value.GetType();
- var name = Enum.GetName(type, value);
- if (name != null)
- {
- var d = type.GetField(name)?.GetCustomAttribute<DisplayAttribute>();
- PlaylistModeButton.Text = d?.ShortName;
- }
- else
- {
- PlaylistModeButton.Text = "?";
- }
- }
- // ----------------- UI event: Form -----------------
- private void MainForm_Shown(object sender, EventArgs e)
- {
- Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
- //var lvi = new ListViewItem();
- //AlbumImageList.Images.Add(this.Icon);
- //AddContent(1);
- //AddContent(2);
- MainListView.Items.Clear();
- if (_saveLoadService.State.FormPosition.HasValue) Location = _saveLoadService.State.FormPosition.Value;
- Application.DoEvents();
- if (_saveLoadService.State.FormSize.HasValue) Size = _saveLoadService.State.FormSize.Value;
- if (_saveLoadService.State.Playlist != null)
- {
- foreach (var playlistItem in _saveLoadService.State.Playlist)
- {
- var item = new ListViewItem();
- for (var i = 0; i < MainListView.Columns.Count - 1; i++) item.SubItems.Add("");
- item.Name = playlistItem.Path;
- item.SubItems[TitleColumnHeader.Index].Text = playlistItem.Title;
- item.SubItems[DurColumnHeader.Index].Text = playlistItem.Duration;
- item.ToolTipText = playlistItem.ToolTip;
- MainListView.Items.Add(item);
- }
- }
- _isRunning = true;
- StartProcessPendingAddQueue();
- }
- private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
- {
- _isRunning = false;
- SaveState();
- }
- // ----------------- UI event: ListView -----------------
- private void MainListView_ColumnWidthChanging(object sender, ColumnWidthChangingEventArgs e)
- {
- if (MainListView.Columns[e.ColumnIndex] == StateColumnHeader)
- {
- e.Cancel = true;
- e.NewWidth = StateColumnHeader.Width;
- }
- if (MainListView.Columns[e.ColumnIndex] == DurColumnHeader)
- {
- e.Cancel = true;
- e.NewWidth = DurColumnHeader.Width;
- }
- }
- private async void MainListView_ItemActivate(object sender, EventArgs e)
- {
- if (MainListView.SelectedItems.Count > 0)
- {
- var item = MainListView.SelectedItems[0];
- await LoadItemAsync(item);
- }
- }
- private void MainListView_SizeChanged(object sender, EventArgs e)
- {
- TitleColumnHeader.Width = MainListView.Width - DurColumnHeader.Width - StateColumnHeader.Width - 50;
- }
- private void MainListView_KeyDown(object sender, KeyEventArgs e)
- {
- if (e.KeyCode == Keys.Delete)
- {
- var selectedItems = MainListView.SelectedItems.Cast<ListViewItem>().ToArray();
- foreach (var item in selectedItems) MainListView.Items.Remove(item);
- }
- }
- private void MainListView_ItemDrag(object sender, ItemDragEventArgs e)
- {
- var selectedItems = MainListView.SelectedItems.Cast<ListViewItem>().ToArray();
- if (selectedItems.Length > 0) MainListView.DoDragDrop(selectedItems, DragDropEffects.Move);
- }
- private void MainListView_DragEnter(object sender, DragEventArgs e)
- {
- e.Effect = e.Data?.GetData(typeof(ListViewItem[])) is ListViewItem[]? DragDropEffects.Move
- : e.Data?.GetFormats().Any(SupportedDropTypes.Contains) == true
- ? DragDropEffects.Link
- : DragDropEffects.None;
- }
- private void MainListView_DragOver(object sender, DragEventArgs e)
- {
- if (e.Data?.GetDataPresent(typeof(ListViewItem[])) == true)
- {
- var point = MainListView.PointToClient(new Point(e.X, e.Y));
- var targetIndex = MainListView.InsertionMark.NearestIndex(point);
- if (targetIndex == -1)
- {
- MainListView.InsertionMark.Index = -1;
- }
- else if (targetIndex >= 0 && targetIndex < MainListView.Items.Count)
- {
- MainListView.InsertionMark.Index = targetIndex;
- }
- }
- }
- private void MainListView_DragLeave(object sender, EventArgs e)
- {
- MainListView.InsertionMark.Index = -1;
- }
- private void MainListView_DragDrop(object sender, DragEventArgs e)
- {
- MainListView.InsertionMark.Index = -1;
- if (e.Data?.GetDataPresent(typeof(ListViewItem[])) == true
- && e.Data.GetData(typeof(ListViewItem[])) is ListViewItem[] items && items.Length > 0)
- {
- var point = MainListView.PointToClient(new Point(e.X, e.Y));
- var targetIndex = MainListView.InsertionMark.NearestIndex(point);
- var isMoveDown = items[0].Index < targetIndex;
- if (isMoveDown) --targetIndex;
- if (targetIndex >= 0 && targetIndex < MainListView.Items.Count)
- {
- foreach (var item in items)
- {
- MainListView.Items.Remove(item);
- }
- targetIndex = Math.Min(targetIndex, MainListView.Items.Count);
- for (var i = 0; i < items.Length; i++)
- {
- MainListView.Items.Insert(targetIndex + i, items[i]);
- }
- }
- return;
- }
- var listAllPath = new List<string>();
- if (e.Data?.GetDataPresent(DataFormats.FileDrop) == true)
- {
- var arrFilePath = (string[]?)e.Data.GetData(DataFormats.FileDrop);
- if (arrFilePath == null) return;
- foreach (var s in arrFilePath)
- {
- if (Directory.Exists(s))
- {
- listAllPath.AddRange(Directory.GetFiles(s, "*", SearchOption.AllDirectories));
- }
- else
- {
- listAllPath.Add(s);
- }
- }
- }
- if (e.Data?.GetDataPresent(DataFormats.Text) == true)
- {
- var text = (string?)e.Data.GetData(DataFormats.Text);
- if (text == null) return;
- listAllPath.Add(text);
- }
- var writer = _pendingAddToList.Writer;
- foreach (var path in listAllPath)
- {
- writer.WriteAsync(path);
- }
- }
- private void MainContextMenu_Opening(object sender, CancelEventArgs e)
- {
- MainContextMenu.Items.Clear();
- var moveTop = new ToolStripMenuItem("置顶");
- var moveUp = new ToolStripMenuItem("上移");
- var moveDown = new ToolStripMenuItem("下移");
- var moveBottom = new ToolStripMenuItem("置底");
- var clearSelected = new ToolStripMenuItem("清空选中");
- var clearAll = new ToolStripMenuItem("清空全部");
- MainContextMenu.Items.Add(moveTop);
- MainContextMenu.Items.Add(moveUp);
- MainContextMenu.Items.Add(moveDown);
- MainContextMenu.Items.Add(moveBottom);
- MainContextMenu.Items.Add("-");
- MainContextMenu.Items.Add(clearSelected);
- MainContextMenu.Items.Add(clearAll);
- var isAnyContain = MainListView.Items.Count != 0;
- var isAnySelected = MainListView.SelectedItems.Count != 0;
- var isMultiSelected = MainListView.SelectedItems.Count > 1;
- var isAllSelected = MainListView.SelectedItems.Count == MainListView.Items.Count;
- var isTopSelected = MainListView.SelectedIndices.Contains(0);
- var isBottomSelected = MainListView.SelectedIndices.Contains(MainListView.Items.Count - 1);
- moveTop.Enabled = isAnySelected && !isAllSelected && !isTopSelected;
- moveUp.Enabled = isAnySelected && !isAllSelected && !isTopSelected;
- moveDown.Enabled = isAnySelected && !isAllSelected && !isBottomSelected;
- moveBottom.Enabled = isAnySelected && !isAllSelected && !isBottomSelected;
- clearAll.Enabled = isAnyContain;
- clearSelected.Enabled = isAnySelected;
- moveTop.Click += delegate
- {
- var selectedItems = MainListView.SelectedItems.Cast<ListViewItem>().ToArray();
- var targetIndex = 0;
- foreach (var item in selectedItems) MainListView.Items.Remove(item);
- for (var i = 0; i < selectedItems.Length; i++) MainListView.Items.Insert(targetIndex + i, selectedItems[i]);
- };
- moveUp.Click += delegate
- {
- var selectedItems = MainListView.SelectedItems.Cast<ListViewItem>().ToArray();
- var targetIndex = selectedItems[0].Index - 1;
- foreach (var item in selectedItems) MainListView.Items.Remove(item);
- for (var i = 0; i < selectedItems.Length; i++) MainListView.Items.Insert(targetIndex + i, selectedItems[i]);
- };
- moveDown.Click += delegate
- {
- var selectedItems = MainListView.SelectedItems.Cast<ListViewItem>().ToArray();
- ListViewItem? nextItem = null;
- for (var i = selectedItems[0].Index; i < MainListView.Items.Count; i++)
- {
- if (MainListView.Items[i].Selected == false)
- {
- nextItem = MainListView.Items[i];
- break;
- }
- }
- if (nextItem == null) return;
- foreach (var item in selectedItems) MainListView.Items.Remove(item);
- var targetIndex = nextItem.Index + 1;
- if (targetIndex > MainListView.Items.Count) targetIndex = MainListView.Items.Count;
- for (var i = 0; i < selectedItems.Length; i++) MainListView.Items.Insert(targetIndex + i, selectedItems[i]);
- };
- moveBottom.Click += delegate
- {
- var selectedItems = MainListView.SelectedItems.Cast<ListViewItem>().ToArray();
- foreach (var item in selectedItems) MainListView.Items.Remove(item);
- var targetIndex = MainListView.Items.Count;
- for (var i = 0; i < selectedItems.Length; i++) MainListView.Items.Insert(targetIndex + i, selectedItems[i]);
- };
- clearSelected.Click += delegate
- {
- foreach (var selectedItem in MainListView.SelectedItems.Cast<ListViewItem>().ToArray()) MainListView.Items.Remove(selectedItem);
- };
- clearAll.Click += delegate
- {
- MainListView.Items.Clear();
- };
- }
- // ----------------- UI event: Buttons -----------------
- private void SettingButton_Click(object sender, EventArgs e) => ShowSettingContextMenu();
- private async void StopButton_Click(object sender, EventArgs e) => await StopAsync();
- private async void PlayButton_Click(object sender, EventArgs e)
- {
- if (_playbackState == UIPlaybackState.Paused)
- {
- Play();
- return;
- }
- var selectedItem = MainListView.SelectedItems.Cast<ListViewItem>().FirstOrDefault();
- if (selectedItem != null) await LoadItemAsync(selectedItem);
- }
- private void PauseButton_Click(object sender, EventArgs e) => Pause();
- private async void PrevButton_Click(object sender, EventArgs e) => await TrackPrevAsync();
- private async void NextButton_Click(object sender, EventArgs e) => await TrackNextAsync();
- private void PlaylistModeButton_Paint(object sender, PaintEventArgs e)
- {
- // Draw the arrow glyph on the right side of the button
- var arrowX = PlaylistModeButton.ClientRectangle.Width - 15;
- var arrowY = PlaylistModeButton.ClientRectangle.Height / 2 + 12;
- var arrowBrush = PlaylistModeButton.Enabled ? SystemBrushes.ControlText : SystemBrushes.ButtonShadow;
- var arrows = new[]
- {
- new PointF(arrowX, arrowY),
- new PointF(arrowX + 10, arrowY),
- new PointF(arrowX + 5 , arrowY +5 )
- };
- e.Graphics.FillPolygon(arrowBrush, arrows);
- }
- private void PlaylistModeButton_Click(object sender, EventArgs e)
- {
- var cm = new ContextMenuStrip { ShowItemToolTips = true };
- foreach (var item in typeof(PlaylistMode).GetFields(BindingFlags.Public | BindingFlags.Static))
- {
- var attr = item.GetCustomAttribute<DisplayAttribute>();
- if (attr == null) continue;
- var mi = new ToolStripMenuItem
- {
- Text = attr.Name,
- ToolTipText = attr.Description
- };
- var value = (PlaylistMode)item.GetRawConstantValue()!;
- if (_saveLoadService.State.PlaylistMode == value) mi.CheckState = CheckState.Indeterminate;
- mi.Click += delegate
- {
- _saveLoadService.State.PlaylistMode = value;
- UpdatePlaylistModeButton();
- };
- cm.Items.Add(mi);
- }
- cm.Show(PlaylistModeButton, 0, PlaylistModeButton.Height);
- }
- // ----------------- UI event: Others -----------------
- private void UpdateTimer_Tick(object sender, EventArgs e)
- {
- if (_outputDevice is AsioOut ao && ao.HasReachedEnd)
- {
- TrackNextAsync();
- }
- var sb = new StringBuilder();
- //播放状态
- sb.Append(_playbackState.AsString(EnumFormat.Description));
- //进度
- var dur = _inputSource?.TotalTime;
- var cur = _inputSource?.CurrentTime;
- if (dur.HasValue && cur.HasValue)
- {
- sb.Append(" | ");
- var tsDur = dur.Value;
- var tsCur = cur.Value;
- sb.Append($"{tsCur:hh\\:mm\\:ss}.{tsCur.Milliseconds / 100:0}/{tsDur:hh\\:mm\\:ss}.{tsDur.Milliseconds / 100:0}");
- var durMs = (int)tsDur.TotalMilliseconds;
- var curMs = (int)cur.Value.TotalMilliseconds;
- if (_trackBarHolding == false)
- {
- SeekTrackBar.Maximum = durMs;
- if (curMs > durMs) curMs = durMs;
- SeekTrackBar.Value = curMs;
- }
- }
- //参数
- if (_inputSource != null)
- {
- sb.Append(" | ");
- if (_nativeDsd) sb.Append("DSD ");
- var format = _inputSource.WaveFormat;
- var rawBit = _inputSource is IHaveBitPerRawSample bpr ? bpr.BitPerRawSample : null;
- var decBit = format.BitsPerSample;
- if (format.Encoding == WaveFormatEncoding.IeeeFloat)
- {
- sb.Append("IeeeFloat ");
- }
- if (format.Encoding == WaveFormatEncoding.Pcm)
- {
- sb.Append("PCM ");
- }
- sb.Append($"{format.AverageBytesPerSecond * 8 / 1000.0:N0}kbps ");
- if (rawBit.HasValue && rawBit != 0 && rawBit != decBit)
- {
- sb.Append($"{decBit}({rawBit})bit");
- }
- else
- {
- sb.Append($"{decBit}bit");
- }
- var unit = "K";
- var sampleRate = format.SampleRate;
- if (sampleRate > 1000000)
- {
- sampleRate /= 1000;
- unit = "M";
- }
- sb.Append(sampleRate % 1000 == 0
- ? $"/{sampleRate / 1000.0}{unit}Hz"
- : $"/{Math.Floor((float)sampleRate / 100) / 10.0:0.0}{unit}Hz");
- }
- //输出设备
- if (_selectedOutputDevice != null && _playbackState == UIPlaybackState.Playing)
- {
- sb.Append(" | ");
- if (_outputDevice is AsioOut asioOut)
- {
- try
- {
- var type = asioOut.Driver.Capabilities.OutputChannelInfos.Select(p => (object)p.type).FirstOrDefault();
- if (type != null) sb.Append($"{type} ");
- }
- catch (Exception exception)
- {
- //TODO: 异常处理 UpdateTimer_Tick asioOut.Driver.Capabilities.OutputChannelInfos
- Console.WriteLine(exception);
- }
- }
- sb.Append($"{_selectedOutputDevice.DisplayName}");
- }
- //解码器 编码 容器
- if (_inputSource is IHaveDecoderInfo dn)
- {
- sb.Append($" | {dn.DecoderName} {dn.FileFormat}");
- }
- StatusBarLabel.Text = sb.ToString();
- }
- private void SeekTrackBar_MouseDown(object sender, MouseEventArgs e)
- {
- _trackBarHolding = true;
- }
- private void SeekTrackBar_ValueChanged(object sender, EventArgs e)
- {
- if (_trackBarHolding == false) return;
- var timeSpan = TimeSpan.FromMilliseconds(SeekTrackBar.Value);
- var text = $"{timeSpan.Hours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}.{timeSpan.Milliseconds:000}";
- SeekTrackBarToolTip.Show(text, SeekTrackBar, 0, -SeekTrackBar.Height / 2);
- }
- private async void SeekTrackBar_MouseUp(object sender, MouseEventArgs e)
- {
- SeekTrackBarToolTip.Hide(SeekTrackBar);
- SeekTrackBarToolTip.RemoveAll();
- MainPanel.Enabled = false;
- if (_inputSource != null)
- {
- try
- {
- Pause();
- _playbackState = UIPlaybackState.Seeking;
- var ms = SeekTrackBar.Value;
- var seekTo = TimeSpan.FromMilliseconds(ms);
- await Task.Run(() => _inputSource.CurrentTime = seekTo);
- //read a cluster for detect seek pos
- _inputSource.ReadBytes(_inputSource.BlockAlign);
- //if content not support seek, reload and read&discard until target pos
- if (seekTo - _inputSource.CurrentTime > TimeSpan.FromMilliseconds(500))
- {
- await ReloadSource();
- await Task.Run(() =>
- {
- do
- {
- var count = _inputSource.ReadBytes(_inputSource.BlockAlign);
- if (count.Length == 0) break;
- } while (_inputSource != null && _inputSource.CurrentTime < seekTo && _playbackState == UIPlaybackState.Seeking);
- });
- }
- Play();
- }
- catch (Exception exception)
- {
- //TODO: 消息机制 错误 跳转发生错误 SeekTrackBar_MouseUp
- Console.WriteLine(exception);
- _playbackState = UIPlaybackState.Error;
- }
- }
- MainPanel.Enabled = true;
- _trackBarHolding = false;
- }
- }
- internal enum PlaylistMode
- {
- [Display(ShortName = "⇶", Name = "正常", Description = "播放完当前项自动播放下一项,直到列表最后一项播放完,停止")]
- Normal = 0,
- [Display(ShortName = "🔁", Name = "列表循环", Description = "无限循环整个播放列表")]
- LoopList = 1,
- [Display(ShortName = "🔂", Name = "单曲循环", Description = "无限循环当前项")]
- LoopTrack = 2,
- [Display(ShortName = "🔀", Name = "随机", Description = "无限随机整个播放列表")]
- Random = 3,
- [Display(ShortName = "⤞", Name = "一次", Description = "播放完当前项,停止")]
- Once = 4,
- }
- internal enum UIPlaybackState
- {
- [Description("就绪")]
- Ready,
- [Description("正在加载")]
- Loading,
- [Description("正在播放")]
- Playing,
- [Description("正在跳转")]
- Seeking,
- [Description("暂停")]
- Paused,
- [Description("停止")]
- Stopped,
- [Description("错误")]
- Error,
- }
- }
|