From de0deace0d3b934d48ea7bf91d803c887f2f67d6 Mon Sep 17 00:00:00 2001 From: ycastonguay Date: Mon, 8 Apr 2013 00:17:42 -0400 Subject: [PATCH] iOS: Refactored wave form generation and cache into a new class, WaveFormCacheManager. This created a few minor bugs but the peak file are generating fine. The goal is to share the same wave form cache and generating threads between wave form controls. Related to issue #405. --- MPfm/MPfm.MVP/MPfm.MVP.iOS.csproj | 2 +- .../PeakFiles/PeakFileStartedData.cs | 1 + .../Controllers/PlayerViewController.cs | 4 - .../Controls/MPfmNavigationController.cs | 10 +- .../Classes/Controls/MPfmWaveFormView.cs | 566 ++++-------------- .../Events/GenerateWaveFormEventArgs.cs | 25 + .../Events/GeneratePeakFileEventArgs.cs | 32 + .../Events/GenerateWaveFormEventArgs.cs | 36 ++ .../Classes/Managers/WaveFormCacheManager.cs | 477 +++++++++++++++ MPfm/MPfm.iOS/MPfm.iOS.csproj | 4 + 10 files changed, 688 insertions(+), 469 deletions(-) create mode 100644 MPfm/MPfm.iOS/Classes/Helpers/Events/GenerateWaveFormEventArgs.cs create mode 100644 MPfm/MPfm.iOS/Classes/Managers/Events/GeneratePeakFileEventArgs.cs create mode 100644 MPfm/MPfm.iOS/Classes/Managers/Events/GenerateWaveFormEventArgs.cs create mode 100644 MPfm/MPfm.iOS/Classes/Managers/WaveFormCacheManager.cs diff --git a/MPfm/MPfm.MVP/MPfm.MVP.iOS.csproj b/MPfm/MPfm.MVP/MPfm.MVP.iOS.csproj index 1c7a396e..fbc83338 100644 --- a/MPfm/MPfm.MVP/MPfm.MVP.iOS.csproj +++ b/MPfm/MPfm.MVP/MPfm.MVP.iOS.csproj @@ -295,6 +295,6 @@ - + \ No newline at end of file diff --git a/MPfm/MPfm.Sound/PeakFiles/PeakFileStartedData.cs b/MPfm/MPfm.Sound/PeakFiles/PeakFileStartedData.cs index 6bb704e6..ab85ec72 100644 --- a/MPfm/MPfm.Sound/PeakFiles/PeakFileStartedData.cs +++ b/MPfm/MPfm.Sound/PeakFiles/PeakFileStartedData.cs @@ -30,5 +30,6 @@ public class PeakFileStartedData /// Defines the total number of blocks to read. /// public int TotalBlocks { get; set; } + public string AudioFilePath { get; set; } } } \ No newline at end of file diff --git a/MPfm/MPfm.iOS/Classes/Controllers/PlayerViewController.cs b/MPfm/MPfm.iOS/Classes/Controllers/PlayerViewController.cs index f59fe4d3..e8105492 100644 --- a/MPfm/MPfm.iOS/Classes/Controllers/PlayerViewController.cs +++ b/MPfm/MPfm.iOS/Classes/Controllers/PlayerViewController.cs @@ -67,10 +67,6 @@ public override void DidReceiveMemoryWarning() public override void ViewDidLoad() { - // Set fonts -// lblPosition.Font = UIFont.FromName("OstrichSans-Black", 18); -// lblLength.Font = UIFont.FromName("OstrichSans-Black", 18); - // Load button bitmaps btnPrevious.SetImage(UIImage.FromBundle("Images/Buttons/previous"), UIControlState.Normal); btnPrevious.SetImage(UIImage.FromBundle("Images/Buttons/previous_on"), UIControlState.Highlighted); diff --git a/MPfm/MPfm.iOS/Classes/Controls/MPfmNavigationController.cs b/MPfm/MPfm.iOS/Classes/Controls/MPfmNavigationController.cs index 99250a93..153103aa 100644 --- a/MPfm/MPfm.iOS/Classes/Controls/MPfmNavigationController.cs +++ b/MPfm/MPfm.iOS/Classes/Controls/MPfmNavigationController.cs @@ -53,11 +53,11 @@ public MPfmNavigationController(MobileNavigationTabType tabType) : base() // Create messenger hub to listen to player changes _messengerHub = Bootstrapper.GetContainer().Resolve(); _messengerHub.Subscribe((message) => { - Console.WriteLine("NavCtrl (" + TabType.ToString() + ") - PlayerPlaylistIndexChangedMessage"); + //Console.WriteLine("NavCtrl (" + TabType.ToString() + ") - PlayerPlaylistIndexChangedMessage"); UpdateNowPlayingView(); }); _messengerHub.Subscribe((message) => { - Console.WriteLine("NavCtrl (" + TabType.ToString() + ") - PlayerStatusMessage - Status=" + message.Status.ToString()); + //Console.WriteLine("NavCtrl (" + TabType.ToString() + ") - PlayerStatusMessage - Status=" + message.Status.ToString()); if(message.Status == PlayerStatusType.Playing) _isPlayerPlaying = true; else @@ -122,7 +122,7 @@ public MPfmNavigationController(MobileNavigationTabType tabType) : base() public override void ViewWillLayoutSubviews() { - Console.WriteLine("MPfmNavCtrl - ViewWillLayoutSubviews"); + //Console.WriteLine("MPfmNavCtrl - ViewWillLayoutSubviews"); float x = 12; if(this.VisibleViewController.NavigationItem.LeftBarButtonItem != null) @@ -163,10 +163,10 @@ public override void ViewWillLayoutSubviews() private void UpdateNowPlayingView() { - Console.WriteLine("NavCtrl (" + TabType.ToString() + ") - UpdateNowPlayingView: isPlayerPlaying=" + _isPlayerPlaying.ToString() + " isViewPlayer=" + _isViewPlayer.ToString()); + //Console.WriteLine("NavCtrl (" + TabType.ToString() + ") - UpdateNowPlayingView: isPlayerPlaying=" + _isPlayerPlaying.ToString() + " isViewPlayer=" + _isViewPlayer.ToString()); if(_isPlayerPlaying && !_isViewPlayer) { - Console.WriteLine("NavCtrl - Showing Now Playing view..."); + //Console.WriteLine("NavCtrl - Showing Now Playing view..."); UIView.Animate(0.3f, () => { _btnEffects.Frame = new RectangleF(UIScreen.MainScreen.Bounds.Width, 0, 44, 44); _btnEffects.Alpha = 0; diff --git a/MPfm/MPfm.iOS/Classes/Controls/MPfmWaveFormView.cs b/MPfm/MPfm.iOS/Classes/Controls/MPfmWaveFormView.cs index 2d84a1df..f6cd3ab7 100644 --- a/MPfm/MPfm.iOS/Classes/Controls/MPfmWaveFormView.cs +++ b/MPfm/MPfm.iOS/Classes/Controls/MPfmWaveFormView.cs @@ -31,21 +31,23 @@ using MonoTouch.Foundation; using MonoTouch.UIKit; using MPfm.iOS.Helpers; +using MPfm.iOS.Managers; +using MPfm.iOS.Managers.Events; namespace MPfm.iOS.Classes.Controls { [Register("MPfmWaveFormView")] public class MPfmWaveFormView : UIView { - private IPeakFileService _peakFileService; + private WaveFormCacheManager _waveFormCacheManager; private string _status = "Initial status"; private bool _isLoading = false; private bool _isGeneratingImageCache = false; private UIImage _imageCache = null; - private AudioFile _currentAudioFile = null; private float _cursorX; private float _secondaryCursorX; - private List _waveDataHistory; + private CGColor _colorGradient1 = new CGColor(0, 0, 0, 1); + private CGColor _colorGradient2 = new CGColor(0.15f, 0.15f, 0.15f, 1); public WaveFormDisplayType DisplayType { get; set; } @@ -61,7 +63,7 @@ public long Position _position = value; // Don't bother if a peak file is loading - if(_isLoading || _imageCache == null || _waveDataHistory.Count == 0) + if(_isLoading || _imageCache == null) return; // Invalidate cursor @@ -145,67 +147,61 @@ public MPfmWaveFormView(RectangleF frame) private void Initialize() { this.BackgroundColor = UIColor.Black; - _waveDataHistory = new List(); DisplayType = WaveFormDisplayType.Stereo; - - _peakFileService = Bootstrapper.GetContainer().Resolve(); - _peakFileService.OnProcessStarted += HandleOnPeakFileProcessStarted; - _peakFileService.OnProcessData += HandleOnPeakFileProcessData; - _peakFileService.OnProcessDone += HandleOnPeakFileProcessDone; + _waveFormCacheManager = Bootstrapper.GetContainer().Resolve(); + _waveFormCacheManager.GeneratePeakFileBegunEvent += HandleGeneratePeakFileBegunEvent; + _waveFormCacheManager.GeneratePeakFileProgressEvent += HandleGeneratePeakFileProgressEvent; + _waveFormCacheManager.GeneratePeakFileEndedEvent += HandleGeneratePeakFileEndedEvent; + _waveFormCacheManager.GenerateWaveFormBitmapBegunEvent += HandleGenerateWaveFormBegunEvent; + _waveFormCacheManager.GenerateWaveFormBitmapEndedEvent += HandleGenerateWaveFormEndedEvent; } - void HandleOnPeakFileProcessStarted(PeakFileStartedData data) + private void HandleGeneratePeakFileBegunEvent(object sender, GeneratePeakFileEventArgs e) { InvokeOnMainThread(() => { - _waveDataHistory = new List((int)data.TotalBlocks); - if(_imageCache != null) - { - _imageCache.Dispose(); - _imageCache = null; - } - _isLoading = true; - _status = "Generating wave form (0% done)"; - SetNeedsDisplay(); + RefreshStatus("Generating wave form (0% done)"); }); } - void HandleOnPeakFileProcessData(PeakFileProgressData data) + private void HandleGeneratePeakFileProgressEvent(object sender, GeneratePeakFileEventArgs e) { 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 = "Generating wave form (" + data.PercentageDone.ToString("0") + "% done)"; - SetNeedsDisplay(); + RefreshStatus("Generating wave form (" + e.PercentageDone.ToString("0") + "% done)"); + }); + } + + private void HandleGeneratePeakFileEndedEvent(object sender, GeneratePeakFileEventArgs e) + { + InvokeOnMainThread(() => { + _waveFormCacheManager.RequestBitmap(e.AudioFilePath, WaveFormDisplayType.Stereo, Bounds, 1); }); } - void HandleOnPeakFileProcessDone(PeakFileDoneData data) + private void HandleGenerateWaveFormBegunEvent(object sender, GenerateWaveFormEventArgs e) + { + + } + + private void HandleGenerateWaveFormEndedEvent(object sender, GenerateWaveFormEventArgs e) { InvokeOnMainThread(() => { - //if(data.Cancelled) - if(_currentAudioFile != null && data.AudioFilePath != _currentAudioFile.FilePath) - { - if(_imageCache != null) - { - _imageCache.Dispose(); - _imageCache = null; - } - _waveDataHistory = new List(); - } - - _status = string.Empty; _isLoading = false; + _imageCache = e.Image; SetNeedsDisplay(); - }); + }); + } + + void HandleOnPeakFileProcessStarted(PeakFileStartedData data) + { + } + + void HandleOnPeakFileProcessData(PeakFileProgressData data) + { + + } + + void HandleOnPeakFileProcessDone(PeakFileDoneData data) + { } public void FlushCache() @@ -219,104 +215,67 @@ public void FlushCache() public void LoadPeakFile(AudioFile audioFile) { - // Indicate peak file loading - _currentAudioFile = audioFile; + Console.WriteLine("WaveFormView - LoadPeakFile audioFile: " + audioFile.FilePath); + RefreshStatus("Loading peak file..."); + _waveFormCacheManager.LoadPeakFile(audioFile); + } + + private void RefreshStatus(string status) + { _isLoading = true; - _status = "Loading peak file..."; + _status = status; SetNeedsDisplay(); + } - // Check if another peak file is already loading - Console.WriteLine("WaveFormView - LoadPeakFile audioFile: " + audioFile.FilePath); - if (_peakFileService.IsLoading) - _peakFileService.Cancel(); - - // Check if the peak file subfolder exists - string peakFileFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "PeakFiles"); - if (!Directory.Exists(peakFileFolder)) - { - try - { - Console.WriteLine("PeakFile - Creating folder " + peakFileFolder + "..."); - DirectoryInfo directoryInfo = Directory.CreateDirectory(peakFileFolder); - } - catch(Exception ex) - { - Console.WriteLine("PeakFile - Failed to create folder: " + ex.Message); - return; - } - } + private void DrawStatus(string status) + { + var context = UIGraphics.GetCurrentContext(); + + // Draw gradient background + CoreGraphicsHelper.FillGradient(context, Bounds, _colorGradient2, _colorGradient1); + + // Draw status string + NSString str = new NSString(status); + context.SetFillColor(UIColor.White.CGColor); + float y = (Bounds.Height - 30) / 2; + str.DrawString(new RectangleF(0, y, Bounds.Width, 30), UIFont.FromName("HelveticaNeue", 12), UILineBreakMode.TailTruncation, UITextAlignment.Center); + return; + } - // Generate peak file path - string peakFilePath = Path.Combine(peakFileFolder, Normalizer.NormalizeStringForUrl(audioFile.ArtistName + "_" + audioFile.AlbumTitle + "_" + audioFile.Title + "_" + audioFile.FileType.ToString()) + ".peak"); + private void DrawWaveFormBitmap() + { + _isLoading = false; + int heightAvailable = (int)Frame.Height; - // Check if peak file exists - if (File.Exists(peakFilePath)) + // Draw bitmap cache + var context = UIGraphics.GetCurrentContext(); + context.DrawImage(Bounds, _imageCache.CGImage); + + // Calculate position + float positionPercentage = (float)Position / (float)Length; + _cursorX = (positionPercentage * Bounds.Width) - ScrollX; + + // Draw cursor line + context.SetStrokeColor(new CGColor(0, 0.5f, 1, 1)); + context.SetLineWidth(1.0f); + context.StrokeLineSegments(new PointF[2] { new PointF(_cursorX, 0), new PointF(_cursorX, heightAvailable) }); + + // Check if a secondary cursor must be drawn + if(_secondaryPosition > 0) { - Task>.Factory.StartNew(() => { - List data = null; - try - { - data = _peakFileService.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 + "..."); - _peakFileService.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; - } - _isLoading = false; - _status = string.Empty; - SetNeedsDisplay(); - }); - } - }, TaskScheduler.FromCurrentSynchronizationContext()); - } - else - { - // Start generating peak file in background - Console.WriteLine("Peak file doesn't exist - Generating " + peakFilePath + "..."); - _peakFileService.GeneratePeakFile(audioFile.FilePath, peakFilePath); + // Calculate position + float secondaryPositionPercentage = (float)SecondaryPosition / (float)Length; + _secondaryCursorX = (secondaryPositionPercentage * Bounds.Width) - ScrollX; + + // Draw cursor line + context.SetStrokeColor(new CGColor(1, 1, 1, 1)); + context.SetLineWidth(1.0f); + context.StrokeLineSegments(new PointF[2] { new PointF(_secondaryCursorX, 0), new PointF(_secondaryCursorX, heightAvailable) }); } } - public void CancelPeakFileGeneration() - { - _peakFileService.Cancel(); - } - - public override void Draw(RectangleF rect) + private void GenerateWaveFormBitmap() { - base.Draw(rect); - // Calculate available size int widthAvailable = (int)Frame.Width; int heightAvailable = (int)Frame.Height; @@ -325,334 +284,23 @@ public override void Draw(RectangleF rect) widthAvailable = (int)(Frame.Width * Zoom); } - CGContext context; - CGColor colorGradient1 = new CGColor(0, 0, 0, 1); - CGColor colorGradient2 = new CGColor(0.15f, 0.15f, 0.15f, 1); - - // Check if a wave form needs to be displayed - string emptyWaveFormMessage = string.Empty; - if (_isLoading) - emptyWaveFormMessage = _status; - else if(_waveDataHistory.Count == 0) - emptyWaveFormMessage = string.Empty; - - //if(!string.IsNullOrEmpty(emptyWaveFormMessage)) - if(_isLoading || _waveDataHistory.Count == 0) + var context = UIGraphics.GetCurrentContext(); + if(!_isGeneratingImageCache) { - context = UIGraphics.GetCurrentContext(); - - // Draw gradient background - CoreGraphicsHelper.FillGradient(context, Bounds, colorGradient2, colorGradient1); - - // Draw status string - NSString str = new NSString(emptyWaveFormMessage); - context.SetFillColor(UIColor.White.CGColor); - float y = (Bounds.Height - 30) / 2; - str.DrawString(new RectangleF(0, y, Bounds.Width, 30), UIFont.FromName("HelveticaNeue", 12), UILineBreakMode.TailTruncation, UITextAlignment.Center); - return; + RefreshStatus("Generating wave form image cache..."); + _isGeneratingImageCache = true; } - // Check if bitmap cache should be reloaded - if (_imageCache != null) - { - // Draw bitmap cache - context = UIGraphics.GetCurrentContext(); - context.DrawImage(Bounds, _imageCache.CGImage); + } - // Calculate position - float positionPercentage = (float)Position / (float)Length; - _cursorX = (positionPercentage * Bounds.Width) - ScrollX; + public override void Draw(RectangleF rect) + { + base.Draw(rect); - // Draw cursor line - context.SetStrokeColor(new CGColor(0, 0.5f, 1, 1)); - context.SetLineWidth(1.0f); - context.StrokeLineSegments(new PointF[2] { new PointF(_cursorX, 0), new PointF(_cursorX, heightAvailable) }); - - // Check if a secondary cursor must be drawn - if(_secondaryPosition > 0) - { - // Calculate position - float secondaryPositionPercentage = (float)SecondaryPosition / (float)Length; - _secondaryCursorX = (secondaryPositionPercentage * Bounds.Width) - ScrollX; - - // Draw cursor line - context.SetStrokeColor(new CGColor(1, 1, 1, 1)); - context.SetLineWidth(1.0f); - context.StrokeLineSegments(new PointF[2] { new PointF(_secondaryCursorX, 0), new PointF(_secondaryCursorX, heightAvailable) }); - } - } - else if (_imageCache == null && _waveDataHistory.Count > 0) - { - // Let the user know the image cache is generating - context = UIGraphics.GetCurrentContext(); - - // Generate image in another thread - if(!_isGeneratingImageCache) - { - // Draw gradient background - CoreGraphicsHelper.FillGradient(context, Bounds, colorGradient2, colorGradient1); - - // Draw status string - NSString str = new NSString("Generating wave form image cache..."); - context.SetFillColor(UIColor.White.CGColor); - float y = (Bounds.Height - 30) / 2; - str.DrawString(new RectangleF(0, y, Bounds.Width, 30), UIFont.FromName("HelveticaNeue", 12), UILineBreakMode.TailTruncation, UITextAlignment.Center); - - _isGeneratingImageCache = true; - Task.Factory.StartNew(() => { - - try - { - Console.WriteLine("WaveFormView - Creating image cache..."); - UIGraphics.BeginImageContextWithOptions(Bounds.Size, false, 0); - context = UIGraphics.GetCurrentContext(); - if (context == null) - { - // Error - Console.WriteLine("Error initializing image cache!"); - return null; - } - - // Draw gradient background - CoreGraphicsHelper.FillGradient(context, Bounds, colorGradient1, colorGradient2); - - // Declare variables - 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 = 0.5f; - 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.5f, 1)); - context.SetLineWidth(0.5f); - //context.SetLineWidth(lineWidth); - - for (float i = 0; i < widthAvailable; i += lineWidth) - { - // Round to 0.5 - //i = (float)Math.Round(i * 2) / 2; - float iRound = (float)Math.Round(i * 2) / 2; - - // Determine the maximum height of a line (+/-) - //Console.WriteLine("WaveForm - Rendering " + i.ToString() + " (rnd=" + iRound.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; - - 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(Exception ex) - { - 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(); - return _imageCache; - } - catch(Exception ex) - { - Console.WriteLine("Error while creating image cache: " + ex.Message); - } - return null; - }, TaskCreationOptions.LongRunning).ContinueWith(t => { - _isGeneratingImageCache = false; - if(t.Result != null) - { - // Force refresh control now that the image cache is generated - InvokeOnMainThread(() => { - SetNeedsDisplay(); - }); - } - }, TaskScheduler.FromCurrentSynchronizationContext()); - } - } + if(_isLoading) + DrawStatus(_status); + else if (_imageCache != null) + DrawWaveFormBitmap(); } } diff --git a/MPfm/MPfm.iOS/Classes/Helpers/Events/GenerateWaveFormEventArgs.cs b/MPfm/MPfm.iOS/Classes/Helpers/Events/GenerateWaveFormEventArgs.cs new file mode 100644 index 00000000..ac4b2498 --- /dev/null +++ b/MPfm/MPfm.iOS/Classes/Helpers/Events/GenerateWaveFormEventArgs.cs @@ -0,0 +1,25 @@ +// Copyright © 2011-2013 Yanick Castonguay +// +// This file is part of MPfm. +// +// MPfm is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// MPfm is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with MPfm. If not, see . + +using System; + +namespace MPfm.iOS.Helpers.Events +{ + public class GenerateWaveFormEventArgs : EventArgs + { + } +} diff --git a/MPfm/MPfm.iOS/Classes/Managers/Events/GeneratePeakFileEventArgs.cs b/MPfm/MPfm.iOS/Classes/Managers/Events/GeneratePeakFileEventArgs.cs new file mode 100644 index 00000000..7e3e8ed2 --- /dev/null +++ b/MPfm/MPfm.iOS/Classes/Managers/Events/GeneratePeakFileEventArgs.cs @@ -0,0 +1,32 @@ +// Copyright © 2011-2013 Yanick Castonguay +// +// This file is part of MPfm. +// +// MPfm is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// MPfm is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with MPfm. If not, see . + +using System; + +namespace MPfm.iOS.Managers.Events +{ + public class GeneratePeakFileEventArgs : EventArgs + { + public string AudioFilePath { get; set; } + public float PercentageDone { get; set; } + + public GeneratePeakFileEventArgs() + : base() + { + } + } +} diff --git a/MPfm/MPfm.iOS/Classes/Managers/Events/GenerateWaveFormEventArgs.cs b/MPfm/MPfm.iOS/Classes/Managers/Events/GenerateWaveFormEventArgs.cs new file mode 100644 index 00000000..6bfe0d32 --- /dev/null +++ b/MPfm/MPfm.iOS/Classes/Managers/Events/GenerateWaveFormEventArgs.cs @@ -0,0 +1,36 @@ +// Copyright © 2011-2013 Yanick Castonguay +// +// This file is part of MPfm. +// +// MPfm is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// MPfm is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with MPfm. If not, see . + +using System; +using MonoTouch.UIKit; +using MPfm.iOS.Classes.Controls; + +namespace MPfm.iOS.Managers.Events +{ + public class GenerateWaveFormEventArgs : EventArgs + { + public string AudioFilePath { get; set; } + public float Zoom { get; set; } + public WaveFormDisplayType DisplayType { get; set; } + public UIImage Image { get; set; } + + public GenerateWaveFormEventArgs() + : base() + { + } + } +} diff --git a/MPfm/MPfm.iOS/Classes/Managers/WaveFormCacheManager.cs b/MPfm/MPfm.iOS/Classes/Managers/WaveFormCacheManager.cs new file mode 100644 index 00000000..dd7a585a --- /dev/null +++ b/MPfm/MPfm.iOS/Classes/Managers/WaveFormCacheManager.cs @@ -0,0 +1,477 @@ +// Copyright © 2011-2013 Yanick Castonguay +// +// This file is part of MPfm. +// +// MPfm is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// MPfm is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with MPfm. If not, see . + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using MPfm.Core; +using MPfm.Sound.AudioFiles; +using MPfm.Sound.PeakFiles; +using MonoTouch.CoreGraphics; +using MonoTouch.Foundation; +using MonoTouch.UIKit; +using MPfm.iOS.Classes.Controls; +using MPfm.iOS.Managers.Events; +using MPfm.iOS.Helpers; + +namespace MPfm.iOS.Managers +{ + /// + /// Manager for caching wave form bitmaps. + /// + public class WaveFormCacheManager + { + private IPeakFileService _peakFileService; + private Dictionary> _waveDataCache = new Dictionary>(); + private Dictionary, UIImage> _bitmapCache = new Dictionary, UIImage>(); + private CGColor _colorGradient1 = new CGColor(0, 0, 0, 1); + private CGColor _colorGradient2 = new CGColor(0.15f, 0.15f, 0.15f, 1); + private float _padding = 0; + + public delegate void GeneratePeakFileEventHandler(object sender, GeneratePeakFileEventArgs e); + public delegate void GenerateWaveFormEventHandler(object sender, GenerateWaveFormEventArgs e); + + public event GeneratePeakFileEventHandler GeneratePeakFileBegunEvent; + public event GeneratePeakFileEventHandler GeneratePeakFileProgressEvent; + public event GeneratePeakFileEventHandler GeneratePeakFileEndedEvent; + public event GenerateWaveFormEventHandler GenerateWaveFormBitmapBegunEvent; + public event GenerateWaveFormEventHandler GenerateWaveFormBitmapEndedEvent; + + public WaveFormCacheManager(IPeakFileService peakFileService) + { + _peakFileService = peakFileService; + _peakFileService.OnProcessStarted += HandleOnPeakFileProcessStarted; + _peakFileService.OnProcessData += HandleOnPeakFileProcessData; + _peakFileService.OnProcessDone += HandleOnPeakFileProcessDone; + } + + protected virtual void OnGeneratePeakFileBegun(GeneratePeakFileEventArgs e) + { + if(GeneratePeakFileBegunEvent != null) + GeneratePeakFileBegunEvent(this, e); + } + + protected virtual void OnGeneratePeakFileProgress(GeneratePeakFileEventArgs e) + { + if(GeneratePeakFileProgressEvent != null) + GeneratePeakFileProgressEvent(this, e); + } + + protected virtual void OnGeneratePeakFileEnded(GeneratePeakFileEventArgs e) + { + if(GeneratePeakFileEndedEvent != null) + GeneratePeakFileEndedEvent(this, e); + } + + protected virtual void OnGenerateWaveFormBitmapBegun(GenerateWaveFormEventArgs e) + { + if(GenerateWaveFormBitmapBegunEvent != null) + GenerateWaveFormBitmapBegunEvent(this, e); + } + + protected virtual void OnGenerateWaveFormBitmapEnded(GenerateWaveFormEventArgs e) + { + if(GenerateWaveFormBitmapEndedEvent != null) + GenerateWaveFormBitmapEndedEvent(this, e); + } + + void HandleOnPeakFileProcessStarted(PeakFileStartedData data) + { + OnGeneratePeakFileBegun(new GeneratePeakFileEventArgs()); + } + + void HandleOnPeakFileProcessData(PeakFileProgressData data) + { + OnGeneratePeakFileProgress(new GeneratePeakFileEventArgs(){ + AudioFilePath = data.AudioFilePath, + PercentageDone = data.PercentageDone + }); + } + + void HandleOnPeakFileProcessDone(PeakFileDoneData data) + { + OnGeneratePeakFileEnded(new GeneratePeakFileEventArgs(){ + AudioFilePath = data.AudioFilePath, + PercentageDone = 100 + }); + } + + // Flow: + // LoadPeakFile(filePath) + // Control displays "Loading peak file" + // Peak file doesn't exist + // Sending GeneratingPeakFile event + // Generating peak file + // Peak file exists + // Loads peak file + // Peak file is loaded. + + // Flow: + // Control requests bitmap at 100% zoom + // Generating wave form. + // Is wave form in cache? + // Return bitmap + // No, generating bitmap + // Tell control the bitmap has been loaded + + // Flow: + // User changes the zoom level on the control + // Same as before but specify a different zoom. + // Thus the bitmap always returns to the control by an event. + + public void LoadPeakFile(AudioFile audioFile) + { + // Indicate peak file loading + bool isLoading = true; + string status = "Loading peak file..."; + //SetNeedsDisplay(); + + // Check if another peak file is already loading + Console.WriteLine("WaveFormCacheManager - LoadPeakFile audioFile: " + audioFile.FilePath); + if (_peakFileService.IsLoading) + _peakFileService.Cancel(); + + // Check if the peak file subfolder exists + string peakFileFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "PeakFiles"); + if (!Directory.Exists(peakFileFolder)) + { + try + { + Console.WriteLine("WaveFormCacheManager - Creating folder " + peakFileFolder + "..."); + DirectoryInfo directoryInfo = Directory.CreateDirectory(peakFileFolder); + } + catch(Exception ex) + { + Console.WriteLine("WaveFormCacheManager - 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"); + + // Check if peak file exists + if (File.Exists(peakFilePath)) + { + Task>.Factory.StartNew(() => { + List data = null; + try + { + Console.WriteLine("WaveFormCacheManager - Reading peak file: " + peakFilePath); + data = _peakFileService.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 + "..."); + OnGeneratePeakFileBegun(new GeneratePeakFileEventArgs()); + _peakFileService.GeneratePeakFile(audioFile.FilePath, peakFilePath); + } + catch (Exception ex) + { + Console.WriteLine("Error generating peak file: " + ex.Message); + } + return null; + }, TaskCreationOptions.LongRunning).ContinueWith(t => { + Console.WriteLine("WaveFormCacheManager - Read peak file over."); + List data = (List)t.Result; + if (data == null) + { + Console.WriteLine("WaveFormCacheManager - Could not load peak file succesfully."); + return; + } + + Console.WriteLine("WaveFormCacheManager - Adding wave data to cache..."); + if(!_waveDataCache.ContainsKey(audioFile.FilePath)) + _waveDataCache.Add(audioFile.FilePath, data); + + OnGeneratePeakFileEnded(new GeneratePeakFileEventArgs(){ + AudioFilePath = audioFile.FilePath, + PercentageDone = 100 + }); + + }, TaskScheduler.FromCurrentSynchronizationContext()); + } + else + { + // Start generating peak file in background + Console.WriteLine("Peak file doesn't exist - Generating " + peakFilePath + "..."); + _peakFileService.GeneratePeakFile(audioFile.FilePath, peakFilePath); + } + } + + public void RequestBitmap(string audioFilePath, WaveFormDisplayType displayType, RectangleF bounds, float zoom) + { + UIImage imageCache; + RectangleF boundsWaveForm; + + // key = FilePath + DisplayType + Zoom + + // Calculate available size + int widthAvailable = (int)bounds.Width; + int heightAvailable = (int)bounds.Height; + if(zoom > 1) + { + widthAvailable = (int)(bounds.Width * zoom); + } + boundsWaveForm = new RectangleF(0, 0, widthAvailable - (_padding * 2), heightAvailable - (_padding * 2)); + + Task.Factory.StartNew(() => { + try + { + Console.WriteLine("WaveFormCacheManager - Creating image cache..."); + UIGraphics.BeginImageContextWithOptions(bounds.Size, false, 0); + var context = UIGraphics.GetCurrentContext(); + if (context == null) + { + // Error + Console.WriteLine("Error initializing image cache!"); + return null; + } + + // Draw gradient background + CoreGraphicsHelper.FillGradient(context, bounds, _colorGradient1, _colorGradient2); + + // Declare variables + 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 = 0.5f; + WaveDataMinMax[] subset = null; + + historyCount = _waveDataCache[audioFilePath].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 = boundsWaveForm.Width / (float)historyCount; + + // 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.5f, 1)); + context.SetLineWidth(0.5f); + //context.SetLineWidth(lineWidth); + + for (float i = 0; i < boundsWaveForm.Width; i += lineWidth) + { + // Round to 0.5 + //i = (float)Math.Round(i * 2) / 2; + float iRound = (float)Math.Round(i * 2) / 2; + + // Determine the maximum height of a line (+/-) + //Console.WriteLine("WaveForm - Rendering " + i.ToString() + " (rnd=" + iRound.ToString() + ") on " + widthAvailable.ToString()); + float heightToRenderLine = 0; + if (displayType == WaveFormDisplayType.Stereo) + heightToRenderLine = boundsWaveForm.Height / 4; + else + heightToRenderLine = boundsWaveForm.Height / 2; + + // Determine x position + x1 = i; + x2 = i; + + if (nHistoryItemsPerLine > 1) + { + if (historyIndex + nHistoryItemsPerLine > historyCount) + { + // Create subset with remaining data + subset = new WaveDataMinMax[historyCount - historyIndex]; + _waveDataCache[audioFilePath].CopyTo(historyIndex, subset, 0, historyCount - historyIndex); + } + else + { + subset = new WaveDataMinMax[nHistoryItemsPerLine]; + _waveDataCache[audioFilePath].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 = _waveDataCache[audioFilePath][historyIndex].leftMin; + leftMax = _waveDataCache[audioFilePath][historyIndex].leftMax; + rightMin = _waveDataCache[audioFilePath][historyIndex].rightMin; + rightMax = _waveDataCache[audioFilePath][historyIndex].rightMax; + mixMin = _waveDataCache[audioFilePath][historyIndex].mixMin; + mixMax = _waveDataCache[audioFilePath][historyIndex].mixMax; + } + + leftMaxHeight = leftMax * heightToRenderLine; + leftMinHeight = leftMin * heightToRenderLine; + rightMaxHeight = rightMax * heightToRenderLine; + rightMinHeight = rightMin * heightToRenderLine; + mixMaxHeight = mixMax * heightToRenderLine; + mixMinHeight = mixMin * heightToRenderLine; + + // 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) + historyIndex += nHistoryItemsPerLine; + } + + // Get image from context + imageCache = UIGraphics.GetImageFromCurrentImageContext(); + UIGraphics.EndImageContext(); + return imageCache; + } + catch(Exception ex) + { + Console.WriteLine("Error while creating image cache: " + ex.Message); + } + return null; + }, TaskCreationOptions.LongRunning).ContinueWith(t => { + Console.WriteLine("WaveFormCacheManager - Created image successfully."); + OnGenerateWaveFormBitmapEnded(new GenerateWaveFormEventArgs(){ + AudioFilePath = audioFilePath, + Zoom = zoom, + DisplayType = displayType, + Image = t.Result + }); + }, TaskScheduler.FromCurrentSynchronizationContext()); + + } + } +} diff --git a/MPfm/MPfm.iOS/MPfm.iOS.csproj b/MPfm/MPfm.iOS/MPfm.iOS.csproj index d2fc6fc1..331429a9 100644 --- a/MPfm/MPfm.iOS/MPfm.iOS.csproj +++ b/MPfm/MPfm.iOS/MPfm.iOS.csproj @@ -185,6 +185,9 @@ MarkerDetailsViewController.cs + + + @@ -232,6 +235,7 @@ +