summaryrefslogtreecommitdiffstats
path: root/apps/plugins/mpegplayer/pcm_output.c
blob: 8db95310499000377dfb1f18db5c9999f90556ff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
/***************************************************************************
 *             __________               __   ___.
 *   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)
{
}