5663e1cd0a
git-svn-id: svn://svn.rockbox.org/rockbox/trunk@30130 a1c6a512-1295-4272-9138-f99709370657
388 lines
10 KiB
C
388 lines
10 KiB
C
/***************************************************************************
|
|
* __________ __ ___.
|
|
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
|
|
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
|
|
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
|
|
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
|
|
* \/ \/ \/ \/ \/
|
|
* $Id$
|
|
*
|
|
* PCM output buffer definitions
|
|
*
|
|
* Copyright (c) 2007 Michael Sevakis
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
|
|
* KIND, either express or implied.
|
|
*
|
|
****************************************************************************/
|
|
#include "plugin.h"
|
|
#include "mpegplayer.h"
|
|
|
|
/* PCM channel we're using */
|
|
#define MPEG_PCM_CHANNEL PCM_MIXER_CHAN_PLAYBACK
|
|
|
|
/* Pointers */
|
|
|
|
/* Start of buffer */
|
|
static struct pcm_frame_header * ALIGNED_ATTR(4) pcm_buffer;
|
|
/* End of buffer (not guard) */
|
|
static struct pcm_frame_header * ALIGNED_ATTR(4) pcmbuf_end;
|
|
/* Read pointer */
|
|
static struct pcm_frame_header * ALIGNED_ATTR(4) pcmbuf_head IBSS_ATTR;
|
|
/* Write pointer */
|
|
static struct pcm_frame_header * ALIGNED_ATTR(4) pcmbuf_tail IBSS_ATTR;
|
|
|
|
/* Bytes */
|
|
static ssize_t pcmbuf_curr_size IBSS_ATTR; /* Size of currently playing frame */
|
|
static ssize_t pcmbuf_read IBSS_ATTR; /* Number of bytes read by DMA */
|
|
static ssize_t pcmbuf_written IBSS_ATTR; /* Number of bytes written by source */
|
|
static ssize_t pcmbuf_threshold IBSS_ATTR; /* Non-silence threshold */
|
|
|
|
/* Clock */
|
|
static uint32_t clock_start IBSS_ATTR; /* Clock at playback start */
|
|
static uint32_t volatile clock_tick IBSS_ATTR; /* Our base clock */
|
|
static uint32_t volatile clock_time IBSS_ATTR; /* Timestamp adjusted */
|
|
|
|
static int pcm_skipped = 0;
|
|
static int pcm_underruns = 0;
|
|
|
|
/* Small silence clip. ~5.80ms @ 44.1kHz */
|
|
static int16_t silence[256*2] ALIGNED_ATTR(4) = { 0 };
|
|
|
|
/* Delete all buffer contents */
|
|
static void pcm_reset_buffer(void)
|
|
{
|
|
pcmbuf_threshold = PCMOUT_PLAY_WM;
|
|
pcmbuf_curr_size = pcmbuf_read = pcmbuf_written = 0;
|
|
pcmbuf_head = pcmbuf_tail = pcm_buffer;
|
|
pcm_skipped = pcm_underruns = 0;
|
|
}
|
|
|
|
/* Advance a PCM buffer pointer by size bytes circularly */
|
|
static inline void pcm_advance_buffer(struct pcm_frame_header **p,
|
|
size_t size)
|
|
{
|
|
*p = SKIPBYTES(*p, size);
|
|
if (*p >= pcmbuf_end)
|
|
*p = SKIPBYTES(*p, -PCMOUT_BUFSIZE);
|
|
}
|
|
|
|
/* Return physical space used */
|
|
static inline ssize_t pcm_output_bytes_used(void)
|
|
{
|
|
return pcmbuf_written - pcmbuf_read; /* wrap-safe */
|
|
}
|
|
|
|
/* Return physical space free */
|
|
static inline ssize_t pcm_output_bytes_free(void)
|
|
{
|
|
return PCMOUT_BUFSIZE - pcm_output_bytes_used();
|
|
}
|
|
|
|
/* Audio DMA handler */
|
|
static void get_more(unsigned char **start, size_t *size)
|
|
{
|
|
ssize_t sz;
|
|
|
|
/* Free-up the last frame played frame if any */
|
|
pcmbuf_read += pcmbuf_curr_size;
|
|
pcmbuf_curr_size = 0;
|
|
|
|
sz = pcm_output_bytes_used();
|
|
|
|
if (sz > pcmbuf_threshold)
|
|
{
|
|
pcmbuf_threshold = PCMOUT_LOW_WM;
|
|
|
|
while (1)
|
|
{
|
|
uint32_t time = pcmbuf_head->time;
|
|
int32_t offset = time - clock_time;
|
|
|
|
sz = pcmbuf_head->size;
|
|
|
|
if (sz < (ssize_t)(PCM_HDR_SIZE + 4) ||
|
|
(sz & 3) != 0)
|
|
{
|
|
/* Just show a warning about this - will never happen
|
|
* without a corrupted buffer */
|
|
DEBUGF("get_more: invalid size (%ld)\n", (long)sz);
|
|
}
|
|
|
|
if (offset < -100*CLOCK_RATE/1000)
|
|
{
|
|
/* Frame more than 100ms late - drop it */
|
|
pcm_advance_buffer(&pcmbuf_head, sz);
|
|
pcmbuf_read += sz;
|
|
pcm_skipped++;
|
|
if (pcm_output_bytes_used() > 0)
|
|
continue;
|
|
|
|
/* Ran out so revert to default watermark */
|
|
pcmbuf_threshold = PCMOUT_PLAY_WM;
|
|
pcm_underruns++;
|
|
}
|
|
else if (offset < 100*CLOCK_RATE/1000)
|
|
{
|
|
/* Frame less than 100ms early - play it */
|
|
struct pcm_frame_header *head = pcmbuf_head;
|
|
|
|
pcm_advance_buffer(&pcmbuf_head, sz);
|
|
pcmbuf_curr_size = sz;
|
|
|
|
sz -= PCM_HDR_SIZE;
|
|
|
|
/* Audio is time master - keep clock synchronized */
|
|
clock_time = time + (sz >> 2);
|
|
|
|
/* Update base clock */
|
|
clock_tick += sz >> 2;
|
|
|
|
*start = head->data;
|
|
*size = sz;
|
|
return;
|
|
}
|
|
/* Frame will be dropped - play silence clip */
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Ran out so revert to default watermark */
|
|
if (pcmbuf_threshold == PCMOUT_LOW_WM)
|
|
pcm_underruns++;
|
|
|
|
pcmbuf_threshold = PCMOUT_PLAY_WM;
|
|
}
|
|
|
|
/* Keep clock going at all times */
|
|
clock_time += sizeof (silence) / 4;
|
|
clock_tick += sizeof (silence) / 4;
|
|
|
|
*start = (unsigned char *)silence;
|
|
*size = sizeof (silence);
|
|
|
|
if (sz < 0)
|
|
pcmbuf_read = pcmbuf_written;
|
|
}
|
|
|
|
/** Public interface **/
|
|
|
|
/* Return a buffer pointer if at least size bytes are available and if so,
|
|
* give the actual free space */
|
|
unsigned char * pcm_output_get_buffer(ssize_t *size)
|
|
{
|
|
ssize_t sz = *size;
|
|
ssize_t free = pcm_output_bytes_free() - PCM_HDR_SIZE;
|
|
|
|
if (sz >= 0 && free >= sz)
|
|
{
|
|
*size = free; /* return actual free space (- header) */
|
|
return pcmbuf_tail->data;
|
|
}
|
|
|
|
/* Leave *size alone so caller doesn't have to reinit */
|
|
return NULL;
|
|
}
|
|
|
|
/* Commit the buffer returned by pcm_ouput_get_buffer; timestamp is PCM
|
|
* clock time units, not video format time units */
|
|
bool pcm_output_commit_data(ssize_t size, uint32_t timestamp)
|
|
{
|
|
if (size <= 0 || (size & 3))
|
|
return false; /* invalid */
|
|
|
|
size += PCM_HDR_SIZE;
|
|
|
|
if (size > pcm_output_bytes_free())
|
|
return false; /* too big */
|
|
|
|
pcmbuf_tail->size = size;
|
|
pcmbuf_tail->time = timestamp;
|
|
|
|
pcm_advance_buffer(&pcmbuf_tail, size);
|
|
pcmbuf_written += size;
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Returns 'true' if the buffer is completely empty */
|
|
bool pcm_output_empty(void)
|
|
{
|
|
return pcm_output_bytes_used() <= 0;
|
|
}
|
|
|
|
/* Flushes the buffer - clock keeps counting */
|
|
void pcm_output_flush(void)
|
|
{
|
|
rb->pcm_play_lock();
|
|
|
|
enum channel_status status = rb->mixer_channel_status(MPEG_PCM_CHANNEL);
|
|
|
|
/* Stop PCM to clear current buffer */
|
|
if (status != CHANNEL_STOPPED)
|
|
rb->mixer_channel_stop(MPEG_PCM_CHANNEL);
|
|
|
|
rb->pcm_play_unlock();
|
|
|
|
pcm_reset_buffer();
|
|
|
|
/* Restart if playing state was current */
|
|
if (status == CHANNEL_PLAYING)
|
|
rb->mixer_channel_play_data(MPEG_PCM_CHANNEL,
|
|
get_more, NULL, 0);
|
|
}
|
|
|
|
/* Seek the reference clock to the specified time - next audio data ready to
|
|
go to DMA should be on the buffer with the same time index or else the PCM
|
|
buffer should be empty */
|
|
void pcm_output_set_clock(uint32_t time)
|
|
{
|
|
rb->pcm_play_lock();
|
|
|
|
clock_start = time;
|
|
clock_tick = time;
|
|
clock_time = time;
|
|
|
|
rb->pcm_play_unlock();
|
|
}
|
|
|
|
/* Return the clock as synchronized by audio frame timestamps */
|
|
uint32_t pcm_output_get_clock(void)
|
|
{
|
|
uint32_t time, rem;
|
|
|
|
/* Reread if data race detected - rem will be 0 if driver hasn't yet
|
|
* updated to the new buffer size. Also be sure pcm state doesn't
|
|
* cause indefinite loop.
|
|
*
|
|
* FYI: NOT scrutinized for rd/wr reordering on different cores. */
|
|
do
|
|
{
|
|
time = clock_time;
|
|
rem = rb->mixer_channel_get_bytes_waiting(MPEG_PCM_CHANNEL) >> 2;
|
|
}
|
|
while (UNLIKELY(time != clock_time ||
|
|
(rem == 0 &&
|
|
rb->mixer_channel_status(MPEG_PCM_CHANNEL) == CHANNEL_PLAYING))
|
|
);
|
|
|
|
return time - rem;
|
|
|
|
}
|
|
|
|
/* Return the raw clock as counted from the last pcm_output_set_clock
|
|
* call */
|
|
uint32_t pcm_output_get_ticks(uint32_t *start)
|
|
{
|
|
uint32_t tick, rem;
|
|
|
|
/* Same procedure as pcm_output_get_clock */
|
|
do
|
|
{
|
|
tick = clock_tick;
|
|
rem = rb->mixer_channel_get_bytes_waiting(MPEG_PCM_CHANNEL) >> 2;
|
|
}
|
|
while (UNLIKELY(tick != clock_tick ||
|
|
(rem == 0 &&
|
|
rb->mixer_channel_status(MPEG_PCM_CHANNEL) == CHANNEL_PLAYING))
|
|
);
|
|
|
|
if (start)
|
|
*start = clock_start;
|
|
|
|
return tick - rem;
|
|
}
|
|
|
|
/* Pauses/Starts pcm playback - and the clock */
|
|
void pcm_output_play_pause(bool play)
|
|
{
|
|
rb->pcm_play_lock();
|
|
|
|
if (rb->mixer_channel_status(MPEG_PCM_CHANNEL) != CHANNEL_STOPPED)
|
|
{
|
|
rb->mixer_channel_play_pause(MPEG_PCM_CHANNEL, play);
|
|
rb->pcm_play_unlock();
|
|
}
|
|
else
|
|
{
|
|
rb->pcm_play_unlock();
|
|
|
|
if (play)
|
|
{
|
|
rb->mixer_channel_set_amplitude(MPEG_PCM_CHANNEL, MIX_AMP_UNITY);
|
|
rb->mixer_channel_play_data(MPEG_PCM_CHANNEL,
|
|
get_more, NULL, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Stops all playback and resets the clock */
|
|
void pcm_output_stop(void)
|
|
{
|
|
rb->pcm_play_lock();
|
|
|
|
if (rb->mixer_channel_status(MPEG_PCM_CHANNEL) != CHANNEL_STOPPED)
|
|
rb->mixer_channel_stop(MPEG_PCM_CHANNEL);
|
|
|
|
rb->pcm_play_unlock();
|
|
|
|
pcm_output_flush();
|
|
pcm_output_set_clock(0);
|
|
}
|
|
|
|
/* Drains any data if the start threshold hasn't been reached */
|
|
void pcm_output_drain(void)
|
|
{
|
|
rb->pcm_play_lock();
|
|
pcmbuf_threshold = PCMOUT_LOW_WM;
|
|
rb->pcm_play_unlock();
|
|
}
|
|
|
|
bool pcm_output_init(void)
|
|
{
|
|
pcm_buffer = mpeg_malloc(PCMOUT_ALLOC_SIZE, MPEG_ALLOC_PCMOUT);
|
|
if (pcm_buffer == NULL)
|
|
return false;
|
|
|
|
pcmbuf_end = SKIPBYTES(pcm_buffer, PCMOUT_BUFSIZE);
|
|
|
|
pcm_reset_buffer();
|
|
|
|
#if INPUT_SRC_CAPS != 0
|
|
/* Select playback */
|
|
rb->audio_set_input_source(AUDIO_SRC_PLAYBACK, SRCF_PLAYBACK);
|
|
rb->audio_set_output_source(AUDIO_SRC_PLAYBACK);
|
|
#endif
|
|
|
|
#if SILENCE_TEST_TONE
|
|
/* Make the silence clip a square wave */
|
|
const int16_t silence_amp = INT16_MAX / 16;
|
|
unsigned i;
|
|
|
|
for (i = 0; i < ARRAYLEN(silence); i += 2)
|
|
{
|
|
if (i < ARRAYLEN(silence)/2)
|
|
{
|
|
silence[i] = silence_amp;
|
|
silence[i+1] = silence_amp;
|
|
}
|
|
else
|
|
{
|
|
silence[i] = -silence_amp;
|
|
silence[i+1] = -silence_amp;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
void pcm_output_exit(void)
|
|
{
|
|
}
|