Fix FS#12238 - WPS delay on pause introduced by r30097 which was the excuse I wanted anyway to do a better PCM fade on stop/pause implementation. New fade is asynchronous tick-based. Restores skin update points in the WPS that were removed when fading mechanism was changed.

git-svn-id: svn://svn.rockbox.org/rockbox/trunk@30340 a1c6a512-1295-4272-9138-f99709370657
diff --git a/apps/gui/wps.c b/apps/gui/wps.c
index 80522d0..48d9e09 100644
--- a/apps/gui/wps.c
+++ b/apps/gui/wps.c
@@ -119,14 +119,28 @@
     return skin_buf[screen];
 }
 
+static void update_non_static(void)
+{
+    int i;
+    FOR_NB_SCREENS(i)
+        skin_update(WPS, i, SKIN_REFRESH_NON_STATIC);
+}
+
 void pause_action(bool may_fade, bool updatewps)
 {
-#if CONFIG_CODEC != SWCODEC
+#if CONFIG_CODEC == SWCODEC
+    /* Do audio first, then update, unless skin were to use its local
+       status in which case, reverse it */
+    audio_pause();
+
+    if (updatewps)
+        update_non_static();
+#else
     if (may_fade && global_settings.fade_on_stop)
         fade(false, updatewps);
     else
-#endif
         audio_pause();
+#endif
 
     if (global_settings.pause_rewind) {
         long newpos;
@@ -139,18 +153,26 @@
         audio_ff_rewind(newpos > 0 ? newpos : 0);
     }
 
-    (void)may_fade; (void)updatewps;
+    (void)may_fade;
 }
 
 void unpause_action(bool may_fade, bool updatewps)
 {
-#if CONFIG_CODEC != SWCODEC
+#if CONFIG_CODEC == SWCODEC
+    /* Do audio first, then update, unless skin were to use its local
+       status in which case, reverse it */
+    audio_resume();
+
+    if (updatewps)
+        update_non_static();
+#else
     if (may_fade && global_settings.fade_on_stop)
         fade(true, updatewps);
     else
-#endif
         audio_resume();
-    (void)may_fade; (void)updatewps;
+#endif
+
+    (void)may_fade;
 }        
 
 #if CONFIG_CODEC != SWCODEC
@@ -159,7 +181,7 @@
     int fp_global_vol = global_settings.volume << 8;
     int fp_min_vol = sound_min(SOUND_VOLUME) << 8;
     int fp_step = (fp_global_vol - fp_min_vol) / 10;
