Commit work started in FS#12153 to put timing/position information in PCM
buffer chunks.

* Samples and position indication is closely associated with audio data
  instead of compensating by a latency constant. Alleviates problems with
  using the elapsed as a track indicator where it could be off by several
  steps.

* Timing is accurate throughout track even if resampling for pitch shift,
  whereas before it updated during transition latency at the normal 1:1 rate.

* Simpler PCM buffer with a constant chunk size, no linked lists.

In converting crossfade, a minor change was made to not change the WPS until
the fade-in of the incoming track, whereas before it would change upon the
start of the fade-out of the outgoing track possibly having the WPS change
with far too much lead time.

Codec changes are to set elapsed times *before* writing next PCM frame because
 time and position data last set are saved in the next committed PCM chunk. 


git-svn-id: svn://svn.rockbox.org/rockbox/trunk@30366 a1c6a512-1295-4272-9138-f99709370657
diff --git a/apps/codec_thread.c b/apps/codec_thread.c
index 945f0b0..199bb0e 100644
--- a/apps/codec_thread.c
+++ b/apps/codec_thread.c
@@ -77,9 +77,10 @@
 static int codec_type = AFMT_UNKNOWN; /* Codec type (C,A-) */
 
 /* Private interfaces to main playback control */
-extern void audio_codec_update_elapsed(unsigned long value);
-extern void audio_codec_update_offset(size_t value);
-extern void audio_queue_post(long id, intptr_t data);
+extern void audio_codec_update_elapsed(unsigned long elapsed);
+extern void audio_codec_update_offset(size_t offset);
+extern void audio_codec_complete(int status);
+extern void audio_codec_seek_complete(void);
 extern struct codec_api ci; /* from codecs.c */
 
 /* Codec thread */
@@ -251,7 +252,7 @@
         if (out_count <= 0)
             return;
 
-        pcmbuf_write_complete(out_count);
+        pcmbuf_write_complete(out_count, ci.id3->elapsed, ci.id3->offset);
 
         count -= inp_count;
     }
@@ -334,9 +335,11 @@
     /* Clear DSP */
     dsp_configure(ci.dsp, DSP_FLUSH, 0);
 
+    /* Sync position */
+    audio_codec_update_offset(ci.curpos);
+
     /* Post notification to audio thread */
-    LOGFQUEUE("audio > Q_AUDIO_CODEC_SEEK_COMPLETE");
-    audio_queue_post(Q_AUDIO_CODEC_SEEK_COMPLETE, 0);
+    audio_codec_seek_complete();
 
     /* Wait for urgent or go message */
     do
@@ -521,8 +524,7 @@
 
         /* Notify audio that we're done for better or worse - advise of the
            status */
-        LOGFQUEUE("codec > audio Q_AUDIO_CODEC_COMPLETE: %d", status);
-        audio_queue_post(Q_AUDIO_CODEC_COMPLETE, status);
+        audio_codec_complete(status);
     }
 }
 
diff --git a/apps/codecs/a52.c b/apps/codecs/a52.c
index 4cd293e..641e2d0 100644
--- a/apps/codecs/a52.c
+++ b/apps/codecs/a52.c
@@ -158,7 +158,7 @@
     }
     else {
         ci->seek_buffer(ci->id3->first_frame_offset);
-        samplesdone = 0;
+        ci->set_elapsed(0);
     }
 
     while (1) {
diff --git a/apps/codecs/a52_rm.c b/apps/codecs/a52_rm.c
index c1930aa..0aa3edc 100644
--- a/apps/codecs/a52_rm.c
+++ b/apps/codecs/a52_rm.c
@@ -178,6 +178,7 @@
     }
     else {
         /* Seek to the first packet */
+        ci->set_elapsed(0);
         ci->advance_buffer(rmctx.data_offset + DATA_HEADER_SIZE );
     }
 
diff --git a/apps/codecs/aac.c b/apps/codecs/aac.c
index 52e08c7..f1e81ba 100644
--- a/apps/codecs/aac.c
+++ b/apps/codecs/aac.c
@@ -134,8 +134,6 @@
         if (m4a_seek_raw(&demux_res, &input_stream, file_offset,
                           &sound_samples_done, (int*) &i)) {
             sound_samples_done *= sbr_fac;
-            elapsed_time = (sound_samples_done * 10) / (ci->id3->frequency / 100);
-            ci->set_elapsed(elapsed_time);
         } else {
             sound_samples_done = 0;
         }
@@ -143,6 +141,9 @@
     } else {
         sound_samples_done = 0;
     }
+
+    elapsed_time = (sound_samples_done * 10) / (ci->id3->frequency / 100);
+    ci->set_elapsed(elapsed_time);
     
     if (i == 0) 
     {
diff --git a/apps/codecs/adx.c b/apps/codecs/adx.c
index e75e7dc..8144d5f 100644
--- a/apps/codecs/adx.c
+++ b/apps/codecs/adx.c
@@ -209,7 +209,7 @@
 
     /* get in position */
     ci->seek_buffer(bufoff);
-
+    ci->set_elapsed(0);
 
     /* setup pcm buffer format */
     ci->configure(DSP_SWITCH_FREQUENCY, ci->id3->frequency);
@@ -276,6 +276,11 @@
                 loop_count++;
             }
             ci->seek_buffer(bufoff);
+
+            ci->set_elapsed(
+               ((end_adr-start_adr)*loop_count + bufoff-chanstart)*
+               1000LL/avgbytespersec);
+
             ci->seek_complete();
         }
 
diff --git a/apps/codecs/aiff.c b/apps/codecs/aiff.c
index a8185b4..333bcd0 100644
--- a/apps/codecs/aiff.c
+++ b/apps/codecs/aiff.c
@@ -288,6 +288,8 @@
         bytesdone = 0;
     }
 
+    ci->set_elapsed(decodedsamples*1000LL/ci->id3->frequency);
+
     /* The main decoder loop */
     endofstream = 0;
 
diff --git a/apps/codecs/alac.c b/apps/codecs/alac.c
index b6852f5..144d796 100644
--- a/apps/codecs/alac.c
+++ b/apps/codecs/alac.c
@@ -97,6 +97,8 @@
         }
     }
 
+    ci->set_elapsed(elapsedtime);
+
     /* The main decoding loop */
     while (i < demux_res.num_sample_byte_sizes) {
         enum codec_command_action action = ci->get_command(&param);
diff --git a/apps/codecs/ape.c b/apps/codecs/ape.c
index 8f95a01..ed6ea21 100644
--- a/apps/codecs/ape.c
+++ b/apps/codecs/ape.c
@@ -220,6 +220,9 @@
         firstbyte = 3;  /* Take account of the little-endian 32-bit byte ordering */
     }
 
+    elapsedtime = (samplesdone*10)/(ape_ctx.samplerate/100);
+    ci->set_elapsed(elapsedtime);
+
     /* Initialise the buffer */
     inbuffer = ci->request_buffer(&bytesleft, INPUT_CHUNKSIZE);
 
diff --git a/apps/codecs/au.c b/apps/codecs/au.c
index ef30835..cb75c74 100644
--- a/apps/codecs/au.c
+++ b/apps/codecs/au.c
@@ -253,6 +253,8 @@
         bytesdone = 0;
     }
 
+    ci->set_elapsed(decodedsamples*1000LL/ci->id3->frequency);
+
     /* The main decoder loop */
     endofstream = 0;
 
diff --git a/apps/codecs/cook.c b/apps/codecs/cook.c
index 4a47e74..55188aa 100644
--- a/apps/codecs/cook.c
+++ b/apps/codecs/cook.c
@@ -105,8 +105,10 @@
         param = (int)resume_offset * ((sps * 8 * 1000)/rmctx.bit_rate);
         action = CODEC_ACTION_SEEK_TIME;
     }
+    else {
+        ci->set_elapsed(0);
+    }
 
-    ci->set_elapsed(0);
     ci->advance_buffer(rmctx.data_offset + DATA_HEADER_SIZE);
 
     /* The main decoder loop */
diff --git a/apps/codecs/flac.c b/apps/codecs/flac.c
index a5521b5..c91a173 100644
--- a/apps/codecs/flac.c
+++ b/apps/codecs/flac.c
@@ -460,7 +460,9 @@
     codec_set_replaygain(ci->id3);
 
     flac_seek_offset(&fc, samplesdone);
-    samplesdone=0;
+    samplesdone=fc.samplenumber+fc.blocksize;
+    elapsedtime=(samplesdone*10)/(ci->id3->frequency/100);
+    ci->set_elapsed(elapsedtime);
 
     /* The main decoding loop */
     frame=0;
diff --git a/apps/codecs/mod.c b/apps/codecs/mod.c
index 3703ecd..5bd6499 100644
--- a/apps/codecs/mod.c
+++ b/apps/codecs/mod.c
@@ -1333,12 +1333,11 @@
             /* New time is ready in param */
             modplayer.patterntableposition = param/1000;
             modplayer.currentline = 0;
-            ci->set_elapsed(modplayer.patterntableposition*1000+500);
             ci->seek_complete();
         }
 
         if(old_patterntableposition != modplayer.patterntableposition) {
-          ci->set_elapsed(modplayer.patterntableposition*1000+500);
+          ci->set_elapsed(modplayer.patterntableposition*1000);
           old_patterntableposition=modplayer.patterntableposition;
         }
 
diff --git a/apps/codecs/mpa.c b/apps/codecs/mpa.c
index c9e2131..ac81f06 100644
--- a/apps/codecs/mpa.c
+++ b/apps/codecs/mpa.c
@@ -144,6 +144,7 @@
 {
     unsigned long offset = id3->offset > id3->first_frame_offset ?
         id3->offset - id3->first_frame_offset : 0;
+    unsigned long elapsed = id3->elapsed;
 
     if ( id3->vbr ) {
         if ( id3->has_toc ) {
@@ -172,27 +173,28 @@
             /* set time for this percent (divide before multiply to prevent
                overflow on long files. loss of precision is negligible on
                short files) */
-            id3->elapsed = i * (id3->length / 100);
+            elapsed = i * (id3->length / 100);
 
             /* calculate remainder time */
             plen = (nextpos - relpos) * (id3->filesize / 256);
-            id3->elapsed += (((remainder * 100) / plen) *
-                             (id3->length / 10000));
+            elapsed += (((remainder * 100) / plen) * (id3->length / 10000));
         }
         else {
             /* no TOC exists. set a rough estimate using average bitrate */
             int tpk = id3->length /
                 ((id3->filesize - id3->first_frame_offset - id3->id3v1len) /
                 1024);
-            id3->elapsed = offset / 1024 * tpk;
+            elapsed = offset / 1024 * tpk;
         }
     }
     else
     {
         /* constant bitrate, use exact calculation */
         if (id3->bitrate != 0)
-            id3->elapsed = offset / (id3->bitrate / 8);
+            elapsed = offset / (id3->bitrate / 8);
     }
+
+    ci->set_elapsed(elapsed);
 }
 
 #ifdef MPA_SYNTH_ON_COP
diff --git a/apps/codecs/mpc.c b/apps/codecs/mpc.c
index 7388799..a405c72 100644
--- a/apps/codecs/mpc.c
+++ b/apps/codecs/mpc.c
@@ -123,6 +123,8 @@
     codec_set_replaygain(ci->id3);
 
     /* Resume to saved sample offset. */
+    elapsed_time = 0;
+
     if (samplesdone > 0) 
     {
         if (mpc_demux_seek_sample(demux, samplesdone) == MPC_STATUS_OK) 
@@ -136,6 +138,8 @@
         }
     }
 
+    ci->set_elapsed(elapsed_time);
+
     /* This is the decoding loop. */
     do 
     {
diff --git a/apps/codecs/shorten.c b/apps/codecs/shorten.c
index db66991..a8ab3f3 100644
--- a/apps/codecs/shorten.c
+++ b/apps/codecs/shorten.c
@@ -99,6 +99,8 @@
     sc.bitindex = sc.gb.index - 8*consumed;
 
 seek_start:
+    ci->set_elapsed(0);
+
     /* The main decoding loop */
     ci->memset(&decoded0, 0, sizeof(int32_t)*MAX_DECODE_SIZE);
     ci->memset(&decoded1, 0, sizeof(int32_t)*MAX_DECODE_SIZE);
@@ -118,7 +120,6 @@
             if (param == 0 &&
                 ci->seek_buffer(sc.header_bits/8 + ci->id3->first_frame_offset)) {
                 sc.bitindex = sc.header_bits - 8*(sc.header_bits/8);
-                ci->set_elapsed(0);
                 ci->seek_complete();
                 goto seek_start;
             }
diff --git a/apps/codecs/sid.c b/apps/codecs/sid.c
index fed1e8a..9b19a20 100644
--- a/apps/codecs/sid.c
+++ b/apps/codecs/sid.c
@@ -1299,8 +1299,8 @@
             nSamplesToRender = 0;       /* Start the rendering from scratch */
 
             /* Set the elapsed time to the current subsong (in seconds) */
-            ci->seek_complete();
             ci->set_elapsed(subSong*1000);
+            ci->seek_complete();
         }
         
         nSamplesRendered = 0;
