/*************************************************************************** * __________ __ ___. * 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) { }