diff --git a/MPfm/MPfm.MVP/Presenters/PlayerPresenter.cs b/MPfm/MPfm.MVP/Presenters/PlayerPresenter.cs index 0be4aff3..d4528239 100644 --- a/MPfm/MPfm.MVP/Presenters/PlayerPresenter.cs +++ b/MPfm/MPfm.MVP/Presenters/PlayerPresenter.cs @@ -128,18 +128,6 @@ void HandleTimerRefreshSongPositionElapsed(object sender, ElapsedEventArgs e) View.RefreshPlayerPosition(entity); } - /// - /// Handles the player playlist index changed event. - /// - /// - /// Playlist index changed data. - /// - protected void HandlePlayerOnPlaylistIndexChanged(PlayerPlaylistIndexChangedData data) - { - // Refresh song information - RefreshSongInformation(playerService.CurrentPlaylistItem.AudioFile); - } - /// /// Starts playback. /// @@ -266,7 +254,7 @@ public void Next() // Refresh controls Tracing.Log("PlayerPresenter.Next -- Refreshing song information..."); - RefreshSongInformation(playerService.CurrentPlaylistItem.AudioFile); + //RefreshSongInformation(playerService.CurrentPlaylistItem.AudioFile); } catch(Exception ex) { @@ -286,7 +274,7 @@ public void Previous() playerService.Previous(); // Refresh controls - RefreshSongInformation(playerService.CurrentPlaylistItem.AudioFile); + //RefreshSongInformation(playerService.CurrentPlaylistItem.AudioFile); Tracing.Log("PlayerPresenter.Previous -- Refreshing song information..."); } catch(Exception ex) diff --git a/MPfm/MPfm.Sound/PeakFiles/PeakFileGenerator.cs b/MPfm/MPfm.Sound/PeakFiles/PeakFileGenerator.cs index 576e6f81..5d08ed95 100644 --- a/MPfm/MPfm.Sound/PeakFiles/PeakFileGenerator.cs +++ b/MPfm/MPfm.Sound/PeakFiles/PeakFileGenerator.cs @@ -157,7 +157,7 @@ public void GeneratePeakFile(string audioFilePath, string peakFilePath) audioFileLength /= 2; // Check if peak file exists - if (File.Exists(peakFilePath)) + if (File.Exists(peakFilePath)) { // Delete peak file File.Delete(peakFilePath); @@ -189,7 +189,6 @@ public void GeneratePeakFile(string audioFilePath, string peakFilePath) // Create buffer data = Marshal.AllocHGlobal(chunkSize); - //buffer = new byte[chunkSize]; buffer = new float[chunkSize]; // Is an event binded to OnProcessData? @@ -211,9 +210,12 @@ public void GeneratePeakFile(string audioFilePath, string peakFilePath) if (cancellationToken.IsCancellationRequested) { // Set flags, exit loop + Console.WriteLine("PeakFileGenerator - Cancelling..."); cancelled = true; IsLoading = false; - OnProcessDone(new PeakFileDoneData()); + OnProcessDone(new PeakFileDoneData() { + Cancelled = true + }); break; } @@ -234,12 +236,11 @@ public void GeneratePeakFile(string audioFilePath, string peakFilePath) if (a % 2 == 0) { // Left channel - floatLeft[a / 2] = buffer[a]; - } - else + floatLeft [a / 2] = buffer [a]; + } else { // Left channel - floatRight[a / 2] = buffer[a]; + floatRight [a / 2] = buffer [a]; } } @@ -274,14 +275,12 @@ public void GeneratePeakFile(string audioFilePath, string peakFilePath) OnProcessData(dataProgress); // Reset min/max list - //listMinMaxForProgressData.Clear(); listMinMaxForProgressData = new List(); } // Increment current block currentBlock++; - } - while (read == chunkSize); + } while (read == chunkSize); // Free channel channelDecode.Free(); @@ -292,14 +291,12 @@ public void GeneratePeakFile(string audioFilePath, string peakFilePath) floatRight = null; buffer = null; minMax = null; - } - catch (Exception ex) + } catch (Exception ex) { // Return exception //e.Result = ex; throw ex; - } - finally + } finally { // Close writer and stream gzipStream.Close(); @@ -321,8 +318,7 @@ public void GeneratePeakFile(string audioFilePath, string peakFilePath) { // Delete file File.Delete(peakFilePath); - } - catch + } catch { // Just skip this step. Tracing.Log("Could not delete peak file " + peakFilePath + "."); @@ -333,8 +329,10 @@ public void GeneratePeakFile(string audioFilePath, string peakFilePath) // Set completed IsLoading = false; - OnProcessDone(new PeakFileDoneData()); - }); + OnProcessDone(new PeakFileDoneData() { + Cancelled = false + }); + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); } /// @@ -342,8 +340,9 @@ public void GeneratePeakFile(string audioFilePath, string peakFilePath) /// public void Cancel() { - if(IsLoading) - cancellationTokenSource.Cancel(); + if (IsLoading) + if(cancellationTokenSource != null) + cancellationTokenSource.Cancel(); } /// @@ -362,6 +361,7 @@ public List ReadPeakFile(string peakFilePath) long audioFileLength = 0; int chunkSize = 0; int numberOfBlocks = 0; + int currentBlock = 0; try @@ -530,10 +530,11 @@ public class PeakFileProgressData } /// - /// Defines the data used with the OnProcessDone event (actually nothing). + /// Defines the data used with the OnProcessDone event. /// public class PeakFileDoneData { + public bool Cancelled { get; set; } } /// diff --git a/MPfm/MPfm.iOS/Classes/Controllers/PlayerViewController.cs b/MPfm/MPfm.iOS/Classes/Controllers/PlayerViewController.cs index a46914a1..d36de203 100644 --- a/MPfm/MPfm.iOS/Classes/Controllers/PlayerViewController.cs +++ b/MPfm/MPfm.iOS/Classes/Controllers/PlayerViewController.cs @@ -176,9 +176,9 @@ public void RefreshSongInformation(AudioFile audioFile) if(audioFile != null) { + Console.WriteLine("PlayerViewCtrl - RefreshSongInformation - " + audioFile.FilePath); try { - // Check if the album art needs to be refreshed string key = audioFile.ArtistName.ToUpper() + "_" + audioFile.AlbumTitle.ToUpper(); if(_currentAlbumArtKey != key) diff --git a/MPfm/MPfm.iOS/Classes/Controls/MPfmWaveFormView.cs b/MPfm/MPfm.iOS/Classes/Controls/MPfmWaveFormView.cs index 9790bd1e..79602e9b 100644 --- a/MPfm/MPfm.iOS/Classes/Controls/MPfmWaveFormView.cs +++ b/MPfm/MPfm.iOS/Classes/Controls/MPfmWaveFormView.cs @@ -28,6 +28,7 @@ using MonoTouch.Foundation; using MonoTouch.UIKit; using System.IO; +using System.Threading.Tasks; namespace MPfm.iOS.Classes.Controls { @@ -36,7 +37,11 @@ public class MPfmWaveFormView : UIView { private PeakFileGenerator _peakFileGenerator; private string _status = "Initial status"; - private bool _isLoading = true; + private bool _isLoading = false; + private UIImage _imageCache = null; + private List WaveDataHistory { get; set; } + + public WaveFormDisplayType DisplayType { get; set; } public MPfmWaveFormView(IntPtr handle) : base (handle) @@ -52,7 +57,10 @@ public MPfmWaveFormView(RectangleF frame) private void Initialize() { - this.BackgroundColor = UIColor.DarkGray; + this.BackgroundColor = UIColor.Black; + WaveDataHistory = new List(); + DisplayType = WaveFormDisplayType.Stereo; + _peakFileGenerator = new PeakFileGenerator(); _peakFileGenerator.OnProcessStarted += HandleOnPeakFileProcessStarted; _peakFileGenerator.OnProcessData += HandleOnPeakFileProcessData; @@ -62,8 +70,14 @@ private void Initialize() void HandleOnPeakFileProcessStarted(PeakFileStartedData data) { InvokeOnMainThread(() => { + WaveDataHistory = new List((int)data.TotalBlocks); + if(_imageCache != null) + { + _imageCache.Dispose(); + _imageCache = null; + } _isLoading = true; - _status = "Peak file started"; + _status = "Loading (0% done)"; SetNeedsDisplay(); }); } @@ -71,6 +85,20 @@ void HandleOnPeakFileProcessStarted(PeakFileStartedData data) void HandleOnPeakFileProcessData(PeakFileProgressData data) { InvokeOnMainThread(() => { + + // Add wave data to history. Use a while a loop to modify the collection while looping. + List minMaxs = data.MinMax; + while (true) + { + if (minMaxs.Count == 0) + { + break; + } + + WaveDataHistory.Add(minMaxs[0]); + minMaxs.RemoveAt(0); + } + _status = "Loading (" + data.PercentageDone.ToString("0") + "% done)"; SetNeedsDisplay(); }); @@ -79,6 +107,16 @@ void HandleOnPeakFileProcessData(PeakFileProgressData data) void HandleOnPeakFileProcessDone(PeakFileDoneData data) { InvokeOnMainThread(() => { + if(data.Cancelled) + { + if(_imageCache != null) + { + _imageCache.Dispose(); + _imageCache = null; + } + WaveDataHistory = new List(); + } + _status = string.Empty; _isLoading = false; SetNeedsDisplay(); @@ -88,9 +126,12 @@ void HandleOnPeakFileProcessDone(PeakFileDoneData data) public void LoadPeakFile(AudioFile audioFile) { // Check if another peak file is already loading + Console.WriteLine("WaveFormView - LoadPeakFile audioFile: " + audioFile.FilePath); if (_peakFileGenerator.IsLoading) _peakFileGenerator.Cancel(); + // + // Check if the peak file subfolder exists string peakFileFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "PeakFiles"); if (!Directory.Exists(peakFileFolder)) @@ -102,17 +143,70 @@ public void LoadPeakFile(AudioFile audioFile) } catch(Exception ex) { - Console.WriteLine("PeakFile - Failed to create folder!"); + Console.WriteLine("PeakFile - Failed to create folder: " + ex.Message); return; } } // Generate peak file path string peakFilePath = Path.Combine(peakFileFolder, Normalizer.NormalizeStringForUrl(audioFile.ArtistName + "_" + audioFile.AlbumTitle + "_" + audioFile.Title + "_" + audioFile.FileType.ToString()) + ".peak"); - - // Start generating peak file in background - Console.WriteLine("PeakFile - Generating " + peakFilePath + "..."); - _peakFileGenerator.GeneratePeakFile(audioFile.FilePath, peakFilePath); + + // Check if peak file exists + bool peakFileLoadedSuccessfully = false; + if (File.Exists(peakFilePath)) + { + Task>.Factory.StartNew(() => { + List data = null; + try + { + data = _peakFileGenerator.ReadPeakFile(peakFilePath); + if(data != null) + return data; + } + catch (Exception ex) + { + Console.WriteLine("Error reading peak file: " + ex.Message); + } + + try + { + Console.WriteLine("Peak file could not be loaded - Generating " + peakFilePath + "..."); + _peakFileGenerator.GeneratePeakFile(audioFile.FilePath, peakFilePath); + } + catch (Exception ex) + { + Console.WriteLine("Error generating peak file: " + ex.Message); + } + return null; + }, TaskCreationOptions.LongRunning).ContinueWith(t => { + List data = (List)t.Result; + + if (data == null) + { + // The peak file has been generated on another thread + return; + } + else + { + InvokeOnMainThread(() => { + // Refresh image cache + WaveDataHistory = data; + if(_imageCache != null) + { + _imageCache.Dispose(); + _imageCache = null; + } + SetNeedsDisplay(); + }); + } + }, TaskScheduler.FromCurrentSynchronizationContext()); + } + else + { + // Start generating peak file in background + Console.WriteLine("Peak file doesn't exist - Generating " + peakFilePath + "..."); + _peakFileGenerator.GeneratePeakFile(audioFile.FilePath, peakFilePath); + } } public void CancelPeakFileGeneration() @@ -134,33 +228,269 @@ public override void Draw(RectangleF rect) return; } - // Load bitmap cache (iOS 4.0+ / OSX 10.6+ does not require to have a byte array buffer) -// int bitsPerComponent = 8; -// int bytesPerPixel = 4; -// int bytesPerRow = (Bounds.Width * bitsPerComponent * bytesPerPixel + 7) / 8; -// int dataSize = bytesPerRow * Bounds.Height; -// CGColorSpace colorSpace = CGColorSpace.CreateDeviceRGB(); -// context = new CGBitmapContext(null, Bounds.Width, Bounds.Height, bitsPerComponent, bytesPerRow, colorSpace, CGImageAlphaInfo.PremultipliedLast | CGBitmapFlags.ByteOrder32Big); - UIGraphics.BeginImageContext(Bounds.Size); - context = UIGraphics.GetCurrentContext(); - if (context == null) + // If there is no wave data and nothing is loading, just display nothing + if (WaveDataHistory.Count == 0) { - // Error - Console.WriteLine("Error initializing bitmap cache!"); return; } - context.SetStrokeColor(new CGColor(1, 1, 1, 1)); - context.SetLineWidth(10); - context.StrokeLineSegments(new PointF[2] { new PointF(10, 0), new PointF(10, this.Bounds.Height) }); + // Check if bitmap cache should be reloaded + if (_imageCache == null) + { + //UIGraphics.BeginImageContext(Bounds.Size); + UIGraphics.BeginImageContextWithOptions(Bounds.Size, false, 0); + context = UIGraphics.GetCurrentContext(); + if (context == null) + { + // Error + Console.WriteLine("Error initializing bitmap cache!"); + return; + } + + // Declare variables + int widthAvailable = (int)Bounds.Width; + int heightAvailable = (int)Bounds.Height; + float x1 = 0; + float x2 = 0; + float leftMin = 0; + float leftMax = 0; + float rightMin = 0; + float rightMax = 0; + float mixMin = 0; + float mixMax = 0; + float leftMaxHeight = 0; + float leftMinHeight = 0; + float rightMaxHeight = 0; + float rightMinHeight = 0; + float mixMaxHeight = 0; + float mixMinHeight = 0; + int historyIndex = 0; + int historyCount = 0; + float lineWidth = 0; + float lineWidthPerHistoryItem = 0; + int nHistoryItemsPerLine = 0; + float desiredLineWidth = 1.0f; + WaveDataMinMax[] subset = null; + + historyCount = WaveDataHistory.Count; + + // Find out how many samples are represented by each line of the wave form, depending on its width. + // For example, if the history has 45000 items, and the control has a width of 1000px, 45 items will need to be averaged by line. + lineWidthPerHistoryItem = (float)widthAvailable / (float)historyCount; + //float historyItemsPerLine = (float)WaveDataHistory.Count / (float)Bounds.Width; + + // Check if the line width is below the desired line width + if (lineWidthPerHistoryItem < desiredLineWidth) + { + // Try to get a line width around 0.5f so the precision is good enough and no artifacts will be shown. + while (lineWidth < desiredLineWidth) + { + // Increment the number of history items per line + Console.WriteLine("Determining line width (lineWidth: " + lineWidth.ToString() + " desiredLineWidth: " + desiredLineWidth.ToString() + " nHistoryItemsPerLine: " + nHistoryItemsPerLine.ToString() + " lineWidthPerHistoryItem: " + lineWidthPerHistoryItem.ToString()); + nHistoryItemsPerLine++; + lineWidth += lineWidthPerHistoryItem; + } + nHistoryItemsPerLine--; + lineWidth -= lineWidthPerHistoryItem; + } + else + { + // The lines are larger than 0.5 pixels. + lineWidth = lineWidthPerHistoryItem; + nHistoryItemsPerLine = 1; + } + + Console.WriteLine("WaveFormView - historyItemsPerLine: " + nHistoryItemsPerLine.ToString()); + + context.SetStrokeColor(new CGColor(1, 1, 0.3f, 1)); + context.SetLineWidth(lineWidth); + //context.SetLineWidth(0.5f); - UIImage image = UIGraphics.GetImageFromCurrentImageContext(); - UIGraphics.EndImageContext(); + for (float i = 0; i < widthAvailable; i += lineWidth) + { + // Determine the maximum height of a line (+/-) + //Console.WriteLine("WaveForm - Rendering " + i.ToString() + " on " + widthAvailable.ToString()); + float heightToRenderLine = 0; + if (DisplayType == WaveFormDisplayType.Stereo) + { + heightToRenderLine = (float)heightAvailable / 4; + } + else + { + heightToRenderLine = (float)heightAvailable / 2; + } + + // Determine x position + x1 = i; + x2 = i + lineWidth; + + try + { + // Check if there are multiple history items per line + if (nHistoryItemsPerLine > 1) + { + if (historyIndex + nHistoryItemsPerLine > historyCount) + { + // Create subset with remaining data + subset = new WaveDataMinMax[historyCount - historyIndex]; + WaveDataHistory.CopyTo(historyIndex, subset, 0, historyCount - historyIndex); + } + else + { + subset = new WaveDataMinMax[nHistoryItemsPerLine]; + WaveDataHistory.CopyTo(historyIndex, subset, 0, nHistoryItemsPerLine); + } + + leftMin = AudioTools.GetMinPeakFromWaveDataMaxHistory(subset.ToList(), nHistoryItemsPerLine, ChannelType.Left); + leftMax = AudioTools.GetMaxPeakFromWaveDataMaxHistory(subset.ToList(), nHistoryItemsPerLine, ChannelType.Left); + rightMin = AudioTools.GetMinPeakFromWaveDataMaxHistory(subset.ToList(), nHistoryItemsPerLine, ChannelType.Right); + rightMax = AudioTools.GetMaxPeakFromWaveDataMaxHistory(subset.ToList(), nHistoryItemsPerLine, ChannelType.Right); + mixMin = AudioTools.GetMinPeakFromWaveDataMaxHistory(subset.ToList(), nHistoryItemsPerLine, ChannelType.Mix); + mixMax = AudioTools.GetMaxPeakFromWaveDataMaxHistory(subset.ToList(), nHistoryItemsPerLine, ChannelType.Mix); + } + else + { + leftMin = WaveDataHistory[historyIndex].leftMin; + leftMax = WaveDataHistory[historyIndex].leftMax; + rightMin = WaveDataHistory[historyIndex].rightMin; + rightMax = WaveDataHistory[historyIndex].rightMax; + mixMin = WaveDataHistory[historyIndex].mixMin; + mixMax = WaveDataHistory[historyIndex].mixMax; + } + + // Increment history count + //historyCount += historyItemsPerLine; + + leftMaxHeight = leftMax * heightToRenderLine; + leftMinHeight = leftMin * heightToRenderLine; + rightMaxHeight = rightMax * heightToRenderLine; + rightMinHeight = rightMin * heightToRenderLine; + mixMaxHeight = mixMax * heightToRenderLine; + mixMinHeight = mixMin * heightToRenderLine; + } + catch + { + throw; + } + + // Determine display type + if (DisplayType == WaveFormDisplayType.LeftChannel || + DisplayType == WaveFormDisplayType.RightChannel || + DisplayType == WaveFormDisplayType.Mix) + { + // Calculate min/max line height + float minLineHeight = 0; + float maxLineHeight = 0; + + // Set mib/max + if (DisplayType == WaveFormDisplayType.LeftChannel) + { + minLineHeight = leftMinHeight; + maxLineHeight = leftMaxHeight; + } + else if (DisplayType == WaveFormDisplayType.RightChannel) + { + minLineHeight = rightMinHeight; + maxLineHeight = rightMaxHeight; + } + else if (DisplayType == WaveFormDisplayType.Mix) + { + minLineHeight = mixMinHeight; + maxLineHeight = mixMaxHeight; + } + + // ------------------------ + // Positive Max Value + + // Draw positive value (y: middle to top) + + context.StrokeLineSegments(new PointF[2] { + new PointF(x1, heightToRenderLine), new PointF(x2, heightToRenderLine - maxLineHeight) + }); + + // ------------------------ + // Negative Max Value + + // Draw negative value (y: middle to height) + context.StrokeLineSegments(new PointF[2] { + new PointF(x1, heightToRenderLine), new PointF(x2, heightToRenderLine + (-minLineHeight)) + }); + } + else if (DisplayType == WaveFormDisplayType.Stereo) + { + // ----------------------------------------- + // LEFT Channel - Positive Max Value + + // Draw positive value (y: middle to top) + context.StrokeLineSegments(new PointF[2] { + new PointF(x1, heightToRenderLine), new PointF(x2, heightToRenderLine - leftMaxHeight) + }); + + // ----------------------------------------- + // LEFT Channel - Negative Max Value + + // Draw negative value (y: middle to height) + context.StrokeLineSegments(new PointF[2] { + new PointF(x1, heightToRenderLine), new PointF(x2, heightToRenderLine + (-leftMinHeight)) + }); + + // ----------------------------------------- + // RIGHT Channel - Positive Max Value + + // Multiply by 3 to get the new center line for right channel + // Draw positive value (y: middle to top) + context.StrokeLineSegments(new PointF[2] { + new PointF(x1, (heightToRenderLine * 3)), new PointF(x2, (heightToRenderLine * 3) - rightMaxHeight) + }); + + // ----------------------------------------- + // RIGHT Channel - Negative Max Value + + // Draw negative value (y: middle to height) + context.StrokeLineSegments(new PointF[2] { + new PointF(x1, (heightToRenderLine * 3)), new PointF(x2, (heightToRenderLine * 3) + (-rightMinHeight)) + }); + } + + // Increment the history index; pad the last values if the count is about to exceed + if (historyIndex < historyCount - 1) + { + // Increment by the number of history items per line + historyIndex += nHistoryItemsPerLine; + } + } + + // Get image from context + _imageCache = UIGraphics.GetImageFromCurrentImageContext(); + UIGraphics.EndImageContext(); + } // Draw bitmap cache context = UIGraphics.GetCurrentContext(); - context.DrawImage(Bounds, image.CGImage); - + context.DrawImage(Bounds, _imageCache.CGImage); } } + + /// + /// Defines the wave form display type. + /// + public enum WaveFormDisplayType + { + /// + /// Left channel. + /// + LeftChannel = 0, + /// + /// Right channel. + /// + RightChannel = 1, + /// + /// Stereo (left and right channels). + /// + Stereo = 2, + /// + /// Mix (mix of left/right channels). + /// + Mix = 3 + } }