diff --git a/apps/codecs/smaf.c b/apps/codecs/smaf.c
index 9211daa..0203d1b 100644
--- a/apps/codecs/smaf.c
+++ b/apps/codecs/smaf.c
@@ -429,6 +429,8 @@
         bytesdone = 0;
     }
 
+    ci->set_elapsed(decodedsamples*1000LL/ci->id3->frequency);
+
     /* The main decoder loop */
     endofstream = 0;
 
diff --git a/apps/codecs/spc.c b/apps/codecs/spc.c
index 6b21f9a..3b00747 100644
--- a/apps/codecs/spc.c
+++ b/apps/codecs/spc.c
@@ -560,6 +560,8 @@
         return CODEC_ERROR;
 
     DEBUGF("SPC: read size = 0x%lx\n",(unsigned long)buffersize);
+    ci->set_elapsed(0);
+
     do
     {
         if (load_spc_buffer(buffer, buffersize)) {
diff --git a/apps/codecs/speex.c b/apps/codecs/speex.c
index c01bcfd..3d82ce7 100644
--- a/apps/codecs/speex.c
+++ b/apps/codecs/speex.c
@@ -417,6 +417,7 @@
     }
 
     ci->seek_buffer(0);
+    ci->set_elapsed(0);
 
     stereo = speex_stereo_state_init();
     spx_ogg_sync_init(&oy);
diff --git a/apps/codecs/tta.c b/apps/codecs/tta.c
index c75f2b0..dda33b3 100644
--- a/apps/codecs/tta.c
+++ b/apps/codecs/tta.c
@@ -90,6 +90,8 @@
             decodedsamples = new_pos;
     }
 
+    ci->set_elapsed((uint64_t)info.LENGTH * 1000 * decodedsamples / info.DATALENGTH);
+
     while (!endofstream)
     {
         enum codec_command_action action = ci->get_command(&param);
diff --git a/apps/codecs/vorbis.c b/apps/codecs/vorbis.c
index e02d459..fb1c9d7 100644
--- a/apps/codecs/vorbis.c
+++ b/apps/codecs/vorbis.c
@@ -196,6 +196,9 @@
         ci->set_elapsed(ov_time_tell(&vf));
         ci->set_offset(ov_raw_tell(&vf));
     }
+    else {
+        ci->set_elapsed(0);
+    }
 
     previous_section = -1;
     eof = 0;
diff --git a/apps/codecs/vox.c b/apps/codecs/vox.c
index bf274c6..279d003 100644
--- a/apps/codecs/vox.c
+++ b/apps/codecs/vox.c
@@ -141,6 +141,8 @@
         bytesdone = 0;
     }
 
+    ci->set_elapsed(decodedsamples*1000LL/ci->id3->frequency);
+
     /* The main decoder loop */
     endofstream = 0;
 
diff --git a/apps/codecs/wav.c b/apps/codecs/wav.c
index f6f83b1..d20331b 100644
--- a/apps/codecs/wav.c
+++ b/apps/codecs/wav.c
@@ -378,6 +378,8 @@
         bytesdone = 0;
     }
 
+    ci->set_elapsed(decodedsamples*1000LL/ci->id3->frequency);
+
     /* The main decoder loop */
     endofstream = 0;
 
diff --git a/apps/codecs/wav64.c b/apps/codecs/wav64.c
index c763e6f..184f39b 100644
--- a/apps/codecs/wav64.c
+++ b/apps/codecs/wav64.c
@@ -381,6 +381,8 @@
         bytesdone = 0;
     }
 
+    ci->set_elapsed(decodedsamples*1000LL/ci->id3->frequency);
+
     /* The main decoder loop */
     endofstream = 0;
 
diff --git a/apps/codecs/wavpack.c b/apps/codecs/wavpack.c
index 32f09d5..4d42391 100644
--- a/apps/codecs/wavpack.c
+++ b/apps/codecs/wavpack.c
@@ -75,7 +75,7 @@
     ci->configure(DSP_SET_STEREO_MODE, nchans == 2 ? STEREO_INTERLEAVED : STEREO_MONO);
     sr_100 = ci->id3->frequency / 100;
 
-    ci->set_elapsed (0);
+    ci->set_elapsed (WavpackGetSampleIndex (wpc) / sr_100 * 10);
 
     /* The main decoder loop */
 
diff --git a/apps/codecs/wma.c b/apps/codecs/wma.c
index c327faf..8986531 100644
--- a/apps/codecs/wma.c
+++ b/apps/codecs/wma.c
@@ -84,7 +84,6 @@
             % wfx.packet_size;
         ci->seek_buffer(resume_offset - packet_offset);
         elapsedtime = asf_get_timestamp(&i);
-        ci->set_elapsed(elapsedtime);
     }
     else
     {
@@ -93,6 +92,8 @@
         elapsedtime = 0;
     }
 
+    ci->set_elapsed(elapsedtime);
+
     resume_offset = 0;
     ci->configure(DSP_SWITCH_FREQUENCY, wfx.rate);
     ci->configure(DSP_SET_STEREO_MODE, wfx.channels == 1 ?
diff --git a/apps/codecs/wmapro.c b/apps/codecs/wmapro.c
index 17e311c..bab3b5a 100644
--- a/apps/codecs/wmapro.c
+++ b/apps/codecs/wmapro.c
@@ -79,6 +79,7 @@
     ci->seek_buffer(ci->id3->first_frame_offset);
     
     elapsedtime = 0;
+    ci->set_elapsed(0);
     
     /* The main decoding loop */
 
diff --git a/apps/codecs/wmavoice.c b/apps/codecs/wmavoice.c
index 64c8cd1..9cf5a49 100644
--- a/apps/codecs/wmavoice.c
+++ b/apps/codecs/wmavoice.c
@@ -109,6 +109,8 @@
     ci->seek_buffer(ci->id3->first_frame_offset);
     
     elapsedtime = 0;
+    ci->set_elapsed(0);
+
     resume_offset = 0;
     
     /* The main decoding loop */
diff --git a/apps/pcmbuf.c b/apps/pcmbuf.c
index 8736fe2..f57021d 100644
--- a/apps/pcmbuf.c
+++ b/apps/pcmbuf.c
@@ -8,6 +8,7 @@
  * $Id$
  *
  * Copyright (C) 2005 by Miika Pekkarinen
+ * Copyright (C) 2011 by Michael Sevakis
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
@@ -35,59 +36,69 @@
 #if (CONFIG_PLATFORM & PLATFORM_NATIVE)
 #include "cpu.h"
 #endif
-#include <string.h>
 #include "settings.h"
 #include "audio.h"
 #include "voice_thread.h"
 #include "dsp.h"
 
-#define PCMBUF_TARGET_CHUNK 32768 /* This is the target fill size of chunks
-                                     on the pcm buffer */
-#define PCMBUF_MINAVG_CHUNK 24576 /* This is the minimum average size of
-                                     chunks on the pcm buffer (or we run out
-                                     of buffer descriptors, which is
-                                     non-fatal) */
-#define PCMBUF_MIN_CHUNK     4096 /* We try to never feed a chunk smaller than
-                                     this to the DMA */
-#define CROSSFADE_BUFSIZE    8192 /* Size of the crossfade buffer */
+/* This is the target fill size of chunks on the pcm buffer
+   Can be any number of samples but power of two sizes make for faster and
+   smaller math - must be < 65536 bytes */
+#define PCMBUF_CHUNK_SIZE    8192u
+#define PCMBUF_GUARD_SIZE    1024u
 
-/* number of bytes played per second (sample rate * 2 channels * 2 bytes/sample) */
+/* Mnemonics for common data commit thresholds */
+#define COMMIT_CHUNKS        PCMBUF_CHUNK_SIZE
+#define COMMIT_ALL_DATA      1u
+
+ /* Size of the crossfade buffer where codec data is written to be faded
+    on commit */
+#define CROSSFADE_BUFSIZE    8192u
+
+/* Number of bytes played per second:
+   (sample rate * 2 channels * 2 bytes/sample) */
 #define BYTERATE            (NATIVE_FREQUENCY * 4)
 
 #if MEMORYSIZE > 2
-/* Keep watermark high for iPods at least (2s) */
+/* Keep watermark high for large memory target - at least (2s) */
 #define PCMBUF_WATERMARK    (BYTERATE * 2)
+#define MIN_BUFFER_SIZE     (BYTERATE * 3)
 #else
 #define PCMBUF_WATERMARK    (BYTERATE / 4)  /* 0.25 seconds */
+#define MIN_BUFFER_SIZE     (BYTERATE * 1)
 #endif
 
-/* Structure we can use to queue pcm chunks in memory to be played
- * by the driver code. */
+/* Describes each audio packet - keep it small since there are many of them */
 struct chunkdesc
 {
-    unsigned char *addr;
-    size_t size;
-    struct chunkdesc* link;
-    /* true if last chunk in the track */
-    bool end_of_track;
+    uint16_t       size;    /* Actual size (0 < size <= PCMBUF_CHUNK_SIZE) */
+    uint8_t        is_end;  /* Flag indicating end of track */
+    uint8_t        pos_key; /* Who put the position info in (0 = undefined) */
+    unsigned long  elapsed; /* Elapsed time to use */
+    off_t          offset;  /* Offset to use */
 };
 
-#define NUM_CHUNK_DESCS(bufsize) \
-    ((bufsize) / PCMBUF_MINAVG_CHUNK)
+/* General PCM buffer data */
+#define INVALID_BUF_INDEX   ((size_t)0 - (size_t)1)
 
-/* Size of the PCM buffer. */
-static size_t pcmbuf_size = 0;
-static char *pcmbuf_bufend;
-static char *pcmbuffer;
-/* Current PCM buffer write index. */
-static size_t pcmbuffer_pos;
-/* Amount pcmbuffer_pos will be increased.*/
-static size_t pcmbuffer_fillpos;
+static unsigned char *pcmbuf_buffer;
+static unsigned char *pcmbuf_guardbuf;
+static size_t pcmbuf_size;
+static struct chunkdesc *pcmbuf_descriptors;
+static unsigned int pcmbuf_desc_count;
+static unsigned int position_key = 1;
 
-static struct chunkdesc *first_desc;
+static size_t chunk_ridx;
+static size_t chunk_widx;
 
-/* Gapless playback */
-static bool track_transition;
+static size_t pcmbuf_bytes_waiting;
+
+static size_t pcmbuf_watermark;
+static struct chunkdesc *current_desc;
+
+static bool low_latency_mode = false;
+
+static bool pcmbuf_sync_position = false;
 
 /* Fade effect */
 static unsigned int fade_vol = MIX_AMP_UNITY;
@@ -104,184 +115,179 @@
 
 #ifdef HAVE_CROSSFADE
 /* Crossfade buffer */
-static char *fadebuf;
+static unsigned char *crossfade_buffer;
 
 /* Crossfade related state */
-static bool crossfade_enabled;
-static bool crossfade_enable_request;
+static int  crossfade_setting;
+static int  crossfade_enable_request;
 static bool crossfade_mixmode;
 static bool crossfade_auto_skip;
-static bool crossfade_active;
-static bool crossfade_track_change_started;
+
+static enum
+{
+    CROSSFADE_INACTIVE = 0,
+    CROSSFADE_TRACK_CHANGE_STARTED,
+    CROSSFADE_ACTIVE,
+} crossfade_status = CROSSFADE_INACTIVE;
 
 /* Track the current location for processing crossfade */
-static struct chunkdesc *crossfade_chunk;
-static size_t crossfade_sample;
+static size_t crossfade_index;
 
 /* Counters for fading in new data */
 static size_t crossfade_fade_in_total;
 static size_t crossfade_fade_in_rem;
-#endif
 
-static struct chunkdesc *read_chunk;
-static struct chunkdesc *read_end_chunk;
-static struct chunkdesc *write_chunk;
-static struct chunkdesc *write_end_chunk;
-static size_t last_chunksize;
+/* Defines for operations on position info when mixing/fading -
+   passed in offset parameter */
+enum
+{
+    MIXFADE_KEEP_POS = -1,    /* Keep position info in chunk */
+    MIXFADE_NULLIFY_POS = -2, /* Ignore position info in chunk */
+    /* Positive values cause stamping/restamping */
+};
 
-static size_t pcmbuf_unplayed_bytes;
-static size_t pcmbuf_watermark;
+static void crossfade_start(void);
+static void write_to_crossfade(size_t size, unsigned long elapsed,
+                               off_t offset);
+static void pcmbuf_finish_crossfade_enable(void);
+#endif /* HAVE_CROSSFADE */
 
-static bool low_latency_mode = false;
-static bool flush_pcmbuf = false;
-
+/* Thread */
 #ifdef HAVE_PRIORITY_SCHEDULING
 static int codec_thread_priority = PRIORITY_PLAYBACK;
 #endif
 
 /* Helpful macros for use in conditionals this assumes some of the above
  * static variable names */
-#define COMMIT_IF_NEEDED if(pcmbuffer_fillpos > PCMBUF_TARGET_CHUNK || \
-    (pcmbuffer_pos + pcmbuffer_fillpos) >= pcmbuf_size) commit_chunk(false)
-#define LOW_DATA(quarter_secs) \
-    (pcmbuf_unplayed_bytes < NATIVE_FREQUENCY * quarter_secs)
-
-#ifdef HAVE_CROSSFADE
-static void crossfade_start(void);
-static void write_to_crossfade(size_t length);
-static void pcmbuf_finish_crossfade_enable(void);
-#endif
+#define DATA_LEVEL(quarter_secs) (NATIVE_FREQUENCY * (quarter_secs))
 
 /* Callbacks into playback.c */