-    int i;
+
     skin_get_global_state()->is_fading = !fade_in;
     if (fade_in) {
         /* fade in */
@@ -171,10 +193,8 @@
         sleep(HZ/10); /* let audio thread run */
         audio_resume();
         
-        if (updatewps) {
-            FOR_NB_SCREENS(i)
-                skin_update(WPS, i, SKIN_REFRESH_NON_STATIC);
-        }
+        if (updatewps)
+            update_non_static();
 
         while (fp_volume < fp_global_vol - fp_step) {
             fp_volume += fp_step;
@@ -187,10 +207,8 @@
         /* fade out */
         int fp_volume = fp_global_vol;
 
-        if (updatewps) {
-            FOR_NB_SCREENS(i)
-                skin_update(WPS, i, SKIN_REFRESH_NON_STATIC);
-        }
+        if (updatewps)
+            update_non_static();
 
         while (fp_volume > fp_min_vol + fp_step) {
             fp_volume -= fp_step;
@@ -1140,6 +1158,7 @@
                 fade(false, true);
 #else
             audio_pause();
+            update_non_static();
 #endif
             if (bookmark)
                 bookmark_autobookmark(true);
diff --git a/apps/pcmbuf.c b/apps/pcmbuf.c
index 2e8bc3f..2ba6b6f 100644
--- a/apps/pcmbuf.c
+++ b/apps/pcmbuf.c
@@ -91,6 +91,13 @@
 
 /* Fade effect */
 static unsigned int fade_vol = MIX_AMP_UNITY;
+static enum
+{
+    PCM_NOT_FADING = 0,
+    PCM_FADING_IN,
+    PCM_FADING_OUT,
+} fade_state = PCM_NOT_FADING;
+static bool fade_out_complete = false;
 
 /* Voice */
 static bool soft_mode = false;
@@ -628,8 +635,9 @@
  * operations involved in sending a new chunk to the DMA. */
 static void pcmbuf_pcm_callback(unsigned char** start, size_t* size)
 {
+    struct chunkdesc *pcmbuf_current = read_chunk;
+
     {
-        struct chunkdesc *pcmbuf_current = read_chunk;
         /* Take the finished chunk out of circulation */
         read_chunk = pcmbuf_current->link;
 
@@ -657,25 +665,28 @@
 
     {
         /* Commit last samples at end of playlist */
-        if (pcmbuffer_fillpos && !read_chunk)
+        if (pcmbuffer_fillpos && !pcmbuf_current)
         {
             logf("pcmbuf_pcm_callback: commit last samples");
             commit_chunk(false);
         }
     }
 
+    /* Stop at this frame */
+    pcmbuf_current = fade_out_complete ? NULL : read_chunk;
+
     {
         /* Send the new chunk to the DMA */
-        if(read_chunk)
+        if(pcmbuf_current)
         {
-            last_chunksize = read_chunk->size;
+            last_chunksize = pcmbuf_current->size;
             pcmbuf_unplayed_bytes -= last_chunksize;
             *size = last_chunksize;
-            *start = read_chunk->addr;
+            *start = pcmbuf_current->addr;
         }
         else
         {
-            /* No more chunks */
+            /* No more chunks or pause indicated */
             logf("pcmbuf_pcm_callback: no more chunks");
             last_chunksize = 0;
             *size = 0;
@@ -694,8 +705,8 @@
         logf("pcmbuf_play_start");
         last_chunksize = read_chunk->size;
         pcmbuf_unplayed_bytes -= last_chunksize;
-        mixer_channel_play_data(PCM_MIXER_CHAN_PLAYBACK,
-                                pcmbuf_pcm_callback, NULL, 0);
+        mixer_channel_play_data(PCM_MIXER_CHAN_PLAYBACK, pcmbuf_pcm_callback,
+                                read_chunk->addr, last_chunksize);
     }
 }
 
@@ -1087,58 +1098,75 @@
 /* Quiet-down the channel if 'shhh' is true or else play at normal level */
 void pcmbuf_soft_mode(bool shhh)
 {
+    /* "Hate this" alert (messing with IRQ in app code): Have to block
+       the tick or improper order could leave volume in soft mode if
+       fading reads the old value first but updates after us. */
+    int oldlevel = disable_irq_save();
     soft_mode = shhh;
     pcmbuf_update_volume();
+    restore_irq(oldlevel);
 }
 
-/* Fade channel in or out */
+/* Tick that does the fade for the playback channel */
+static void pcmbuf_fade_tick(void)
+{
+    /* ~1/3 second for full range fade */
+    const unsigned int fade_step = MIX_AMP_UNITY / (HZ / 3);
+
+    if (fade_state == PCM_FADING_IN)
+        fade_vol += MIN(fade_step, MIX_AMP_UNITY - fade_vol);
+    else if (fade_state == PCM_FADING_OUT)
+        fade_vol -= MIN(fade_step, fade_vol - MIX_AMP_MUTE);
+
+    pcmbuf_update_volume();
+
+    if (fade_vol == MIX_AMP_MUTE || fade_vol == MIX_AMP_UNITY)
+    {
+        /* Fade is complete */
+        tick_remove_task(pcmbuf_fade_tick);
+
+        if (fade_state == PCM_FADING_OUT)
+        {
+            /* Tell PCM to stop at its earliest convenience */
+            fade_out_complete = true;
+        }
+
+        fade_state = PCM_NOT_FADING;
+    }
+}
+
+/* Fade channel in or out in the background - must pause it first */
 void pcmbuf_fade(bool fade, bool in)
 {
+    pcm_play_lock();
+
+    if (fade_state != PCM_NOT_FADING)
+        tick_remove_task(pcmbuf_fade_tick);
+
+    fade_out_complete = false;
+
+    pcm_play_unlock();
+
     if (!fade)
     {
         /* Simply set the level */
+        fade_state = PCM_NOT_FADING;
         fade_vol = in ? MIX_AMP_UNITY : MIX_AMP_MUTE;
+        pcmbuf_update_volume();
     }
     else
     {
-        /* Start from the opposing end */
-        fade_vol = in ? MIX_AMP_MUTE : MIX_AMP_UNITY;
-    }
-
-    pcmbuf_update_volume();
-
-    if (fade)
-    {
-        /* Do this on thread for now */
-#ifdef HAVE_PRIORITY_SCHEDULING
-        int old_prio = thread_set_priority(thread_self(), PRIORITY_REALTIME);
-#endif
-
-        while (1)
-        {
-            /* Linear fade actually sounds better */
-            if (in)
-                fade_vol += MIN(MIX_AMP_UNITY/16, MIX_AMP_UNITY - fade_vol);
-            else
-                fade_vol -= MIN(MIX_AMP_UNITY/16, fade_vol - MIX_AMP_MUTE);
-
-            pcmbuf_update_volume();
-
-            if (fade_vol > MIX_AMP_MUTE && fade_vol < MIX_AMP_UNITY)
-            {
-                sleep(0);
-                continue;
-            }
-
-            break;
-        }
-
-#ifdef HAVE_PRIORITY_SCHEDULING
-        thread_set_priority(thread_self(), old_prio);
-#endif
+        /* Set direction and resume fade from current point */
+        fade_state = in ? PCM_FADING_IN : PCM_FADING_OUT;
+        tick_add_task(pcmbuf_fade_tick);
     }
 }
 
+/* Return 'true' if fade is in progress */
+bool pcmbuf_fading(void)
+{
+    return fade_state != PCM_NOT_FADING;
+}
 
 /** Misc */
 
diff --git a/apps/pcmbuf.h b/apps/pcmbuf.h
index b7f5a3c..a5cd316 100644
--- a/apps/pcmbuf.h
+++ b/apps/pcmbuf.h
@@ -65,6 +65,7 @@
 
 /* Misc */
 void pcmbuf_fade(bool fade, bool in);
+bool pcmbuf_fading(void);
 void pcmbuf_soft_mode(bool shhh);
 bool pcmbuf_is_lowdata(void);
 void pcmbuf_set_low_latency(bool state);
diff --git a/apps/playback.c b/apps/playback.c
index a38534a..3adf6f6 100644
--- a/apps/playback.c
+++ b/apps/playback.c
@@ -1100,6 +1100,14 @@
         pcmbuf_play_stop();
 }
 
+/* Wait for any in-progress fade to complete */
+static void audio_wait_fade_complete(void)
+{
+    /* Just loop until it's done */
+    while (pcmbuf_fading())
+        sleep(0);
+}
+
 /* End the ff/rw mode */
 static void audio_ff_rewind_end(void)
 {
@@ -2439,7 +2447,12 @@
     if (play_status == PLAY_STOPPED)
         return;
 
-    pcmbuf_fade(global_settings.fade_on_stop, false);
+    bool do_fade = global_settings.fade_on_stop && filling != STATE_ENDED;
+
+    pcmbuf_fade(do_fade, false);
+
+    /* Wait for fade-out */
+    audio_wait_fade_complete();
 
     /* Stop the codec and unload it */
     halt_decoding_track(true);
@@ -2480,21 +2493,6 @@
     if (play_status == PLAY_STOPPED || pause == (play_status == PLAY_PAUSED))
         return;
 
-    bool const do_fade = global_settings.fade_on_stop;
-
-    if (pause)
-        pcmbuf_fade(do_fade, false);
-
-    if (!ff_rw_mode)
-    {
-        /* Not in ff/rw mode - may set the state (otherwise this could make
-           old data play because seek hasn't completed and cleared it) */
-        pcmbuf_pause(pause);
-    }
-
-    if (!pause)
-        pcmbuf_fade(do_fade, true);
-
     play_status = pause ? PLAY_PAUSED : PLAY_PLAYING;
 
     if (!pause && codec_skip_pending)
@@ -2502,6 +2500,16 @@
         /* Actually do the skip that is due - resets the status flag */
         audio_on_codec_complete(codec_skip_status);
     }
+
+    bool do_fade = global_settings.fade_on_stop;
+
+    pcmbuf_fade(do_fade, !pause);
+
+    if (!ff_rw_mode && !(do_fade && pause))
+    {
+        /* Not in ff/rw mode - can actually change the audio state now */
+        pcmbuf_pause(pause);
+    }
 }
 
 /* Skip a certain number of tracks forwards or backwards
@@ -2644,6 +2652,8 @@
 
     ff_rw_mode = true;
 
+    audio_wait_fade_complete();
+
     if (play_status == PLAY_PAUSED)
         return;