diff --git a/BassBoom.Basolia/File/FileTools.cs b/BassBoom.Basolia/File/FileTools.cs index e76e281..b2fcc91 100644 --- a/BassBoom.Basolia/File/FileTools.cs +++ b/BassBoom.Basolia/File/FileTools.cs @@ -98,7 +98,7 @@ public static void OpenFile(string path) throw new BasoliaException("Can't open file", mpg123_errors.MPG123_ERR); isOpened = true; } - currentFile = new(false, path); + currentFile = new(false, path, null, null, ""); } /// @@ -123,7 +123,9 @@ public static async Task OpenUrlAsync(string path) throw new BasoliaException("Provide a path to a music file or a radio station", mpg123_errors.MPG123_BAD_FILE); // Check to see if the radio station exists + ShoutcastServer.client.DefaultRequestHeaders.Add("Icy-MetaData", "1"); var reply = await ShoutcastServer.client.GetAsync(path, HttpCompletionOption.ResponseHeadersRead); + ShoutcastServer.client.DefaultRequestHeaders.Remove("Icy-MetaData"); if (!reply.IsSuccessStatusCode) throw new BasoliaException($"This radio station doesn't exist. Error code: {(int)reply.StatusCode} ({reply.StatusCode}).", mpg123_errors.MPG123_BAD_FILE); @@ -142,7 +144,7 @@ public static async Task OpenUrlAsync(string path) isOpened = true; isRadioStation = true; } - currentFile = new(true, path); + currentFile = new(true, path, reply.Content.ReadAsStreamAsync().Result, reply.Headers, reply.Headers.GetValues("icy-name").First()); // If necessary, feed. PlaybackTools.FeedRadio(); @@ -174,7 +176,6 @@ public static void CloseFile() isOpened = false; isRadioStation = false; currentFile = null; - PlaybackTools.radioStream = null; } } } diff --git a/BassBoom.Basolia/File/FileType.cs b/BassBoom.Basolia/File/FileType.cs index 61e06b6..e8c0d17 100644 --- a/BassBoom.Basolia/File/FileType.cs +++ b/BassBoom.Basolia/File/FileType.cs @@ -18,6 +18,8 @@ // using System; +using System.IO; +using System.Net.Http.Headers; namespace BassBoom.Basolia.File { @@ -28,6 +30,9 @@ public class FileType { private bool isLink; private string path; + private Stream stream; + private HttpResponseHeaders headers; + private string stationName; /// /// Is this file type a link? @@ -41,10 +46,31 @@ public class FileType public string Path => path; - internal FileType(bool isLink, string path) + /// + /// Radio station stream + /// + public Stream Stream => + stream; + + /// + /// Radio station ICY headers + /// + public HttpResponseHeaders Headers => + headers; + + /// + /// Radio station name + /// + public string StationName => + stationName; + + internal FileType(bool isLink, string path, Stream stream, HttpResponseHeaders headers, string stationName) { this.isLink = isLink; this.path = path ?? throw new ArgumentNullException(nameof(path)); + this.stream = stream; + this.headers = headers; + this.stationName = stationName; } } } diff --git a/BassBoom.Basolia/Playback/PlaybackTools.cs b/BassBoom.Basolia/Playback/PlaybackTools.cs index ba348db..1d564ea 100644 --- a/BassBoom.Basolia/Playback/PlaybackTools.cs +++ b/BassBoom.Basolia/Playback/PlaybackTools.cs @@ -33,6 +33,8 @@ using BassBoom.Basolia.Radio; using System.Net.Http; using System.IO; +using System.Linq; +using System.Text; namespace BassBoom.Basolia.Playback { @@ -43,7 +45,7 @@ public static class PlaybackTools { internal static bool bufferPlaying = false; internal static bool holding = false; - internal static Stream radioStream = null; + internal static string radioIcy = ""; private static PlaybackState state = PlaybackState.Stopped; /// @@ -58,6 +60,12 @@ public static class PlaybackTools public static PlaybackState State => state; + /// + /// Current radio ICY metadata + /// + public static string RadioIcy => + radioIcy; + /// /// Plays the currently open file (synchronous) /// @@ -166,7 +174,8 @@ public static void Stop() // Stop the music and seek to the beginning state = PlaybackState.Stopped; - PlaybackPositioningTools.SeekToTheBeginning(); + if (!FileTools.IsRadioStation) + PlaybackPositioningTools.SeekToTheBeginning(); } /// @@ -323,34 +332,48 @@ public static (long, double) GetNativeState(mpg123_state state) } } - internal static int FeedRadio() + internal static void FeedRadio() { - if (FileTools.IsOpened && FileTools.IsRadioStation) + if (!FileTools.IsOpened || !FileTools.IsRadioStation) + return; + + unsafe { - unsafe + var handle = Mpg123Instance._mpg123Handle; + + // Get the MP3 frame length first + string metaIntStr = FileTools.CurrentFile.Headers.GetValues("icy-metaint").First(); + int metaInt = int.Parse(metaIntStr); + + // Now, get the MP3 frame + byte[] buffer = new byte[metaInt]; + int numBytesRead = 0; + int numBytesToRead = metaInt; + do { - var handle = Mpg123Instance._mpg123Handle; - if (radioStream is null) - { - ShoutcastServer.client.DefaultRequestHeaders.Add("Icy-MetaData", "1"); - var reply = ShoutcastServer.client.GetAsync(FileTools.CurrentFile.Path, HttpCompletionOption.ResponseHeadersRead).Result; - ShoutcastServer.client.DefaultRequestHeaders.Remove("Icy-MetaData"); - if (!reply.IsSuccessStatusCode) - throw new BasoliaException($"This radio station doesn't exist. Error code: {(int)reply.StatusCode} ({reply.StatusCode}).", mpg123_errors.MPG123_BAD_FILE); - radioStream = reply.Content.ReadAsStreamAsync().Result; - } - byte[] buffer = new byte[8192]; - radioStream.Read(buffer, 0, buffer.Length); - IntPtr data = Marshal.AllocHGlobal(buffer.Length); - Marshal.Copy(buffer, 0, data, buffer.Length); - int feedResult = NativeInput.mpg123_feed(handle, data, buffer.Length); - if (feedResult != (int)mpg123_errors.MPG123_OK) - throw new BasoliaException("Can't feed.", mpg123_errors.MPG123_ERR); - return buffer.Length; - } + int n = FileTools.CurrentFile.Stream.Read(buffer, numBytesRead, 1); + numBytesRead += n; + numBytesToRead -= n; + } while (numBytesToRead > 0); + + // Fetch the metadata. + int lengthOfMetaData = FileTools.CurrentFile.Stream.ReadByte(); + int metaBytesToRead = lengthOfMetaData * 16; + Debug.WriteLine($"Buffer: {lengthOfMetaData} [{metaBytesToRead}]"); + byte[] metadataBytes = new byte[metaBytesToRead]; + FileTools.CurrentFile.Stream.Read(metadataBytes, 0, metaBytesToRead); + string icy = Encoding.UTF8.GetString(metadataBytes).Replace("\0", "").Trim(); + if (!string.IsNullOrEmpty(icy)) + radioIcy = icy; + Debug.WriteLine($"{radioIcy}\n"); + + // Copy the data to MPG123 + IntPtr data = Marshal.AllocHGlobal(buffer.Length); + Marshal.Copy(buffer, 0, data, buffer.Length); + int feedResult = NativeInput.mpg123_feed(handle, data, buffer.Length); + if (feedResult != (int)mpg123_errors.MPG123_OK) + throw new BasoliaException("Can't feed.", mpg123_errors.MPG123_ERR); } - else - return 0; } internal static int PlayBuffer(byte[] buffer) diff --git a/BassBoom.Cli/CliBase/Radio.cs b/BassBoom.Cli/CliBase/Radio.cs index 9d05e2d..02fee21 100644 --- a/BassBoom.Cli/CliBase/Radio.cs +++ b/BassBoom.Cli/CliBase/Radio.cs @@ -54,7 +54,6 @@ internal static class Radio internal static bool populate = true; internal static bool paused = false; internal static bool failedToPlay = false; - internal static string icyMetadata = ""; internal static readonly List stationUrls = []; internal static readonly List cachedInfos = []; internal static Version mpgVer; @@ -82,13 +81,20 @@ public static void RadioLoop() // Handle drawing screenPart.AddDynamicText(HandleDraw); - // Current duration + // Current volume + int hue = 0; screenPart.AddDynamicText(() => { var buffer = new StringBuilder(); string indicator = $"Volume: {volume:0.00}"; + if (PlaybackTools.Playing) + { + hue++; + if (hue >= 360) + hue = 0; + } buffer.Append( - BoxFrameColor.RenderBoxFrame(2, ConsoleWrapper.WindowHeight - 8, ConsoleWrapper.WindowWidth - 6, 1) + + BoxFrameColor.RenderBoxFrame(2, ConsoleWrapper.WindowHeight - 8, ConsoleWrapper.WindowWidth - 6, 1, PlaybackTools.Playing ? new Color($"hsl:{hue};50;50") : new Color(ConsoleColors.White)) + TextWriterWhereColor.RenderWhere(indicator, ConsoleWrapper.WindowWidth - indicator.Length - 3, ConsoleWrapper.WindowHeight - 9, ConsoleColors.White, ConsoleColors.Black) ); return buffer.ToString(); @@ -341,7 +347,7 @@ private static string HandleDraw() for (int i = 0; i < stationUrls.Count; i++) { // Populate the first pane - string stationName = "Station name"; + string stationName = cachedInfos[i].MetadataIcy; string duration = cachedInfos[i].DurationSpan; string stationPreview = $"[{duration}] {stationName}"; choices.Add(new($"{i + 1}", stationPreview)); diff --git a/BassBoom.Cli/CliBase/RadioControls.cs b/BassBoom.Cli/CliBase/RadioControls.cs index be753ca..9131053 100644 --- a/BassBoom.Cli/CliBase/RadioControls.cs +++ b/BassBoom.Cli/CliBase/RadioControls.cs @@ -31,6 +31,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using Terminaux.Base; using Terminaux.Colors.Data; @@ -149,27 +150,25 @@ internal static void PopulateRadioStationInfo(string musicPath) if (PlaybackTools.Playing || !Radio.populate) return; Radio.populate = false; - if (!TryOpenStation(musicPath)) - return; - FileTools.OpenUrl(musicPath); if (Radio.cachedInfos.Any((csi) => csi.MusicPath == musicPath)) { var instance = Radio.cachedInfos.Single((csi) => csi.MusicPath == musicPath); Radio.formatInfo = instance.FormatInfo; Radio.frameInfo = instance.FrameInfo; - Radio.icyMetadata = instance.MetadataIcy; if (!Radio.stationUrls.Contains(musicPath)) Radio.stationUrls.Add(musicPath); } else { + if (!TryOpenStation(musicPath)) + return; InfoBoxColor.WriteInfoBox($"Loading BassBoom to open {musicPath}...", false); + FileTools.OpenUrl(musicPath); Radio.formatInfo = FormatTools.GetFormatInfo(); Radio.frameInfo = AudioInfoTools.GetFrameInfo(); - Radio.icyMetadata = AudioInfoTools.GetIcyMetadata(); // Try to open the lyrics - var instance = new CachedSongInfo(musicPath, null, null, -1, Radio.formatInfo, Radio.frameInfo, null, Radio.icyMetadata); + var instance = new CachedSongInfo(musicPath, null, null, -1, Radio.formatInfo, Radio.frameInfo, null, FileTools.CurrentFile.StationName); Radio.cachedInfos.Add(instance); } TextWriterWhereColor.WriteWhere(new string(' ', ConsoleWrapper.WindowWidth), 0, 1); @@ -180,7 +179,10 @@ internal static void PopulateRadioStationInfo(string musicPath) internal static string RenderStationName() { // Render the station name - string icy = Radio.icyMetadata; + string icy = PlaybackTools.RadioIcy; + if (icy.Length == 0) + return ""; + icy = Regex.Match(icy, @"StreamTitle='((?:[^']|\\')*)'").Groups[1].Value.Trim().Replace("\\'", "'"); // Print the music name return CenteredTextColor.RenderCentered(1, "Now playing: {0}", ConsoleColors.White, ConsoleColors.Black, icy);