-extern void audio_pcmbuf_position_callback(unsigned int time);
+extern void audio_pcmbuf_position_callback(unsigned long elapsed,
+                                           off_t offset, unsigned int key);
 extern void audio_pcmbuf_track_change(bool pcmbuf);
 extern bool audio_pcmbuf_may_play(void);
+extern void audio_pcmbuf_sync_position(void);
 
 
 /**************************************/
 
-/* define this to show detailed chunkdesc usage information on the sim console */
-/*#define DESC_DEBUG*/
-
-#ifndef SIMULATOR
-#undef DESC_DEBUG
-#endif
-
-#ifdef DESC_DEBUG
-#define DISPLAY_DESC(caller) while(!show_desc(caller))
-#define DESC_IDX(desc)       (desc ? desc - first_desc : -1)
-#define SHOW_DESC(desc)      if(DESC_IDX(desc)==-1) DEBUGF("--"); \
-                             else DEBUGF("%02d", DESC_IDX(desc))
-#define SHOW_DESC_LINK(desc) if(desc){SHOW_DESC(desc->link);DEBUGF(" ");} \
-                             else DEBUGF("-- ")
-#define SHOW_DETAIL(desc)    DEBUGF(":");SHOW_DESC(desc); DEBUGF(">"); \
-                             SHOW_DESC_LINK(desc)
-#define SHOW_POINT(tag,desc) DEBUGF("%s",tag);SHOW_DETAIL(desc)
-#define SHOW_NUM(num,desc)   DEBUGF("%02d>",num);SHOW_DESC_LINK(desc)
-
-static bool show_desc(char *caller)
+/* Return number of commited bytes in buffer (committed chunks count as
+   a full chunk even if only partially filled) */
+static size_t pcmbuf_unplayed_bytes(void)
 {
-    if (show_desc_in_use) return false;
-    show_desc_in_use = true;
-    DEBUGF("%-14s\t", caller);
-    SHOW_POINT("r",  read_chunk);
-    SHOW_POINT("re", read_end_chunk);
-    DEBUGF(" ");
-    SHOW_POINT("w",  write_chunk);
-    SHOW_POINT("we", write_end_chunk);
-    DEBUGF("\n");
-    int i;
-    for (i = 0; i < pcmbuf_descs(); i++)
-    {
-        SHOW_NUM(i, (first_desc + i));
-        if (i%10 == 9) DEBUGF("\n");
-    }
-    DEBUGF("\n\n");
-    show_desc_in_use = false;
-    return true;
+    size_t ridx = chunk_ridx;
+    size_t widx = chunk_widx;
+
+    if (ridx > widx)
+        widx += pcmbuf_size;
+
+    return widx - ridx;
 }
-#else
-#define DISPLAY_DESC(caller) do{}while(0)
-#endif
+
+/* Return the next PCM chunk in the PCM buffer given a byte index into it */
+static size_t index_next(size_t index)
+{
+    index = ALIGN_DOWN(index + PCMBUF_CHUNK_SIZE, PCMBUF_CHUNK_SIZE);
+
+    if (index >= pcmbuf_size)
+        index -= pcmbuf_size;
+
+    return index;
+}
+
+/* Convert a byte offset in the PCM buffer into a pointer in the buffer */
+static FORCE_INLINE void * index_buffer(size_t index)
+{
+    return pcmbuf_buffer + index;
+}
+
+/* Convert a pointer in the buffer into an index offset */
+static FORCE_INLINE size_t buffer_index(void *p)
+{
+    return (uintptr_t)p - (uintptr_t)pcmbuf_buffer;
+}
+
+/* Return a chunk descriptor for a byte index in the buffer */
+static struct chunkdesc * index_chunkdesc(size_t index)
+{
+    return &pcmbuf_descriptors[index / PCMBUF_CHUNK_SIZE];
+}
+
+/* Return a chunk descriptor for a byte index in the buffer, offset by 'offset'
+   chunks */
+static struct chunkdesc * index_chunkdesc_offs(size_t index, int offset)
+{
+    int i = index / PCMBUF_CHUNK_SIZE;
+
+    if (offset != 0)
+    {
+        i = (i + offset) % pcmbuf_desc_count;
+
+        /* remainder => modulus */ 
+        if (i < 0)
+            i += pcmbuf_desc_count;
+    }
+
+    return &pcmbuf_descriptors[i];
+}
 
 
 /** Accept new PCM data */
 
-/* Commit PCM buffer samples as a new chunk for playback */
-static void commit_chunk(bool flush_next_time)
+/* Split the uncommitted data as needed into chunks, stopping when uncommitted
+   data is below the threshold */
+static void commit_chunks(size_t threshold)
 {
-    if (!pcmbuffer_fillpos)
-        return;
+    size_t index = chunk_widx;
+    size_t end_index = index + pcmbuf_bytes_waiting;
 
-    /* Never use the last buffer descriptor */
-    while (write_chunk == write_end_chunk) {
-        /* If this happens, something is being stupid */
-        if (!pcm_is_playing()) {
-            logf("commit_chunk error");
-            pcmbuf_play_start();
-        }
-        /* Let approximately one chunk of data playback */
-        sleep(HZ * PCMBUF_TARGET_CHUNK / BYTERATE);
-    }
+    /* Copy to the beginning of the buffer all data that must wrap */
+    if (end_index > pcmbuf_size)
+        memcpy(pcmbuf_buffer, pcmbuf_guardbuf, end_index - pcmbuf_size);
 
-    /* commit the chunk */
+    struct chunkdesc *desc = index_chunkdesc(index);
 
-    register size_t size = pcmbuffer_fillpos;
-    /* Grab the next description to write, and change the write pointer */
-    register struct chunkdesc *pcmbuf_current = write_chunk;
-    write_chunk = pcmbuf_current->link;
-    /* Fill in the values in the new buffer chunk */
-    pcmbuf_current->addr = &pcmbuffer[pcmbuffer_pos];
-    pcmbuf_current->size = size;
-    pcmbuf_current->end_of_track = false;
-    pcmbuf_current->link = NULL;
-
-    if (read_chunk != NULL)
+    do
     {
-        if (flush_pcmbuf)
-        {
-            /* Flush! Discard all data after the currently playing chunk,
-               and make the current chunk play next */
-            logf("commit_chunk: flush");
-            pcm_play_lock();
-            write_end_chunk->link = read_chunk->link;
-            read_chunk->link = pcmbuf_current;
-            while (write_end_chunk->link)
-            {
-                write_end_chunk = write_end_chunk->link;
-                pcmbuf_unplayed_bytes -= write_end_chunk->size;
-            }
+        size_t size = MIN(pcmbuf_bytes_waiting, PCMBUF_CHUNK_SIZE);
+        pcmbuf_bytes_waiting -= size;
 
-            read_chunk->end_of_track = track_transition;
-            pcm_play_unlock();
-        }
-        /* If there is already a read buffer setup, add to it */
-        else
-            read_end_chunk->link = pcmbuf_current;
+        /* Fill in the values in the new buffer chunk */
+        desc->size = (uint16_t)size;
+
+        /* Advance the current write chunk and make it available to the
+           PCM callback */
+        chunk_widx = index = index_next(index);
+        desc = index_chunkdesc(index);
+
+        /* Reset it before using it */
+        desc->is_end = 0;
+        desc->pos_key = 0;
     }
-    else
+    while (pcmbuf_bytes_waiting >= threshold);
+}
+
+/* If uncommitted data count is above or equal to the threshold, commit it */
+static FORCE_INLINE void commit_if_needed(size_t threshold)
+{
+    if (pcmbuf_bytes_waiting >= threshold)
+        commit_chunks(threshold);
+}
+
+/* Place positioning information in the chunk */
+static void stamp_chunk(struct chunkdesc *desc, unsigned long elapsed,
+                        off_t offset)
+{
+    /* One-time stamping of a given chunk by the same track - new track may
+       overwrite */
+    unsigned int key = position_key;
+
+    if (desc->pos_key != key)
     {
-        /* Otherwise create the buffer */
-        read_chunk = pcmbuf_current;
+        desc->pos_key = key;
+        desc->elapsed = elapsed;
+        desc->offset = offset;
     }
-
-    /* If flush_next_time is true, then the current chunk will be thrown out
-     * and the next chunk to be committed will be the next to be played.
-     * This is used to empty the PCM buffer for a track change. */
-    flush_pcmbuf = flush_next_time;
-
-    /* This is now the last buffer to read */
-    read_end_chunk = pcmbuf_current;
-
-    /* Update bytes counters */
-    pcmbuf_unplayed_bytes += size;
-
-    pcmbuffer_pos += size;
-    if (pcmbuffer_pos >= pcmbuf_size)
-        pcmbuffer_pos -= pcmbuf_size;
-
-    pcmbuffer_fillpos = 0;
-    DISPLAY_DESC("commit_chunk");
 }
 
 /* Set priority of the codec thread */
