| /*************************************************************************** |
| * __________ __ ___. |
| * 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" |
| |
| /* 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 uint64_t pcmbuf_read IBSS_ATTR; /* Number of bytes read by DMA */ |
| static uint64_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_base IBSS_ATTR; /* Our base clock */ |
| static uint32_t clock_start IBSS_ATTR; /* Clock at playback start */ |
| static int32_t clock_adjust IBSS_ATTR; /* Clock drift adjustment */ |
| |
| int pcm_skipped = 0; |
| int pcm_underruns = 0; |
| |
| /* Small silence clip. ~5.80ms @ 44.1kHz */ |
| static int16_t silence[256*2] ALIGNED_ATTR(4) = { 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); |
| } |
| |
| /* Inline internally but not externally */ |
| inline ssize_t pcm_output_used(void) |
| { |
| return (ssize_t)(pcmbuf_written - pcmbuf_read); |
| } |
| |
| inline ssize_t pcm_output_free(void) |
| { |
| return (ssize_t)(PCMOUT_BUFSIZE - pcmbuf_written + pcmbuf_read); |
| } |
| |
| /* 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_used(); |
| |
| if (sz > pcmbuf_threshold) |
| { |
| pcmbuf_threshold = PCMOUT_LOW_WM; |
| |
| while (1) |
| { |
| uint32_t time = pcmbuf_head->time; |
| int32_t offset = time - (clock_base + clock_adjust); |
| |
| sz = pcmbuf_head->size; |
| |
| if (sz < (ssize_t)(sizeof(pcmbuf_head) + 4) || |
| (sz & 3) != 0) |
| { |
| /* Just show a warning about this - will never happen |
| * without a bug in the audio thread code or a clobbered |
| * buffer */ |
| DEBUGF("get_more: invalid size (%ld)\n", 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 (pcmbuf_read < pcmbuf_written) |
| continue; |
| |
| /* Ran out so revert to default watermark */ |
| pcmbuf_threshold = PCMOUT_PLAY_WM; |
| } |
| else if (offset < 100*CLOCK_RATE/1000) |
| { |
| /* Frame less than 100ms early - play it */ |
| *start = pcmbuf_head->data; |
| |
| pcm_advance_buffer(&pcmbuf_head, sz); |
| pcmbuf_curr_size = sz; |
| |
| sz -= sizeof (struct pcm_frame_header); |
| |
| *size = sz; |
| |
| /* Audio is time master - keep clock synchronized */ |
| clock_adjust = time - clock_base; |
| |
| /* Update base clock */ |
| clock_base += sz >> 2; |
| 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 */ |
| *start = (unsigned char *)silence; |
| *size = sizeof (silence); |
| |
| clock_base += sizeof (silence) / 4; |
| |
| if (pcmbuf_read > pcmbuf_written) |
| pcmbuf_read = pcmbuf_written; |
| } |
| |
| struct pcm_frame_header * pcm_output_get_buffer(void) |
| { |
| return pcmbuf_tail; |
| } |
| |
| void pcm_output_add_data(void) |
| { |
| size_t size = pcmbuf_tail->size; |
| pcm_advance_buffer(&pcmbuf_tail, size); |
| pcmbuf_written += size; |
| } |
| |
| /* Flushes the buffer - clock keeps counting */ |
| void pcm_output_flush(void) |
| { |
| bool playing, paused; |
| |
| rb->pcm_play_lock(); |
| |
| playing = rb->pcm_is_playing(); |
| paused = rb->pcm_is_paused(); |
| |
| /* Stop PCM to clear current buffer */ |
| if (playing) |
| rb->pcm_play_stop(); |
| |
| pcmbuf_threshold = PCMOUT_PLAY_WM; |
| pcmbuf_curr_size = pcmbuf_read = pcmbuf_written = 0; |
| pcmbuf_head = pcmbuf_tail = pcm_buffer; |
| pcm_skipped = pcm_underruns = 0; |
| |
| /* Restart if playing state was current */ |
| if (playing && !paused) |
| rb->pcm_play_data(get_more, NULL, 0); |
| |
| rb->pcm_play_unlock(); |
| } |
| |
| /* 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_base = time; |
| clock_start = time; |
| clock_adjust = 0; |
| |
| rb->pcm_play_unlock(); |
| } |
| |
| uint32_t pcm_output_get_clock(void) |
| { |
| return clock_base + clock_adjust |
| - (rb->pcm_get_bytes_waiting() >> 2); |
| } |
| |
| uint32_t pcm_output_get_ticks(uint32_t *start) |
| { |
| if (start) |
| *start = clock_start; |
| |
| return clock_base - (rb->pcm_get_bytes_waiting() >> 2); |
| } |
| |
| /* Pauses/Starts pcm playback - and the clock */ |
| void pcm_output_play_pause(bool play) |
| { |
| rb->pcm_play_lock(); |
| |
| if (rb->pcm_is_playing()) |
| { |
| rb->pcm_play_pause(play); |
| } |
| else if (play) |
| { |
| rb->pcm_play_data(get_more, NULL, 0); |
| } |
| |
| rb->pcm_play_unlock(); |
| } |
| |
| /* Stops all playback and resets the clock */ |
| void pcm_output_stop(void) |
| { |
| rb->pcm_play_lock(); |
| |
| if (rb->pcm_is_playing()) |
| rb->pcm_play_stop(); |
| |
| pcm_output_flush(); |
| pcm_output_set_clock(0); |
| |
| rb->pcm_play_unlock(); |
| } |
| |
| /* 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_threshold = PCMOUT_PLAY_WM; |
| pcmbuf_head = pcm_buffer; |
| pcmbuf_tail = pcm_buffer; |
| pcmbuf_end = SKIPBYTES(pcm_buffer, PCMOUT_BUFSIZE); |
| pcmbuf_curr_size = pcmbuf_read = pcmbuf_written = 0; |
| |
| rb->pcm_set_frequency(NATIVE_FREQUENCY); |
| |
| #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) |
| { |
| rb->pcm_set_frequency(HW_SAMPR_DEFAULT); |
| } |