@@ -290,7 +296,8 @@
  * expects pcm_fill_state in tenth-% units (e.g. full pcm buffer is 10) */
 static void boost_codec_thread(int pcm_fill_state)
 {
-    static const int prios[11] = {
+    static const int8_t prios[11] =
+    {
         PRIORITY_PLAYBACK_MAX,      /*   0 - 10% */
         PRIORITY_PLAYBACK_MAX+1,    /*  10 - 20% */
         PRIORITY_PLAYBACK_MAX+3,    /*  20 - 30% */
@@ -298,12 +305,13 @@
         PRIORITY_PLAYBACK_MAX+7,    /*  40 - 50% */
         PRIORITY_PLAYBACK_MAX+8,    /*  50 - 60% */
         PRIORITY_PLAYBACK_MAX+9,    /*  60 - 70% */
-        /* raiseing priority above 70% shouldn't be needed */
+        /* raising priority above 70% shouldn't be needed */
         PRIORITY_PLAYBACK,          /*  70 - 80% */
         PRIORITY_PLAYBACK,          /*  80 - 90% */
         PRIORITY_PLAYBACK,          /*  90 -100% */
         PRIORITY_PLAYBACK,          /*      100% */
     };
+
     int new_prio = prios[pcm_fill_state];
 
     /* Keep voice and codec threads at the same priority or else voice
@@ -319,37 +327,86 @@
 #define boost_codec_thread(pcm_fill_state) do{}while(0)
 #endif /* HAVE_PRIORITY_SCHEDULING */
 
-/* Return true if the PCM buffer is able to receive new data.
- * Also maintain buffer level above the watermark. */
-static bool prepare_insert(size_t length)
+/* Get the next available buffer and size - assumes adequate space exists */
+static void * get_write_buffer(size_t *size)
 {
-    bool playing = mixer_channel_status(PCM_MIXER_CHAN_PLAYBACK) != CHANNEL_STOPPED;
+    /* Obtain current chunk fill address */
+    size_t index = chunk_widx + pcmbuf_bytes_waiting;
+    size_t index_end = pcmbuf_size + PCMBUF_GUARD_SIZE;
 
-    if (low_latency_mode)
-    {
-        /* 1/4s latency. */
-        if (!LOW_DATA(1) && playing)
-            return false;
-    }
+    /* Get count to the end of the buffer where a wrap will happen +
+       the guard */
+    size_t endsize = index_end - index;
 
-    /* Need to save PCMBUF_MIN_CHUNK to prevent wrapping overwriting */
-    if (pcmbuf_free() < length + PCMBUF_MIN_CHUNK)
-        return false;
+    /* Return available unwrapped space */
+    *size = MIN(*size, endsize);
+
+    return index_buffer(index);
+}
+
+/* Commit outstanding data leaving less than a chunk size remaining and
+   write position info to the first chunk */
+static void commit_write_buffer(size_t size, unsigned long elapsed, off_t offset)
+{
+    struct chunkdesc *desc = index_chunkdesc(chunk_widx);
+    stamp_chunk(desc, elapsed, offset);
+
+    /* Add this data and commit if one or more chunks are ready */
+    pcmbuf_bytes_waiting += size;
+
+    commit_if_needed(COMMIT_CHUNKS);
+}
+
+/* Request space in the buffer for writing output samples */
+void * pcmbuf_request_buffer(int *count)
+{
+    size_t size = *count * 4;
+
+#ifdef HAVE_CROSSFADE
+    /* We're going to crossfade to a new track, which is now on its way */
+    if (crossfade_status == CROSSFADE_TRACK_CHANGE_STARTED)
+        crossfade_start();
+
+    /* If crossfade has begun, put the new track samples in crossfade_buffer */
+    if (crossfade_status != CROSSFADE_INACTIVE && size > CROSSFADE_BUFSIZE)
+        size = CROSSFADE_BUFSIZE;
+#endif
+
+    enum channel_status status = mixer_channel_status(PCM_MIXER_CHAN_PLAYBACK);
+    size_t remaining = pcmbuf_unplayed_bytes();
+
+    /* Need to have length bytes to prevent wrapping overwriting - leave one
+       descriptor free to guard so that 0 != full in ring buffer */
+    size_t freespace = pcmbuf_free();
+
+    if (pcmbuf_sync_position)
+        audio_pcmbuf_sync_position();
+
+    if (freespace < size + PCMBUF_CHUNK_SIZE)
+        return NULL;
 
     /* Maintain the buffer level above the watermark */
-    if (playing)
+    if (status != CHANNEL_STOPPED)
     {
-        /* boost cpu if necessary */
-        if (pcmbuf_unplayed_bytes < pcmbuf_watermark)
+        if (low_latency_mode)
+        {
+            /* 1/4s latency. */
+            if (remaining > DATA_LEVEL(1))
+                return NULL;
+        }
+
+        /* Boost CPU if necessary */
+        size_t realrem = pcmbuf_size - freespace;
+
+        if (realrem < pcmbuf_watermark)
             trigger_cpu_boost();
-        boost_codec_thread(pcmbuf_unplayed_bytes*10/pcmbuf_size);
+
+        boost_codec_thread(realrem*10 / pcmbuf_size);
 
 #ifdef HAVE_CROSSFADE
         /* Disable crossfade if < .5s of audio */
-        if (LOW_DATA(2))
-        {
-            crossfade_active = false;
-        }
+        if (remaining < DATA_LEVEL(2))
+            crossfade_status = CROSSFADE_INACTIVE;
 #endif
     }
     else    /* !playing */
@@ -359,378 +416,344 @@
 
         /* If pre-buffered to the watermark, start playback */
 #if MEMORYSIZE > 2
-        if (!LOW_DATA(4))
+        if (remaining > DATA_LEVEL(4))
 #else
-        if (pcmbuf_unplayed_bytes > pcmbuf_watermark)
+        if (remaining > pcmbuf_watermark)
 #endif
         {
-            logf("pcm starting");
             if (audio_pcmbuf_may_play())
                 pcmbuf_play_start();
         }
     }
 
-    return true;
-}
-
-/* Request space in the buffer for writing output samples */
-void *pcmbuf_request_buffer(int *count)
-{
+    void *buf =
 #ifdef HAVE_CROSSFADE
-    /* we're going to crossfade to a new track, which is now on its way */
-    if (crossfade_track_change_started)
-        crossfade_start();
-
-    /* crossfade has begun, put the new track samples in fadebuf */
-    if (crossfade_active)
-    {
-        int cnt = MIN(*count, CROSSFADE_BUFSIZE/4);
-        if (prepare_insert(cnt << 2))
-        {
-            *count = cnt;
-            return fadebuf;
-        }
-    }
-    else
+        crossfade_status != CROSSFADE_INACTIVE ? crossfade_buffer :
 #endif
-    /* if possible, reserve room in the PCM buffer for new samples */
-    {
-        if(prepare_insert(*count << 2))
-        {
-            size_t pcmbuffer_index = pcmbuffer_pos + pcmbuffer_fillpos;
-            if (pcmbuf_size - pcmbuffer_index >= PCMBUF_MIN_CHUNK)
-            {
-                /* Usual case, there's space here */
-                return &pcmbuffer[pcmbuffer_index];
-            }
-            else
-            {
-                /* Wrap the buffer, the new samples go at the beginning */
-                commit_chunk(false);
-                pcmbuffer_pos = 0;
-                return &pcmbuffer[0];
-            }
-        }
-    }
-    /* PCM buffer not ready to receive new data yet */
-    return NULL;
+        get_write_buffer(&size);
+
+    *count = size / 4;
+    return buf;
 }
 
 /* Handle new samples to the buffer */
-void pcmbuf_write_complete(int count)
+void pcmbuf_write_complete(int count, unsigned long elapsed, off_t offset)
 {
-    size_t length = (size_t)(unsigned int)count << 2;
+    size_t size = count * 4;
+
 #ifdef HAVE_CROSSFADE
-    if (crossfade_active)
-        write_to_crossfade(length);
+    if (crossfade_status != CROSSFADE_INACTIVE)
+    {
+        write_to_crossfade(size, elapsed, offset);
+    }
     else
 #endif
     {
-        pcmbuffer_fillpos += length;
-        COMMIT_IF_NEEDED;
+        commit_write_buffer(size, elapsed, offset);
     }
+
+    /* Revert to position updates by PCM */
+    pcmbuf_sync_position = false;
 }
 
 
 /** Init */
-
-static inline void init_pcmbuffers(void)
+static unsigned int get_next_required_pcmbuf_chunks(void)
 {
-    first_desc = write_chunk;
-    struct chunkdesc *next = write_chunk;
-    next++;
-    write_end_chunk = write_chunk;
-    while ((void *)next < (void *)pcmbuf_bufend) {
-        write_end_chunk->link=next;
-        write_end_chunk=next;
-        next++;
-    }
-    DISPLAY_DESC("init");
-}
-
-static size_t get_next_required_pcmbuf_size(void)
-{
-    size_t seconds = 1;
+    size_t size = MIN_BUFFER_SIZE;
 
 #ifdef HAVE_CROSSFADE
-    if (crossfade_enable_request)
-        seconds += global_settings.crossfade_fade_out_delay +
-                   global_settings.crossfade_fade_out_duration;
+    if (crossfade_enable_request != CROSSFADE_ENABLE_OFF)
+    {
+        size_t seconds = global_settings.crossfade_fade_out_delay +
+                         global_settings.crossfade_fade_out_duration;
+        size += seconds * BYTERATE;
+    }
 #endif
 
-#if MEMORYSIZE > 2
-    /* Buffer has to be at least 2s long. */
-    seconds += 2;
-#endif
-    logf("pcmbuf len: %ld", (long)seconds);
-    return seconds * BYTERATE;
+    logf("pcmbuf len: %lu", (unsigned long)(size / BYTERATE));
+    return size / PCMBUF_CHUNK_SIZE;
 }
 
-/* Initialize the pcmbuffer the structure looks like this:
- * ...|---------PCMBUF---------[|FADEBUF]|DESCS|... */
+/* Initialize the ringbuffer state */
+static void init_buffer_state(void)
+{
+    /* Reset counters */
+    chunk_ridx = chunk_widx = 0;
+    pcmbuf_bytes_waiting = 0;
+
+    /* Reset first descriptor */
+    struct chunkdesc *desc = pcmbuf_descriptors;
+    desc->is_end = 0;
+    desc->pos_key = 0;
+}
+
+/* Initialize the PCM buffer. The structure looks like this:
+ * ...[|FADEBUF]|---------PCMBUF---------|GUARDBUF|DESCS| */
 size_t pcmbuf_init(unsigned char *bufend)
 {
-    pcmbuf_bufend = bufend;
-    pcmbuf_size = get_next_required_pcmbuf_size();
-    write_chunk = (struct chunkdesc *)pcmbuf_bufend -
-        NUM_CHUNK_DESCS(pcmbuf_size);
+    unsigned char *bufstart;
+
+    /* Set up the buffers */
+    pcmbuf_desc_count = get_next_required_pcmbuf_chunks();
+    pcmbuf_size = pcmbuf_desc_count * PCMBUF_CHUNK_SIZE;
+    pcmbuf_descriptors = (struct chunkdesc *)bufend - pcmbuf_desc_count;
+
+    pcmbuf_buffer = (void *)pcmbuf_descriptors -
+                    pcmbuf_size - PCMBUF_GUARD_SIZE;
+
+    /* Mem-align buffer chunks for more efficient handling in lower layers */
+    pcmbuf_buffer = ALIGN_DOWN(pcmbuf_buffer, (uintptr_t)MEM_ALIGN_SIZE);
+
+    pcmbuf_guardbuf = pcmbuf_buffer + pcmbuf_size;
+    bufstart = pcmbuf_buffer;
 
 #ifdef HAVE_CROSSFADE
-    fadebuf = (unsigned char *)write_chunk -
-        (crossfade_enable_request ? CROSSFADE_BUFSIZE : 0);
-    pcmbuffer = fadebuf - pcmbuf_size;
-#else
-    pcmbuffer = (unsigned char *)write_chunk - pcmbuf_size;
-#endif
+    /* Allocate FADEBUF if it will be needed */
+    if (crossfade_enable_request != CROSSFADE_ENABLE_OFF)
+    {
+        bufstart -= CROSSFADE_BUFSIZE;
+        crossfade_buffer = bufstart;
+    }
 
-    init_pcmbuffers();
-
-#ifdef HAVE_CROSSFADE
     pcmbuf_finish_crossfade_enable();
-#else
+#else  /* !HAVE_CROSSFADE */
     pcmbuf_watermark = PCMBUF_WATERMARK;
-#endif
+#endif /* HAVE_CROSSFADE */
 
-    pcmbuf_play_stop();
+    init_buffer_state();
 
     pcmbuf_soft_mode(false);
 
-    return pcmbuf_bufend - pcmbuffer;
+    return bufend - bufstart;
 }
 
 
 /** Track change */
+
+/* Place a track change notification in a specific descriptor or post it
+   immediately if the buffer is empty or the index is invalid */
+static void pcmbuf_monitor_track_change_ex(size_t index, int offset)
+{
+    if (chunk_ridx != chunk_widx && index != INVALID_BUF_INDEX)
+    {
+        /* If monitoring, set flag in specified chunk */
+        index_chunkdesc_offs(index, offset)->is_end = 1;
+    }
+    else
+    {
+        /* Post now if no outstanding buffers exist */
+        audio_pcmbuf_track_change(false);
+    }
+}
+
+/* Clear end of track and optionally the positioning info for all data */
+static void pcmbuf_cancel_track_change(bool position)
+{
+    size_t index = chunk_ridx;
+
+    while (1)
+    {
+        struct chunkdesc *desc = index_chunkdesc(index);
+
+        desc->is_end = 0;
+
+        if (position)
+            desc->pos_key = 0;
+
+        if (index == chunk_widx)
+            break;
+
+        index = index_next(index);
+    }
+}
+
+/* Place a track change notification at the end of the buffer or post it
+   immediately if the buffer is empty */
 void pcmbuf_monitor_track_change(bool monitor)
 {
     pcm_play_lock();
 
-    if (last_chunksize != 0)
-    {
-        /* If monitoring, wait until this track runs out. Place in
-           currently playing chunk. If not, cancel notification. */
-        track_transition = monitor;
-        read_end_chunk->end_of_track = monitor;
-        if (!monitor)
-        {
-            /* Clear all notifications */
-            struct chunkdesc *desc = first_desc;
-            struct chunkdesc *end = desc + pcmbuf_descs();
-            while (desc < end)
-                desc++->end_of_track = false;
-        }
-    }
+    if (monitor)
+        pcmbuf_monitor_track_change_ex(chunk_widx, -1);
     else
-    {
-        /* Post now if PCM stopped and last buffer was sent. */
-        track_transition = false;
-        if (monitor)
-            audio_pcmbuf_track_change(false);
-    }
+        pcmbuf_cancel_track_change(false);
 
     pcm_play_unlock();
 }
 
-bool pcmbuf_start_track_change(bool auto_skip)
+void pcmbuf_start_track_change(enum pcm_track_change_type type)
 {
-    bool crossfade = false;
 #ifdef HAVE_CROSSFADE
-    /* Determine whether this track change needs to crossfade */
-    if(crossfade_enabled && !pcmbuf_is_crossfade_active())
+    bool crossfade = false;
+#endif
+    bool auto_skip = type != TRACK_CHANGE_MANUAL;
+
+    /* Commit all outstanding data before starting next track - tracks don't
+       comingle inside a single buffer chunk */
+    commit_if_needed(COMMIT_ALL_DATA);
+
+    /* Update position key so that:
+       1) Positions are keyed to the track to which they belong for sync
+          purposes
+
+       2) Buffers stamped with the outgoing track's positions are restamped
+          to the incoming track's positions when crossfading
+    */
+    if (++position_key > UINT8_MAX)
+        position_key = 1;
+
+    if (type == TRACK_CHANGE_END_OF_DATA)
     {
-        switch(global_settings.crossfade)
+        /* If end of all data, force playback */
+        if (audio_pcmbuf_may_play())
+            pcmbuf_play_start();
+    }
+#ifdef HAVE_CROSSFADE
+    /* Determine whether this track change needs to crossfaded and how */
+    else if (crossfade_setting != CROSSFADE_ENABLE_OFF &&
+             !pcmbuf_is_crossfade_active() &&
+             pcmbuf_unplayed_bytes() >= DATA_LEVEL(2) &&
+             !low_latency_mode)
+    {
+        switch (crossfade_setting)
         {
-            case CROSSFADE_ENABLE_AUTOSKIP:
-                crossfade = auto_skip;
-                break;
-            case CROSSFADE_ENABLE_MANSKIP:
-                crossfade = !auto_skip;
-                break;
-            case CROSSFADE_ENABLE_SHUFFLE:
-                crossfade = global_settings.playlist_shuffle;
-                break;
-            case CROSSFADE_ENABLE_SHUFFLE_OR_MANSKIP:
-                crossfade = global_settings.playlist_shuffle || !auto_skip;
-                break;
-            case CROSSFADE_ENABLE_ALWAYS:
-                crossfade = true;
-                break;
+        case CROSSFADE_ENABLE_AUTOSKIP:
+            crossfade = auto_skip;
+            break;
+        case CROSSFADE_ENABLE_MANSKIP:
+            crossfade = !auto_skip;
+            break;
+        case CROSSFADE_ENABLE_SHUFFLE:
+            crossfade = global_settings.playlist_shuffle;
+            break;
+        case CROSSFADE_ENABLE_SHUFFLE_OR_MANSKIP:
+            crossfade = global_settings.playlist_shuffle || !auto_skip;
+            break;
+        case CROSSFADE_ENABLE_ALWAYS:
+            crossfade = true;
+            break;
         }
     }
-#endif
+    /* else crossfade is off, crossfade is already active, not enough data,
+     * pcm is off now (implying low data), not crossfading or low latency mode
+     */
 
-    if (!auto_skip || crossfade)
-    /* manual skip or crossfade */
+    if (crossfade)
     {
-        if (crossfade)
-            { logf("  crossfade track change"); }
-        else
-            { logf("  manual track change"); }
+        logf("crossfade track change");
 
-        pcm_play_lock();
+        /* Don't enable mix mode when skipping tracks manually */
+        crossfade_mixmode = auto_skip &&
+            global_settings.crossfade_fade_out_mixmode;
 
-        /* Cancel any pending automatic gapless transition */
-        pcmbuf_monitor_track_change(false);
+        crossfade_auto_skip = auto_skip;
 
-        /* Can't do two crossfades at once and, no fade if pcm is off now */
-        if (
-#ifdef HAVE_CROSSFADE
-            pcmbuf_is_crossfade_active() ||
-#endif
-            mixer_channel_status(PCM_MIXER_CHAN_PLAYBACK) == CHANNEL_STOPPED)
-        {
-            pcmbuf_play_stop();
-            pcm_play_unlock();
-            /* Notify playback that the track change starts now */
-            return true;
-        }
+        crossfade_status = CROSSFADE_TRACK_CHANGE_STARTED;
 
-        /* Not enough data, or not crossfading, flush the old data instead */
-        if (LOW_DATA(2) || !crossfade || low_latency_mode)
-        {
-            commit_chunk(true);
-        }
-#ifdef HAVE_CROSSFADE
-        else
-        {
-            /* Don't enable mix mode when skipping tracks manually. */
-            crossfade_mixmode = auto_skip &&
-                global_settings.crossfade_fade_out_mixmode;
-
-            crossfade_auto_skip = auto_skip;
-
-            crossfade_track_change_started = crossfade;
-        }
-#endif
-        pcm_play_unlock();
-
-        /* Keep trigger outside the play lock or HW FIFO underruns can happen
-           since frequency scaling is *not* always fast */
         trigger_cpu_boost();
 
-        /* Notify playback that the track change starts now */
-        return true;
+        /* Cancel any pending automatic gapless transition and if a manual
+           skip, stop position updates */
+        pcm_play_lock();
+        pcmbuf_cancel_track_change(!auto_skip);
+        pcm_play_unlock();
     }
-    else    /* automatic and not crossfading, so do gapless track change */
+    else
+#endif /* HAVE_CROSSFADE */
+    if (auto_skip)
     {
         /* The codec is moving on to the next track, but the current track will
-         * continue to play.  Set a flag to make sure the elapsed time of the
-         * current track will be updated properly, and mark the current chunk
-         * as the last one in the track. */
-        logf("  gapless track change");
+         * continue to play, so mark the last write chunk as the last one in
+         * the track */
+        logf("gapless track change");
+#ifdef HAVE_CROSSFADE
+        if (crossfade_status != CROSSFADE_INACTIVE)
+        {
+            /* Crossfade is still active but crossfade is not happening - for
+             * now, chicken-out and clear out the buffer (just like before) to
+             * avoid fade pile-up on short tracks fading-in over long ones */
+            pcmbuf_play_stop();
+        }
+#endif
         pcmbuf_monitor_track_change(true);
-        return false;
+    }
+    else
+    {
+        /* Discard old data; caller needs no transition notification */
+        logf("manual track change");
+        pcmbuf_play_stop();
     }
 }
 
 
 /** Playback */
 
-/* PCM driver callback
- * This function has 3 major logical parts (separated by brackets both for
- * readability and variable scoping).  The first part performs the
- * operations related to finishing off the last chunk we fed to the DMA.
- * The second part detects the end of playlist condition when the PCM
- * buffer is empty except for uncommitted samples.  Then they are committed
- * and sent to the PCM driver for playback.  The third part performs the
- * operations involved in sending a new chunk to the DMA. */
-static void pcmbuf_pcm_callback(unsigned char** start, size_t* size)
+/* PCM driver callback */
+static void pcmbuf_pcm_callback(unsigned char **start, size_t *size)
 {
-    struct chunkdesc *pcmbuf_current = read_chunk;
+    /*- Process the chunk that just finished -*/
+    size_t index = chunk_ridx;
+    struct chunkdesc *desc = current_desc;
 
+    if (desc)
     {
-        /* Take the finished chunk out of circulation */
-        read_chunk = pcmbuf_current->link;
-
-        /* if during a track transition, update the elapsed time in ms */
-        if (track_transition)
-            audio_pcmbuf_position_callback(last_chunksize * 1000 / BYTERATE);
-
-        /* if last chunk in the track, stop updates and notify audio thread */
-        if (pcmbuf_current->end_of_track)
-        {
-            track_transition = false;
+        /* If last chunk in the track, notify of track change */
+        if (desc->is_end != 0)
             audio_pcmbuf_track_change(true);
-        }
 
-        /* Put the finished chunk back into circulation */
-        write_end_chunk->link = pcmbuf_current;
-        write_end_chunk = pcmbuf_current;
-
-#ifdef HAVE_CROSSFADE
-        /* If we've read over the crossfade chunk while it's still fading */
-        if (pcmbuf_current == crossfade_chunk)
-            crossfade_chunk = read_chunk;
-#endif
+        /* Free it for reuse */
+        chunk_ridx = index = index_next(index);
     }
 
+    /*- Process the new one -*/
+    if (index != chunk_widx && !fade_out_complete)
     {
-        /* Commit last samples at end of playlist */
-        if (pcmbuffer_fillpos && !pcmbuf_current)
+        current_desc = desc = index_chunkdesc(index);
+
+        *start = index_buffer(index);
+        *size = desc->size;
+
+        if (desc->pos_key != 0)
         {
-            logf("pcmbuf_pcm_callback: commit last samples");
-            commit_chunk(false);
+            /* Positioning chunk - notify playback */
+            audio_pcmbuf_position_callback(desc->elapsed, desc->offset,
+                                           desc->pos_key);
         }
     }
-
-    /* Stop at this frame */
-    pcmbuf_current = fade_out_complete ? NULL : read_chunk;
-
-    {
-        /* Send the new chunk to the DMA */
-        if(pcmbuf_current)
-        {
-            last_chunksize = pcmbuf_current->size;
-            pcmbuf_unplayed_bytes -= last_chunksize;
-            *size = last_chunksize;
-            *start = pcmbuf_current->addr;
-        }
-        else
-        {
-            /* No more chunks or pause indicated */
-            logf("pcmbuf_pcm_callback: no more chunks");
-            last_chunksize = 0;
-            *size = 0;
-            *start = NULL;
-        }
-    }
-    DISPLAY_DESC("callback");
 }
 
 /* Force playback */
 void pcmbuf_play_start(void)
 {
+    logf("pcmbuf_play_start");
+
     if (mixer_channel_status(PCM_MIXER_CHAN_PLAYBACK) == CHANNEL_STOPPED &&
-        pcmbuf_unplayed_bytes && read_chunk != NULL)
+        chunk_widx != chunk_ridx)
     {
-        logf("pcmbuf_play_start");
-        last_chunksize = read_chunk->size;
-        pcmbuf_unplayed_bytes -= last_chunksize;
+        current_desc = NULL;
         mixer_channel_play_data(PCM_MIXER_CHAN_PLAYBACK, pcmbuf_pcm_callback,
-                                read_chunk->addr, last_chunksize);
+                                NULL, 0);
     }
 }
 
+/* Stop channel, empty and reset buffer */
 void pcmbuf_play_stop(void)
 {
     logf("pcmbuf_play_stop");
+
+    /* Reset channel */
     mixer_channel_stop(PCM_MIXER_CHAN_PLAYBACK);
 
-    pcmbuf_unplayed_bytes = 0;
-    if (read_chunk) {
-        write_end_chunk->link = read_chunk;
-        write_end_chunk = read_end_chunk;
-        read_chunk = read_end_chunk = NULL;
-    }
-    last_chunksize = 0;
-    pcmbuffer_pos = 0;
-    pcmbuffer_fillpos = 0;
+    /* Reset buffer */
+    init_buffer_state();
+
+    /* Revert to position updates by PCM */
+    pcmbuf_sync_position = false;
+
 #ifdef HAVE_CROSSFADE
-    crossfade_track_change_started = false;
-    crossfade_active = false;
+    crossfade_status = CROSSFADE_INACTIVE;
 #endif
-    track_transition = false;
-    flush_pcmbuf = false;
-    DISPLAY_DESC("play_stop");
 
     /* Can unboost the codec thread here no matter who's calling,
      * pretend full pcm buffer to unboost */
@@ -750,74 +773,153 @@
 
 /** Crossfade */
 
+#ifdef HAVE_CROSSFADE
+/* Find the buffer index that's 'size' bytes away from 'index' */
+static size_t crossfade_find_index(size_t index, size_t size)
+{
+    if (index != INVALID_BUF_INDEX)
+    {
+        size_t i = ALIGN_DOWN(index, PCMBUF_CHUNK_SIZE);
+        size += index - i;
+
+        while (i != chunk_widx)
+        {
+            size_t desc_size = index_chunkdesc(i)->size;
+ 
+            if (size < desc_size)
+                return i + size;
+
+            size -= desc_size;
+            i = index_next(i);
+        }
+    }
+
+    return INVALID_BUF_INDEX;
+}
+
+/* Align the needed buffer area up to the end of existing data */
+static size_t crossfade_find_buftail(size_t buffer_rem, size_t buffer_need)
+{
+    crossfade_index = chunk_ridx;
+
+    if (buffer_rem > buffer_need)
+    {
+        size_t distance;
+
+        if (crossfade_auto_skip)
+        {
+            /* Automatic track changes only modify the last part of the buffer,
+             * so find the right chunk and sample to start the crossfade */
+            distance = buffer_rem - buffer_need;
+            buffer_rem = buffer_need;
+        }
+        else
+        {
+            /* Manual skips occur immediately, but give 1/5s to process */
+            distance = BYTERATE / 5;
+            buffer_rem -= BYTERATE / 5;
+        }
+
+        crossfade_index = crossfade_find_index(crossfade_index, distance);
+    }
+
+    return buffer_rem;
+}
+
 /* Clip sample to signed 16 bit range */
-static inline int32_t clip_sample_16(int32_t sample)
+static FORCE_INLINE int32_t clip_sample_16(int32_t sample)
 {
     if ((int16_t)sample != sample)
         sample = 0x7fff ^ (sample >> 31);
     return sample;
 }
 
-#ifdef HAVE_CROSSFADE
-/* Find the chunk that's (length) deep in the list. Return the position within
- * the chunk, and leave the chunkdesc pointer pointing to the chunk. */
-static size_t find_chunk(size_t length, struct chunkdesc **chunk)
-{
-    while (*chunk && length >= (*chunk)->size)
-    {
-        length -= (*chunk)->size;
-        *chunk = (*chunk)->link;
-    }
-    return length;
-}
-
 /* Returns the number of bytes _NOT_ mixed/faded */
-static size_t crossfade_mix_fade(int factor, size_t length, const char *buf,
-                                 size_t *out_sample, struct chunkdesc **out_chunk)
+static int crossfade_mix_fade(int factor, size_t size, void *buf, size_t *out_index,
+                              unsigned long elapsed, off_t offset)
 {
-    if (length == 0)
+    if (size == 0)
         return 0;
 
-    const int16_t *input_buf = (const int16_t *)buf;
-    int16_t *output_buf = (int16_t *)((*out_chunk)->addr);
-    int16_t *chunk_end = SKIPBYTES(output_buf, (*out_chunk)->size);
-    output_buf = &output_buf[*out_sample];
-    int32_t sample;
+    size_t index = *out_index;
 
-    while (length)
+    if (index == INVALID_BUF_INDEX)
+        return size;
+
+    const int16_t *input_buf = buf;
+    int16_t *output_buf = (int16_t *)index_buffer(index);
+
+    while (size)
     {
-        /* fade left and right channel at once to keep buffer alignment */
-        int i;
-        for (i = 0; i < 2; i++)
+        struct chunkdesc *desc = index_chunkdesc(index);
+
+        switch (offset)
         {
-            if (input_buf)
-            /* fade the input buffer and mix into the chunk */
-            {
-                sample = *input_buf++;
-                sample = ((sample * factor) >> 8) + *output_buf;
-                *output_buf++ = clip_sample_16(sample);
-            }
-            else
-            /* fade the chunk only */
-            {
-                sample = *output_buf;
-                *output_buf++ = (sample * factor) >> 8;
-            }
+        case MIXFADE_NULLIFY_POS:
+            /* Stop position updates for the chunk */
+            desc->pos_key = 0;
+            break;
+        case MIXFADE_KEEP_POS:
+            /* Keep position info as it is */
+            break;
+        default:
+            /* Replace position info */
+            stamp_chunk(desc, elapsed, offset);
         }
 
-        length -= 4; /* 2 samples, each 16 bit -> 4 bytes */
+        size_t rem = desc->size - (index % PCMBUF_CHUNK_SIZE);
+        int16_t *chunk_end = SKIPBYTES(output_buf, rem);
+
+        if (size < rem)
+            rem = size;
+
+        size -= rem;
+
+        do
+        {
+            /* fade left and right channel at once to keep buffer alignment */
+            int32_t left = output_buf[0];
+            int32_t right = output_buf[1];
+
+            if (input_buf)
+            {
+                /* fade the input buffer and mix into the chunk */
+                left += *input_buf++ * factor >> 8;
+                right += *input_buf++ * factor >> 8;
+                left = clip_sample_16(left);
+                right = clip_sample_16(right);
+            }
+            else
+            {
+                /* fade the chunk only */
+                left = left * factor >> 8;
+                right = right * factor >> 8;
+            }
+
+            *output_buf++ = left;
+            *output_buf++ = right;
+
+            rem -= 4;
+        }
+        while (rem);
 
         /* move to next chunk as needed */
         if (output_buf >= chunk_end)
         {
-            *out_chunk = (*out_chunk)->link;
-            if (!(*out_chunk))
-                return length;
-            output_buf = (int16_t *)((*out_chunk)->addr);
-            chunk_end = SKIPBYTES(output_buf, (*out_chunk)->size);
+            index = index_next(index);
+
+            if (index == chunk_widx)
+            {
+                /* End of existing data */
+                *out_index = INVALID_BUF_INDEX;
+                return size;
+            }
+
+            output_buf = (int16_t *)index_buffer(index);
         }
     }
-    *out_sample = output_buf - (int16_t *)((*out_chunk)->addr);
+
+    *out_index = buffer_index(output_buf);
     return 0;
 }
 
@@ -825,184 +927,209 @@
  * fade-out with the PCM buffer.  */
 static void crossfade_start(void)
 {
-    size_t crossfade_rem;
-    size_t crossfade_need;
-    size_t fade_out_rem;
-    size_t fade_out_delay;
-    size_t fade_in_delay;
-
-    crossfade_track_change_started = false;
-    /* Reject crossfade if less than .5s of data */
-    if (LOW_DATA(2)) {
-        logf("crossfade rejected");
-        pcmbuf_play_stop();
-        return ;
-    }
-
     logf("crossfade_start");
-    commit_chunk(false);
-    crossfade_active = true;
+
+    pcm_play_lock();
 
     /* Initialize the crossfade buffer size to all of the buffered data that
      * has not yet been sent to the DMA */
-    crossfade_rem = pcmbuf_unplayed_bytes;
-    crossfade_chunk = read_chunk->link;
-    crossfade_sample = 0;
+    size_t unplayed = pcmbuf_unplayed_bytes();
 
-    /* Get fade out info from settings. */
-    fade_out_delay = global_settings.crossfade_fade_out_delay * BYTERATE;
-    fade_out_rem = global_settings.crossfade_fade_out_duration * BYTERATE;
-
-    crossfade_need = fade_out_delay + fade_out_rem;
-    if (crossfade_rem > crossfade_need)
+    /* Reject crossfade if less than .5s of data */
+    if (unplayed < DATA_LEVEL(2))
     {
+        logf("crossfade rejected");
+
+        crossfade_status = CROSSFADE_INACTIVE;
+
         if (crossfade_auto_skip)
-        /* Automatic track changes only modify the last part of the buffer,
-         * so find the right chunk and sample to start the crossfade */
-        {
-            crossfade_sample = find_chunk(crossfade_rem - crossfade_need,
-                &crossfade_chunk) / 2;
-            crossfade_rem = crossfade_need;
-        }
-        else
-        /* Manual skips occur immediately, but give time to process */
-        {
-            crossfade_rem -= crossfade_chunk->size;
-            crossfade_chunk = crossfade_chunk->link;
-        }
-    }
-    /* Truncate fade out duration if necessary. */
-    if (crossfade_rem < crossfade_need)
-    {
-        size_t crossfade_short = crossfade_need - crossfade_rem;
-        if (fade_out_rem >= crossfade_short)
-            fade_out_rem -= crossfade_short;
-        else
-        {
-            fade_out_delay -= crossfade_short - fade_out_rem;
-            fade_out_rem = 0;
-        }
-    }
-    crossfade_rem -= fade_out_delay + fade_out_rem;
+            pcmbuf_monitor_track_change(true);
 
-    /* Completely process the crossfade fade-out effect with current PCM buffer */
+        pcm_play_unlock();
+        return;
+    }
+
+    /* Fading will happen */
+    crossfade_status = CROSSFADE_ACTIVE;
+
+    /* Get fade info from settings. */
+    size_t fade_out_delay = global_settings.crossfade_fade_out_delay * BYTERATE;
+    size_t fade_out_rem = global_settings.crossfade_fade_out_duration * BYTERATE;
+    size_t fade_in_delay = global_settings.crossfade_fade_in_delay * BYTERATE;
+    size_t fade_in_duration = global_settings.crossfade_fade_in_duration * BYTERATE;
+
+    if (!crossfade_auto_skip)
+    {
+        /* Forego fade-in delay on manual skip - do the best to preserve auto skip
+           relationship */
+        if (fade_out_delay > fade_in_delay)
+            fade_out_delay -= fade_in_delay;
+        else
+            fade_out_delay = 0;
+
+        fade_in_delay = 0;
+    }
+
+    size_t fade_out_need = fade_out_delay + fade_out_rem;
+
     if (!crossfade_mixmode)
     {
+        size_t buffer_rem = crossfade_find_buftail(unplayed, fade_out_need);
+
+        pcm_play_unlock();
+
+        if (buffer_rem < fade_out_need)
+        {
+            /* Existing buffers are short */
+            size_t fade_out_short = fade_out_need - buffer_rem;
+
+            if (fade_out_rem >= fade_out_short)
+            {
+                /* Truncate fade-out duration */
+                fade_out_rem -= fade_out_short;
+            }
+            else
+            {
+                /* Truncate fade-out and fade-out delay */
+                fade_out_delay = fade_out_rem;
+                fade_out_rem = 0;
+            }
+        }
+
+        /* Completely process the crossfade fade-out effect with current PCM buffer */
+
         /* Fade out the specified amount of the already processed audio */
-        size_t total_fade_out = fade_out_rem;
-        size_t fade_out_sample;
-        struct chunkdesc *fade_out_chunk = crossfade_chunk;
+        size_t fade_out_total = fade_out_rem;
 
         /* Find the right chunk and sample to start fading out */
-        fade_out_delay += crossfade_sample * 2;
-        fade_out_sample = find_chunk(fade_out_delay, &fade_out_chunk) / 2;
+        size_t fade_out_index = crossfade_find_index(crossfade_index, fade_out_delay);
 
         while (fade_out_rem > 0)
         {
-            /* Each 1/10 second of audio will have the same fade applied */
-            size_t block_rem = MIN(BYTERATE / 10, fade_out_rem);
-            int factor = (fade_out_rem << 8) / total_fade_out;
+            /* Each 1/20 second of audio will have the same fade applied */
+            size_t block_rem = MIN(BYTERATE / 20, fade_out_rem);
+            int factor = (fade_out_rem << 8) / fade_out_total;
 
             fade_out_rem -= block_rem;
 
-            crossfade_mix_fade(factor, block_rem, NULL,
-                &fade_out_sample, &fade_out_chunk);
+            crossfade_mix_fade(factor, block_rem, NULL, &fade_out_index,
+                               0, MIXFADE_KEEP_POS);
         }
 
         /* zero out the rest of the buffer */
-        crossfade_mix_fade(0, crossfade_rem, NULL,
-            &fade_out_sample, &fade_out_chunk);
+        crossfade_mix_fade(0, INT_MAX, NULL, &fade_out_index,
+                           0, MIXFADE_NULLIFY_POS);
+
+        pcm_play_lock();
     }
 
     /* Initialize fade-in counters */
-    crossfade_fade_in_total = global_settings.crossfade_fade_in_duration * BYTERATE;
-    crossfade_fade_in_rem = crossfade_fade_in_total;
+    crossfade_fade_in_total = fade_in_duration;
+    crossfade_fade_in_rem = fade_in_duration;
 
-    fade_in_delay = global_settings.crossfade_fade_in_delay * BYTERATE;
+    /* Find the right chunk and sample to start fading in - redo from read
+       chunk in case original position were/was overrun in callback - the
+       track change event _must not_ ever fail to happen */
+    unplayed = pcmbuf_unplayed_bytes() + fade_in_delay;
 
-    /* Find the right chunk and sample to start fading in */
-    fade_in_delay += crossfade_sample * 2;
-    crossfade_sample = find_chunk(fade_in_delay, &crossfade_chunk) / 2;
+    crossfade_find_buftail(unplayed, fade_out_need);
+
+    if (crossfade_auto_skip)
+        pcmbuf_monitor_track_change_ex(crossfade_index, 0);
+
+    pcm_play_unlock();
+
     logf("crossfade_start done!");
 }
 
 /* Perform fade-in of new track */
-static void write_to_crossfade(size_t length)
+static void write_to_crossfade(size_t size, unsigned long elapsed, off_t offset)
 {
-    if (length)
+    unsigned char *buf = crossfade_buffer;
+
+    if (crossfade_fade_in_rem)
     {
-        char *buf = fadebuf;
-        if (crossfade_fade_in_rem)
-        {
-            size_t samples;
-            int16_t *input_buf;
+        /* Fade factor for this packet */
+        int factor =
+            ((crossfade_fade_in_total - crossfade_fade_in_rem) << 8) /
+            crossfade_fade_in_total;
+        /* Bytes to fade */
+        size_t fade_rem = MIN(size, crossfade_fade_in_rem);
 
-            /* Fade factor for this packet */
-            int factor =
-                ((crossfade_fade_in_total - crossfade_fade_in_rem) << 8) /
-                crossfade_fade_in_total;
-            /* Bytes to fade */
-            size_t fade_rem = MIN(length, crossfade_fade_in_rem);
+        /* We _will_ fade this many bytes */
+        crossfade_fade_in_rem -= fade_rem;
 
-            /* We _will_ fade this many bytes */
-            crossfade_fade_in_rem -= fade_rem;
-
-            if (crossfade_chunk)
-            {
-                /* Mix the data */
-                size_t fade_total = fade_rem;
-                fade_rem = crossfade_mix_fade(factor, fade_rem, buf,
-                    &crossfade_sample, &crossfade_chunk);
-                length -= fade_total - fade_rem;
-                buf += fade_total - fade_rem;
-                if (!length)
-                    return;
-            }
-
-            samples = fade_rem / 2;
-            input_buf = (int16_t *)buf;
-            /* Fade remaining samples in place */
-            while (samples--)
-            {
-                int32_t sample = *input_buf;
-                *input_buf++ = (sample * factor) >> 8;
-            }
-        }
-
-        if (crossfade_chunk)
+        if (crossfade_index != INVALID_BUF_INDEX)
         {
             /* Mix the data */
-            size_t mix_total = length;
-            /* A factor of 256 means mix only, no fading */
-            length = crossfade_mix_fade(256, length, buf,
-                    &crossfade_sample, &crossfade_chunk);
-            buf += mix_total - length;
-            if (!length)
+            size_t fade_total = fade_rem;
+            fade_rem = crossfade_mix_fade(factor, fade_rem, buf, &crossfade_index,
+                                          elapsed, offset);
+            fade_total -= fade_rem;
+            size -= fade_total;
+            buf += fade_total;
+
+            if (!size)
                 return;
         }
 
-        while (length > 0)
+        /* Fade remaining samples in place */
+        int samples = fade_rem / 4;
+        int16_t *input_buf = (int16_t *)buf;
+
+        while (samples--)
         {
-            COMMIT_IF_NEEDED;
-            size_t pcmbuffer_index = pcmbuffer_pos + pcmbuffer_fillpos;
-            size_t copy_n = MIN(length, pcmbuf_size - pcmbuffer_index);
-            memcpy(&pcmbuffer[pcmbuffer_index], buf, copy_n);
-            buf += copy_n;
-            pcmbuffer_fillpos += copy_n;
-            length -= copy_n;
+            int32_t left = input_buf[0];
+            int32_t right = input_buf[1];
+            *input_buf++ = left * factor >> 8;
+            *input_buf++ = right * factor >> 8;
         }
     }
+
+    if (crossfade_index != INVALID_BUF_INDEX)
+    {
+        /* Mix the data */
+        size_t mix_total = size;
+
+        /* A factor of 256 means mix only, no fading */
+        size = crossfade_mix_fade(256, size, buf, &crossfade_index,
+                                  elapsed, offset);
+        buf += mix_total - size;
+
+        if (!size)
+            return;
+    }
+
+    /* Data might remain in the fade buffer yet the fade-in has run its
+       course - finish it off as normal chunks */
+    while (size > 0)
+    {
+        size_t copy_n = size;
+        unsigned char *outbuf = get_write_buffer(&copy_n);
+        memcpy(outbuf, buf, copy_n);
+        commit_write_buffer(copy_n, elapsed, offset);
+        buf += copy_n;
+        size -= copy_n;
+    }
+
     /* if no more fading-in to do, stop the crossfade */
-    if (!(crossfade_fade_in_rem || crossfade_chunk))
-        crossfade_active = false;
+#if 0
+    /* This way (the previous way) can cause a sudden volume jump if mixable
+       data is used up before the fade-in completes and that just sounds wrong
+            -- jethead71 */
+    if (!crossfade_fade_in_rem || crossfade_index == INVALID_BUF_INDEX)
+#endif
+    /* Let fade-in complete even if not fully overlapping the existing data */
+    if (!crossfade_fade_in_rem)
+        crossfade_status = CROSSFADE_INACTIVE;
 }
 
 static void pcmbuf_finish_crossfade_enable(void)
 {
     /* Copy the pending setting over now */
-    crossfade_enabled = crossfade_enable_request;
+    crossfade_setting = crossfade_enable_request;
 
-    pcmbuf_watermark = (crossfade_enabled && pcmbuf_size) ?
+    pcmbuf_watermark = (crossfade_setting != CROSSFADE_ENABLE_OFF && pcmbuf_size) ?
         /* If crossfading, try to keep the buffer full other than 1 second */
         (pcmbuf_size - BYTERATE) :
         /* Otherwise, just use the default */
@@ -1011,20 +1138,20 @@
 
 bool pcmbuf_is_crossfade_active(void)
 {
-    return crossfade_active || crossfade_track_change_started;
+    return crossfade_status != CROSSFADE_INACTIVE;
 }
 
-void pcmbuf_request_crossfade_enable(bool on_off)
+void pcmbuf_request_crossfade_enable(int setting)
 {
     /* Next setting to be used, not applied now */
-    crossfade_enable_request = on_off;
+    crossfade_enable_request = setting;
 }
 
 bool pcmbuf_is_same_size(void)
 {
-    /* if pcmbuffer is NULL, then not set up yet even once so always */
-    bool same_size = pcmbuffer ?
-        (get_next_required_pcmbuf_size() == pcmbuf_size) : true;
+    /* if pcmbuf_buffer is NULL, then not set up yet even once so always */
+    bool same_size = pcmbuf_buffer ?
+        (get_next_required_pcmbuf_chunks() == pcmbuf_desc_count) : true;
 
     /* no buffer change needed, so finish crossfade setup now */
     if (same_size)
@@ -1037,50 +1164,30 @@
 
 /** Debug menu, other metrics */
 
-/* Amount of bytes left in the buffer. */
+/* Amount of bytes left in the buffer, accounting for uncommitted bytes */
 size_t pcmbuf_free(void)
 {
-    if (read_chunk != NULL)
-    {
-        void *read = (void *)read_chunk->addr;
-        void *write = &pcmbuffer[pcmbuffer_pos + pcmbuffer_fillpos];
-        if (read < write)
-            return (size_t)(read - write) + pcmbuf_size;
-        else
-            return (size_t) (read - write);
-    }
-    return pcmbuf_size - pcmbuffer_fillpos;
+    return pcmbuf_size - pcmbuf_unplayed_bytes() - pcmbuf_bytes_waiting;
 }
 
+/* Data bytes allocated for buffer */
 size_t pcmbuf_get_bufsize(void)
 {
     return pcmbuf_size;
 }
 
+/* Number of committed descriptors */
 int pcmbuf_used_descs(void)
 {
-    struct chunkdesc *temp = read_chunk;
-    unsigned int i = 0;
-    while (temp) {
-        temp = temp->link;
-        i++;
-    }
-    return i;
+    return pcmbuf_unplayed_bytes() / PCMBUF_CHUNK_SIZE;
 }
 
+/* Total number of descriptors allocated */
 int pcmbuf_descs(void)
 {
-    return NUM_CHUNK_DESCS(pcmbuf_size);
+    return pcmbuf_desc_count;
 }
 
-#ifdef ROCKBOX_HAS_LOGF
-unsigned char *pcmbuf_get_meminfo(size_t *length)
-{
-    *length = pcmbuf_bufend - pcmbuffer;
-    return pcmbuffer;
-}
-#endif
-
 
 /** Fading and channel volume control */
 
@@ -1112,7 +1219,6 @@
     {
         /* Fade is complete */
         tick_remove_task(pcmbuf_fade_tick);
-
         if (fade_state == PCM_FADING_OUT)
         {
             /* Tell PCM to stop at its earliest convenience */
@@ -1177,19 +1283,15 @@
 
 bool pcmbuf_is_lowdata(void)
 {
-    if (!pcm_is_playing() || pcm_is_paused()
-#ifdef HAVE_CROSSFADE
-        || pcmbuf_is_crossfade_active()
-#endif
-        )
+    if (!audio_pcmbuf_may_play() || pcmbuf_is_crossfade_active())
         return false;
 
 #if MEMORYSIZE > 2
     /* 1 seconds of buffer is low data */
-    return LOW_DATA(4);
+    return pcmbuf_unplayed_bytes() < DATA_LEVEL(4);
 #else
     /* under watermark is low data */
-    return (pcmbuf_unplayed_bytes < pcmbuf_watermark);
+    return pcmbuf_unplayed_bytes() < pcmbuf_watermark;
 #endif
 }
 
@@ -1198,8 +1300,15 @@
     low_latency_mode = state;
 }
 
-unsigned long pcmbuf_get_latency(void)
+/* Return the current position key value */
+unsigned int pcmbuf_get_position_key(void)
 {
-    return (pcmbuf_unplayed_bytes +
-        mixer_channel_get_bytes_waiting(PCM_MIXER_CHAN_PLAYBACK)) * 1000 / BYTERATE;
+    return position_key;
+}
+
+/* Set position updates to be synchronous and immediate in addition to during
+   PCM frames - cancelled upon first codec insert or upon stopping */
+void pcmbuf_sync_position_update(void)
+{
+    pcmbuf_sync_position = true;
 }
diff --git a/apps/pcmbuf.h b/apps/pcmbuf.h
index a5cd316..3261b20 100644
--- a/apps/pcmbuf.h
+++ b/apps/pcmbuf.h
@@ -21,9 +21,11 @@
 #ifndef PCMBUF_H
 #define PCMBUF_H
 
+#include <sys/types.h>
+
 /* Commit PCM data */
 void *pcmbuf_request_buffer(int *count);
-void pcmbuf_write_complete(int count);
+void pcmbuf_write_complete(int count, unsigned long elapsed, off_t offset);
 
 /* Init */
 size_t pcmbuf_init(unsigned char *bufend);
@@ -33,20 +35,30 @@
 void pcmbuf_play_stop(void);
 void pcmbuf_pause(bool pause);
 void pcmbuf_monitor_track_change(bool monitor);
-bool pcmbuf_start_track_change(bool manual_skip);
+void pcmbuf_sync_position_update(void);
+
+/* Track change origin type */
+enum pcm_track_change_type
+{
+    TRACK_CHANGE_NONE = 0,     /* No track change pending */
+    TRACK_CHANGE_MANUAL,       /* Manual change (from user) */
+    TRACK_CHANGE_AUTO,         /* Automatic change (from codec) */
+    TRACK_CHANGE_END_OF_DATA,  /* Expect no more data (from codec) */
+};
+void pcmbuf_start_track_change(enum pcm_track_change_type type);
 
 /* Crossfade */
 #ifdef HAVE_CROSSFADE
 bool pcmbuf_is_crossfade_active(void);
-void pcmbuf_request_crossfade_enable(bool on_off);
+void pcmbuf_request_crossfade_enable(int setting);
 bool pcmbuf_is_same_size(void);
 #else
 /* Dummy functions with sensible returns */
-static inline bool pcmbuf_is_crossfade_active(void)
+static FORCE_INLINE bool pcmbuf_is_crossfade_active(void)
     { return false; }
-static inline void pcmbuf_request_crossfade_enable(bool on_off)
+static FORCE_INLINE void pcmbuf_request_crossfade_enable(bool on_off)
     { return; (void)on_off; }
-static inline bool pcmbuf_is_same_size(void)
+static FORCE_INLINE bool pcmbuf_is_same_size(void)
     { return true; }
 #endif
 
@@ -59,9 +71,7 @@
 size_t pcmbuf_get_bufsize(void);
 int pcmbuf_descs(void);
 int pcmbuf_used_descs(void);
-#ifdef ROCKBOX_HAS_LOGF
-unsigned char *pcmbuf_get_meminfo(size_t *length);
-#endif
+unsigned int pcmbuf_get_position_key(void);
 
 /* Misc */
 void pcmbuf_fade(bool fade, bool in);
@@ -69,6 +79,5 @@
 void pcmbuf_soft_mode(bool shhh);
 bool pcmbuf_is_lowdata(void);
 void pcmbuf_set_low_latency(bool state);
-unsigned long pcmbuf_get_latency(void);
 
 #endif
diff --git a/apps/playback.c b/apps/playback.c
index 6578341..7dad086 100644
--- a/apps/playback.c
+++ b/apps/playback.c
@@ -330,7 +330,7 @@
 static bool codec_skip_pending = false;
 static int  codec_skip_status;
 static bool codec_seeking = false;          /* Codec seeking ack expected? */
-
+static unsigned int position_key = 0;
 
 /* Event queues */
 static struct event_queue audio_queue SHAREDBSS_ATTR;
@@ -353,14 +353,13 @@
 static void buffer_event_buffer_low_callback(void *data);
 static void buffer_event_rebuffer_callback(void *data);
 static void buffer_event_finished_callback(void *data);
+void audio_pcmbuf_sync_position(void);
 
 
 /**************************************/
 
 /** --- audio_queue helpers --- **/
-
-/* codec thread needs access */
-void audio_queue_post(long id, intptr_t data)
+static void audio_queue_post(long id, intptr_t data)
 {
     queue_post(&audio_queue, id, data);
 }
@@ -805,14 +804,10 @@
        aids viewing and the summation of certain variables should add up to
        the location of others. */
     {
-        size_t pcmbufsize;
-        const unsigned char *pcmbuf = pcmbuf_get_meminfo(&pcmbufsize);
         logf("fbuf:   %08X", (unsigned)filebuf);
         logf("fbufe:  %08X", (unsigned)(filebuf + filebuflen));
         logf("sbuf:   %08X", (unsigned)audio_scratch_memory);
         logf("sbufe:  %08X", (unsigned)(audio_scratch_memory + allocsize));
-        logf("pcmb:   %08X", (unsigned)pcmbuf);
-        logf("pcmbe:  %08X", (unsigned)(pcmbuf + pcmbufsize));
     }
 #endif
 
@@ -978,7 +973,8 @@
 /* Announce the end of playing the current track */
 static void audio_playlist_track_finish(void)
 {
-    struct mp3entry *id3 = valid_mp3entry(id3_get(PLAYING_ID3));
+    struct mp3entry *ply_id3 = id3_get(PLAYING_ID3);
+    struct mp3entry *id3 = valid_mp3entry(ply_id3);
 
     playlist_update_resume_info(filling == STATE_ENDED ? NULL : id3);
 
@@ -1001,6 +997,8 @@
     if (id3)
         send_event(PLAYBACK_EVENT_TRACK_CHANGE, id3);
 
+    position_key = pcmbuf_get_position_key();
+
     playlist_update_resume_info(id3);
 }
 
@@ -1014,26 +1012,28 @@
 
 /* Bring the user current mp3entry up to date and set a new offset for the
    buffered metadata */
-static void playing_id3_sync(struct track_info *user_info, size_t offset)
+static void playing_id3_sync(struct track_info *user_info, off_t offset)
 {
     id3_mutex_lock();
 
     struct mp3entry *id3 = bufgetid3(user_info->id3_hid);
+    struct mp3entry *playing_id3 = id3_get(PLAYING_ID3);
 
-    if (offset == (size_t)-1)
+    pcm_play_lock();
+
+    unsigned long e = playing_id3->elapsed;
+    unsigned long o = playing_id3->offset;
+
+    id3_write(PLAYING_ID3, id3);
+
+    if (offset < 0)
     {
-        struct mp3entry *ply_id3 = id3_get(PLAYING_ID3);
-        size_t play_offset = ply_id3->offset;
-        long play_elapsed = ply_id3->elapsed;
-        id3_write(PLAYING_ID3, id3);
-        ply_id3->offset = play_offset;
-        ply_id3->elapsed = play_elapsed;
+        playing_id3->elapsed = e;
+        playing_id3->offset = o;
         offset = 0;
     }
-    else
-    {
-        id3_write(PLAYING_ID3, id3);
-    }
+
+    pcm_play_unlock();
 
     if (id3)
         id3->offset = offset;
@@ -1093,13 +1093,6 @@
     return retval;
 }
 
-/* Clear the PCM on a manual skip */
-static void audio_clear_paused_pcm(void)
-{
-    if (play_status == PLAY_PAUSED && !pcmbuf_is_crossfade_active())
-        pcmbuf_play_stop();
-}
-
 /* Wait for any in-progress fade to complete */
 static void audio_wait_fade_complete(void)
 {
@@ -1121,6 +1114,7 @@
         {
             /* Clear the buffer */
             pcmbuf_play_stop();
+            audio_pcmbuf_sync_position();
         }
 
         if (play_status != PLAY_PAUSED)
@@ -2063,7 +2057,7 @@
 
 /* Called to make an outstanding track skip the current track and to send the
    transition events */
-static void audio_finalise_track_change(bool delayed)
+static void audio_finalise_track_change(void)
 {
     switch (skip_pending)
     {
@@ -2117,15 +2111,6 @@
 
     id3_write(PLAYING_ID3, track_id3);
 
-    if (delayed)
-    {
-        /* Delayed skip where codec is ahead of user's current track */
-        struct mp3entry *ci_id3 = id3_get(CODEC_ID3);
-        struct mp3entry *ply_id3 = id3_get(PLAYING_ID3);
-        ply_id3->elapsed = ci_id3->elapsed;
-        ply_id3->offset = ci_id3->offset;
-    }
-
     /* The skip is technically over */
     skip_pending = TRACK_SKIP_NONE;
 
@@ -2141,25 +2126,25 @@
 }
 
 /* Actually begin a transition and take care of the codec change - may complete
-   it now or ask pcmbuf for notification depending on the type and what pcmbuf
-   has to say */
-static void audio_begin_track_change(bool auto_skip, int trackstat)
+   it now or ask pcmbuf for notification depending on the type */
+static void audio_begin_track_change(enum pcm_track_change_type type,
+                                     int trackstat)
 {
     /* Even if the new track is bad, the old track must be finished off */
-    bool finalised = pcmbuf_start_track_change(auto_skip);
+    pcmbuf_start_track_change(type);
 
-    if (finalised)
+    bool auto_skip = type != TRACK_CHANGE_MANUAL;
+
+    if (!auto_skip)
     {
-        /* pcmbuf says that the transition happens now - complete it */
-        audio_finalise_track_change(false);
+        /* Manual track change happens now */
+        audio_finalise_track_change();
+        pcmbuf_sync_position_update();
 
         if (play_status == PLAY_STOPPED)
             return; /* Stopped us */
     }
 
-    if (!auto_skip)
-        audio_clear_paused_pcm();
-
     if (trackstat >= LOAD_TRACK_OK)
     {
         struct track_info *info = track_list_current(0);
@@ -2170,7 +2155,7 @@
         /* Everything needed for the codec is ready - start it */
         if (audio_start_codec(auto_skip))
         {
-            if (finalised)
+            if (!auto_skip)
                 playing_id3_sync(info, -1);
             return;
         }
@@ -2186,7 +2171,7 @@
 {
     skip_pending = TRACK_SKIP_AUTO_END_PLAYLIST;
     filling = STATE_ENDING;
-    pcmbuf_monitor_track_change(true);
+    pcmbuf_start_track_change(TRACK_CHANGE_END_OF_DATA);
 }
 
 /* Codec has completed decoding the track
@@ -2221,14 +2206,6 @@
 
     codec_skip_pending = false;
 
-#ifdef AB_REPEAT_ENABLE
-    if (status >= 0)
-    {
-        /* Normal automatic skip */
-        ab_end_of_track_report();
-    }
-#endif
-
     int trackstat = LOAD_TRACK_OK;
 
     automatic_skip = true;
@@ -2263,7 +2240,7 @@
             {
                 /* Continue filling after this track */
                 audio_reset_and_rebuffer(TRACK_LIST_KEEP_CURRENT, 1);
-                audio_begin_track_change(true, trackstat);
+                audio_begin_track_change(TRACK_CHANGE_AUTO, trackstat);
                 return;
             }
             /* else rebuffer at this track; status applies to the track we
@@ -2299,7 +2276,7 @@
         }
     }
 
-    audio_begin_track_change(true, trackstat);
+    audio_begin_track_change(TRACK_CHANGE_AUTO, trackstat);
 }
 
 /* Called when codec completes seek operation
@@ -2316,7 +2293,7 @@
 static void audio_on_track_changed(void)
 {
     /* Finish whatever is pending so that the WPS is in sync */
-    audio_finalise_track_change(true);
+    audio_finalise_track_change();
 
     if (codec_skip_pending)
     {
@@ -2367,8 +2344,7 @@
             track_list_clear(TRACK_LIST_CLEAR_ALL);
 
             /* Indicate manual track change */
-            pcmbuf_start_track_change(false);
-            audio_clear_paused_pcm();
+            pcmbuf_start_track_change(TRACK_CHANGE_MANUAL);
             wipe_track_metadata(true);
         }
 
@@ -2398,6 +2374,10 @@
         play_status = PLAY_PLAYING;
     }
 
+    /* Codec's position should be available as soon as it knows it */
+    position_key = pcmbuf_get_position_key();
+    pcmbuf_sync_position_update();
+
     /* Start fill from beginning of playlist */
     playlist_peek_offset = -1;
     buf_set_base_handle(-1);
@@ -2592,7 +2572,7 @@
         trackstat = audio_reset_and_rebuffer(TRACK_LIST_CLEAR_ALL, -1);
     }
 
-    audio_begin_track_change(false, trackstat);
+    audio_begin_track_change(TRACK_CHANGE_MANUAL, trackstat);
 }
 
 /* Skip to the next/previous directory
@@ -2638,7 +2618,7 @@
         return;
     }
 
-    audio_begin_track_change(false, trackstat);
+    audio_begin_track_change(TRACK_CHANGE_MANUAL, trackstat);
 }
 
 /* Enter seek mode in order to start a seek
@@ -2689,11 +2669,6 @@
         if (time == 0)
             send_event(PLAYBACK_EVENT_TRACK_FINISH, id3);
 
-        /* Prevent user codec time update - coerce to something that is
-           innocuous concerning lookaheads */
-        if (pending == TRACK_SKIP_NONE)
-            skip_pending = TRACK_SKIP_AUTO_END_PLAYLIST;
-
         id3->elapsed = time;
         queue_reply(&audio_queue, 1);
 
@@ -2703,6 +2678,9 @@
            halt that will reset it */
         codec_seeking = true;
 
+        /* If in transition, key will have changed - sync to it */
+        position_key = pcmbuf_get_position_key();
+
         if (pending == TRACK_SKIP_AUTO)
         {
             if (!track_list_advance_current(-1))
@@ -3124,75 +3102,66 @@
 
 /** -- Codec callbacks -- **/
 
-/* Update elapsed times with latency-adjusted values */
-void audio_codec_update_elapsed(unsigned long value)
+/* Update elapsed time for next PCM insert */
+void audio_codec_update_elapsed(unsigned long elapsed)
 {
 #ifdef AB_REPEAT_ENABLE
-    ab_position_report(value);
+    ab_position_report(elapsed);
 #endif
-
-    unsigned long latency = pcmbuf_get_latency();
-
-    if (LIKELY(value >= latency))
-    {
-        unsigned long elapsed = value - latency;
-
-        if (elapsed > value || elapsed < value - 2)
-            value = elapsed;
-    }
-    else
-    {
-        value = 0;
-    }
-
-    /* Track codec: used later when updating the playing at the user
-       transition */
-    id3_get(CODEC_ID3)->elapsed = value;
-
-    /* If a skip is pending, the PCM buffer is updating the time on the
-       previous song */
-    if (LIKELY(skip_pending == TRACK_SKIP_NONE))
-        id3_get(PLAYING_ID3)->elapsed = value;
+    /* Save in codec's id3 where it is used at next pcm insert */
+    id3_get(CODEC_ID3)->elapsed = elapsed;
 }
 
-/* Update offsets with latency-adjusted values */
-void audio_codec_update_offset(size_t value)
+/* Update offset for next PCM insert */
+void audio_codec_update_offset(size_t offset)
 {
-    struct mp3entry *ci_id3 = id3_get(CODEC_ID3);
-    unsigned long latency = pcmbuf_get_latency() * ci_id3->bitrate / 8;
+    /* Save in codec's id3 where it is used at next pcm insert */
+    id3_get(CODEC_ID3)->offset = offset;
+}
 
-    if (LIKELY(value >= latency))
+/* Codec has finished running */
+void audio_codec_complete(int status)
+{
+#ifdef AB_REPEAT_ENABLE
+    if (status >= CODEC_OK)
     {
-        value -= latency;
+        /* Normal automatic skip */
+        ab_end_of_track_report();
     }
-    else
-    {
-        value = 0;
-    }
+#endif
 
-    /* Track codec: used later when updating the playing id3 at the user
-       transition */
-    ci_id3->offset = value;
+    LOGFQUEUE("codec > audio Q_AUDIO_CODEC_COMPLETE: %d", status);
+    audio_queue_post(Q_AUDIO_CODEC_COMPLETE, status);
+}
 
-    /* If a skip is pending, the PCM buffer is updating the time on the
-       previous song */
-    if (LIKELY(skip_pending == TRACK_SKIP_NONE))
-        id3_get(PLAYING_ID3)->offset = value;
+/* Codec has finished seeking */
+void audio_codec_seek_complete(void)
+{
+    LOGFQUEUE("codec > audio Q_AUDIO_CODEC_SEEK_COMPLETE");
+    audio_queue_post(Q_AUDIO_CODEC_SEEK_COMPLETE, 0);
 }
 
 
 /** --- Pcmbuf callbacks --- **/
 
-/* Between the codec and PCM track change, we need to keep updating the
- * "elapsed" value of the previous (to the codec, but current to the
- * user/PCM/WPS) track, so that the progressbar reaches the end. */
-void audio_pcmbuf_position_callback(unsigned int time)
+/* Update the elapsed and offset from the information cached during the
+   PCM buffer insert */
+void audio_pcmbuf_position_callback(unsigned long elapsed, off_t offset,
+                                    unsigned int key)
 {
-    struct mp3entry *id3 = id3_get(PLAYING_ID3);
+    if (key == position_key)
+    {
+        struct mp3entry *id3 = id3_get(PLAYING_ID3);
+        id3->elapsed = elapsed;
+        id3->offset = offset;
+    }
+}
 
-    time += id3->elapsed;
-
-    id3->elapsed = MIN(time, id3->length);
+/* Synchronize position info to the codec's */
+void audio_pcmbuf_sync_position(void)
+{
+    audio_pcmbuf_position_callback(ci.id3->elapsed, ci.id3->offset,
+                                   pcmbuf_get_position_key());
 }
 
 /* Post message from pcmbuf that the end of the previous track has just
diff --git a/firmware/export/system.h b/firmware/export/system.h
index 5128801..1f8ed03 100644
--- a/firmware/export/system.h
+++ b/firmware/export/system.h
@@ -378,12 +378,16 @@
 #if   defined(CPU_ARM)
     /* Use ARMs cache alignment. */
     #define MEM_ALIGN_ATTR CACHEALIGN_ATTR
+    #define MEM_ALIGN_SIZE CACHEALIGN_SIZE
 #elif defined(CPU_COLDFIRE)
     /* Use fixed alignment of 16 bytes. Speed up only for 'movem' in DRAM. */
     #define MEM_ALIGN_ATTR __attribute__((aligned(16)))
+    #define MEM_ALIGN_SIZE 16
 #else
     /* Do nothing. */
     #define MEM_ALIGN_ATTR
+    /* Align pointer size */
+    #define MEM_ALIGN_SIZE sizeof(intptr_t)
 #endif
 
 #ifdef STORAGE_WANTS_ALIGN