69d08be083
In addition to the existing behavior of pausing after each song, this adds options to pause after playing current: Album, Album Artist, Artist, Composer, Grouping / Work, or Genre. Allows you, for example, to only listen to the remaining movements of a classical work without having to purge your playlist of any upcoming songs. Change-Id: If18f4a5d139320026cc5fcc9adf29dd8e4e028a8
1437 lines
39 KiB
C
1437 lines
39 KiB
C
/***************************************************************************
|
|
* __________ __ ___.
|
|
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
|
|
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
|
|
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
|
|
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
|
|
* \/ \/ \/ \/ \/
|
|
* $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
|
|
* 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 <stdio.h>
|
|
#include "config.h"
|
|
#include "system.h"
|
|
#include "debug.h"
|
|
#include <kernel.h>
|
|
#include "pcm.h"
|
|
#include "pcm_mixer.h"
|
|
#include "pcmbuf.h"
|
|
#include "dsp-util.h"
|
|
#include "playback.h"
|
|
#include "codec_thread.h"
|
|
|
|
/* Define LOGF_ENABLE to enable logf output in this file */
|
|
/*#define LOGF_ENABLE*/
|
|
#include "logf.h"
|
|
#if (CONFIG_PLATFORM & PLATFORM_NATIVE)
|
|
#include "cpu.h"
|
|
#endif
|
|
#include "settings.h"
|
|
#include "audio.h"
|
|
#include "voice_thread.h"
|
|
|
|
/* 2 channels * 2 bytes/sample, interleaved */
|
|
#define PCMBUF_SAMPLE_SIZE (2 * 2)
|
|
|
|
/* 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
|
|
|
|
/* Small guard buf to give decent space near end */
|
|
#define PCMBUF_GUARD_SIZE (PCMBUF_CHUNK_SIZE / 8)
|
|
|
|
/* 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 PCMBUF_CHUNK_SIZE
|
|
|
|
/* Maximum contiguous space that PCM buffer will allow (to avoid excessive
|
|
draining between inserts and observe low-latency mode) */
|
|
#define PCMBUF_MAX_BUFFER (PCMBUF_CHUNK_SIZE * 4)
|
|
|
|
/* Forced buffer insert constraint can thus be from 1KB to 32KB using 8KB
|
|
chunks */
|
|
|
|
/* Return data level in 1/4-second increments */
|
|
#define DATA_LEVEL(quarter_secs) (pcmbuf_sampr * (quarter_secs))
|
|
|
|
/* Number of bytes played per second */
|
|
#define BYTERATE (pcmbuf_sampr * PCMBUF_SAMPLE_SIZE)
|
|
|
|
#if MEMORYSIZE > 2
|
|
/* Keep watermark high for large memory target - at least (2s) */
|
|
#define PCMBUF_WATERMARK (BYTERATE * 2)
|
|
#define MIN_BUFFER_SIZE (BYTERATE * 3)
|
|
/* 1 seconds of buffer is low data */
|
|
#define LOW_DATA DATA_LEVEL(4)
|
|
#else
|
|
#define PCMBUF_WATERMARK (BYTERATE / 4) /* 0.25 seconds */
|
|
#define MIN_BUFFER_SIZE (BYTERATE * 1)
|
|
/* under watermark is low data */
|
|
#define LOW_DATA pcmbuf_watermark
|
|
#endif
|
|
|
|
/* Describes each audio packet - keep it small since there are many of them */
|
|
struct chunkdesc
|
|
{
|
|
uint16_t size; /* Actual size (0 < size <= PCMBUF_CHUNK_SIZE) */
|
|
uint16_t pos_key; /* Who put the position info in
|
|
(undefined: 0, valid: 1..POSITION_KEY_MAX) */
|
|
unsigned long elapsed; /* Elapsed time to use */
|
|
off_t offset; /* Offset to use */
|
|
};
|
|
|
|
#define POSITION_KEY_MAX UINT16_MAX
|
|
|
|
|
|
/* General PCM buffer data */
|
|
#define INVALID_BUF_INDEX ((size_t)0 - (size_t)1)
|
|
|
|
static void *pcmbuf_buffer;
|
|
static void *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 unsigned int pcmbuf_sampr = 0;
|
|
|
|
static size_t chunk_ridx;
|
|
static size_t chunk_widx;
|
|
|
|
static size_t pcmbuf_bytes_waiting;
|
|
static struct chunkdesc *current_desc;
|
|
static size_t chunk_transidx;
|
|
|
|
static size_t pcmbuf_watermark = 0;
|
|
|
|
static bool low_latency_mode = false;
|
|
|
|
static bool pcmbuf_sync_position = false;
|
|
|
|
/* Fade effect */
|
|
static unsigned int fade_vol = MIX_AMP_UNITY;
|
|
static enum
|
|
{
|
|
PCM_NOT_FADING = 0,
|
|
PCM_FADING_IN,
|
|
PCM_FADING_OUT,
|
|
} fade_state = PCM_NOT_FADING;
|
|
static bool fade_out_complete = false;
|
|
|
|
/* Voice */
|
|
static bool soft_mode = false;
|
|
|
|
#ifdef HAVE_CROSSFADE
|
|
/* Crossfade related state */
|
|
|
|
static int crossfade_setting;
|
|
static int crossfade_enable_request;
|
|
|
|
static enum
|
|
{
|
|
CROSSFADE_INACTIVE = 0, /* Crossfade is OFF */
|
|
CROSSFADE_ACTIVE, /* Crossfade is fading in */
|
|
CROSSFADE_START, /* New crossfade is starting */
|
|
CROSSFADE_CONTINUE, /* Next track continues fade */
|
|
} crossfade_status = CROSSFADE_INACTIVE;
|
|
|
|
static bool crossfade_mixmode;
|
|
static bool crossfade_auto_skip;
|
|
static size_t crossfade_widx;
|
|
static size_t crossfade_bufidx;
|
|
|
|
struct mixfader
|
|
{
|
|
int32_t factor; /* Current volume factor to use */
|
|
int32_t endfac; /* Saturating end factor */
|
|
int32_t nsamp2; /* Twice the number of samples */
|
|
int32_t dfact2; /* Twice the range of factors */
|
|
int32_t ferr; /* Current error accumulator */
|
|
int32_t dfquo; /* Quotient of fade range / sample range */
|
|
int32_t dfrem; /* Remainder of fade range / sample range */
|
|
int32_t dfinc; /* Base increment (-1 or +1) */
|
|
bool alloc; /* Allocate blocks if needed else abort at EOB */
|
|
} crossfade_infader;
|
|
|
|
/* 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 */
|
|
};
|
|
|
|
#define MIXFADE_UNITY_BITS 16
|
|
#define MIXFADE_UNITY (1 << MIXFADE_UNITY_BITS)
|
|
|
|
static void crossfade_cancel(void);
|
|
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);
|
|
#else
|
|
#define crossfade_cancel() do {} while(0)
|
|
#endif /* HAVE_CROSSFADE */
|
|
|
|
/* Thread */
|
|
#ifdef HAVE_PRIORITY_SCHEDULING
|
|
static int codec_thread_priority = PRIORITY_PLAYBACK;
|
|
#endif
|
|
|
|
/* Callbacks into playback.c */
|
|
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);
|
|
|
|
|
|
/**************************************/
|
|
|
|
/* start PCM if callback says it's alright */
|
|
static void start_audio_playback(void)
|
|
{
|
|
if (audio_pcmbuf_may_play())
|
|
pcmbuf_play_start();
|
|
}
|
|
|
|
/* 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)
|
|
{
|
|
size_t ridx = chunk_ridx;
|
|
size_t widx = chunk_widx;
|
|
|
|
if (ridx > widx)
|
|
widx += pcmbuf_size;
|
|
|
|
return widx - ridx;
|
|
}
|
|
|
|
/* Returns TRUE if amount of data is under the target fill size */
|
|
static bool pcmbuf_data_critical(void)
|
|
{
|
|
return pcmbuf_unplayed_bytes() < LOW_DATA;
|
|
}
|
|
|
|
/* 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 the first byte of a chunk for a byte index in the buffer, offset by 'offset'
|
|
chunks */
|
|
static size_t index_chunk_offs(size_t index, int offset)
|
|
{
|
|
int i = index / PCMBUF_CHUNK_SIZE;
|
|
|
|
if (offset != 0)
|
|
{
|
|
i = (i + offset) % (int)pcmbuf_desc_count;
|
|
|
|
/* remainder => modulus */
|
|
if (i < 0)
|
|
i += pcmbuf_desc_count;
|
|
}
|
|
|
|
return i * PCMBUF_CHUNK_SIZE;
|
|
}
|
|
|
|
/* Test if a buffer index lies within the committed data region */
|
|
static bool index_committed(size_t index)
|
|
{
|
|
if (index == INVALID_BUF_INDEX)
|
|
return false;
|
|
|
|
size_t ridx = chunk_ridx;
|
|
size_t widx = chunk_widx;
|
|
|
|
if (widx < ridx)
|
|
{
|
|
widx += pcmbuf_size;
|
|
|
|
if (index < ridx)
|
|
index += pcmbuf_size;
|
|
}
|
|
|
|
return index >= ridx && index < widx;
|
|
}
|
|
|
|
/* Snip the tail of buffer at chunk of specified index plus chunk offset */
|
|
void snip_buffer_tail(size_t index, int offset)
|
|
{
|
|
/* Call with PCM lockout */
|
|
if (index == INVALID_BUF_INDEX)
|
|
return;
|
|
|
|
index = index_chunk_offs(index, offset);
|
|
|
|
if (!index_committed(index) && index != chunk_widx)
|
|
return;
|
|
|
|
chunk_widx = index;
|
|
pcmbuf_bytes_waiting = 0;
|
|
index_chunkdesc(index)->pos_key = 0;
|
|
|
|
#ifdef HAVE_CROSSFADE
|
|
/* Kill crossfade if it would now be operating in the void */
|
|
if (crossfade_status != CROSSFADE_INACTIVE &&
|
|
!index_committed(crossfade_widx) && crossfade_widx != chunk_widx)
|
|
{
|
|
crossfade_cancel();
|
|
}
|
|
#endif /* HAVE_CROSSFADE */
|
|
}
|
|
|
|
|
|
/** Accept new PCM data */
|
|
|
|
/* Split the uncommitted data as needed into chunks, stopping when uncommitted
|
|
data is below the threshold */
|
|
static void commit_chunks(size_t threshold)
|
|
{
|
|
size_t index = chunk_widx;
|
|
size_t end_index = index + pcmbuf_bytes_waiting;
|
|
|
|
/* 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);
|
|
|
|
struct chunkdesc *desc = index_chunkdesc(index);
|
|
|
|
do
|
|
{
|
|
size_t size = MIN(pcmbuf_bytes_waiting, PCMBUF_CHUNK_SIZE);
|
|
pcmbuf_bytes_waiting -= size;
|
|
|
|
/* 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->pos_key = 0;
|
|
}
|
|
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)
|
|
{
|
|
desc->pos_key = key;
|
|
desc->elapsed = elapsed;
|
|
desc->offset = offset;
|
|
}
|
|
}
|
|
|
|
/* Set priority of the codec thread */
|
|
#ifdef HAVE_PRIORITY_SCHEDULING
|
|
/*
|
|
* 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 int8_t prios[11] =
|
|
{
|
|
PRIORITY_PLAYBACK_MAX, /* 0 - 10% */
|
|
PRIORITY_PLAYBACK_MAX+1, /* 10 - 20% */
|
|
PRIORITY_PLAYBACK_MAX+3, /* 20 - 30% */
|
|
PRIORITY_PLAYBACK_MAX+5, /* 30 - 40% */
|
|
PRIORITY_PLAYBACK_MAX+7, /* 40 - 50% */
|
|
PRIORITY_PLAYBACK_MAX+8, /* 50 - 60% */
|
|
PRIORITY_PLAYBACK_MAX+9, /* 60 - 70% */
|
|
/* 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
|
|
* will starve if the codec thread's priority is boosted. */
|
|
if (new_prio != codec_thread_priority)
|
|
{
|
|
codec_thread_set_priority(new_prio);
|
|
voice_thread_set_priority(new_prio);
|
|
codec_thread_priority = new_prio;
|
|
}
|
|
}
|
|
#else
|
|
#define boost_codec_thread(pcm_fill_state) do{}while(0)
|
|
#endif /* HAVE_PRIORITY_SCHEDULING */
|
|
|
|
/* Get the next available buffer and size - assumes adequate space exists */
|
|
static void * get_write_buffer(size_t *size)
|
|
{
|
|
/* Obtain current chunk fill address */
|
|
size_t index = chunk_widx + pcmbuf_bytes_waiting;
|
|
size_t index_end = pcmbuf_size + PCMBUF_GUARD_SIZE;
|
|
|
|
/* Get count to the end of the buffer where a wrap will happen +
|
|
the guard */
|
|
size_t endsize = index_end - index;
|
|
|
|
/* Return available unwrapped space */
|
|
*size = MIN(*size, endsize);
|
|
|
|
return index_buffer(index);
|
|
}
|
|
|
|
/* Commit outstanding data leaving less than a chunk size remaining */
|
|
static void commit_write_buffer(size_t size)
|
|
{
|
|
/* 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 * PCMBUF_SAMPLE_SIZE;
|
|
|
|
#ifdef HAVE_CROSSFADE
|
|
/* We're going to crossfade to a new track, which is now on its way */
|
|
if (crossfade_status > CROSSFADE_ACTIVE)
|
|
crossfade_start();
|
|
|
|
/* If crossfade has begun, put the new track samples in the crossfade
|
|
buffer area */
|
|
if (crossfade_status != CROSSFADE_INACTIVE && size > CROSSFADE_BUFSIZE)
|
|
size = CROSSFADE_BUFSIZE;
|
|
else
|
|
#endif /* HAVE_CROSSFADE */
|
|
if (size > PCMBUF_MAX_BUFFER)
|
|
size = PCMBUF_MAX_BUFFER; /* constrain request */
|
|
|
|
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 (status != CHANNEL_STOPPED)
|
|
{
|
|
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(realrem*10 / pcmbuf_size);
|
|
}
|
|
else /* !playing */
|
|
{
|
|
/* Boost CPU for pre-buffer */
|
|
trigger_cpu_boost();
|
|
|
|
/* If pre-buffered to the watermark, start playback */
|
|
if (!pcmbuf_data_critical())
|
|
start_audio_playback();
|
|
}
|
|
|
|
void *buf;
|
|
|
|
#ifdef HAVE_CROSSFADE
|
|
if (crossfade_status != CROSSFADE_INACTIVE)
|
|
{
|
|
crossfade_bufidx = index_chunk_offs(chunk_ridx, -1);
|
|
buf = index_buffer(crossfade_bufidx); /* always CROSSFADE_BUFSIZE */
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
/* Give the maximum amount available if there's more */
|
|
if (size + PCMBUF_CHUNK_SIZE < freespace)
|
|
size = freespace - PCMBUF_CHUNK_SIZE;
|
|
|
|
buf = get_write_buffer(&size);
|
|
}
|
|
|
|
*count = size / PCMBUF_SAMPLE_SIZE;
|
|
return buf;
|
|
}
|
|
|
|
/* Handle new samples to the buffer */
|
|
void pcmbuf_write_complete(int count, unsigned long elapsed, off_t offset)
|
|
{
|
|
size_t size = count * PCMBUF_SAMPLE_SIZE;
|
|
|
|
#ifdef HAVE_CROSSFADE
|
|
if (crossfade_status != CROSSFADE_INACTIVE)
|
|
{
|
|
write_to_crossfade(size, elapsed, offset);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
stamp_chunk(index_chunkdesc(chunk_widx), elapsed, offset);
|
|
commit_write_buffer(size);
|
|
}
|
|
|
|
/* Revert to position updates by PCM */
|
|
pcmbuf_sync_position = false;
|
|
}
|
|
|
|
|
|
/** Init */
|
|
static unsigned int get_next_required_pcmbuf_chunks(void)
|
|
{
|
|
size_t size = MIN_BUFFER_SIZE;
|
|
|
|
#ifdef HAVE_CROSSFADE
|
|
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
|
|
|
|
logf("pcmbuf len: %lu", (unsigned long)(size / BYTERATE));
|
|
return size / PCMBUF_CHUNK_SIZE;
|
|
}
|
|
|
|
/* Initialize the ringbuffer state */
|
|
static void init_buffer_state(void)
|
|
{
|
|
/* Reset counters */
|
|
chunk_ridx = chunk_widx = 0;
|
|
pcmbuf_bytes_waiting = 0;
|
|
|
|
/* Reset first descriptor */
|
|
if (pcmbuf_descriptors)
|
|
pcmbuf_descriptors->pos_key = 0;
|
|
|
|
/* Clear change notification */
|
|
chunk_transidx = INVALID_BUF_INDEX;
|
|
}
|
|
|
|
/* Initialize the PCM buffer. The structure looks like this:
|
|
* ...|---------PCMBUF---------|GUARDBUF|DESCS| */
|
|
size_t pcmbuf_init(void *bufend)
|
|
{
|
|
void *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
|
|
pcmbuf_finish_crossfade_enable();
|
|
#else
|
|
pcmbuf_watermark = PCMBUF_WATERMARK;
|
|
#endif /* HAVE_CROSSFADE */
|
|
|
|
init_buffer_state();
|
|
|
|
pcmbuf_soft_mode(false);
|
|
|
|
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)
|
|
{
|
|
/* Call with PCM lockout */
|
|
if (chunk_ridx != chunk_widx && index != INVALID_BUF_INDEX)
|
|
{
|
|
/* If monitoring, set flag for one previous to specified chunk */
|
|
index = index_chunk_offs(index, -1);
|
|
|
|
/* Ensure PCM playback hasn't already played this out */
|
|
if (index_committed(index))
|
|
{
|
|
chunk_transidx = index;
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Post now if buffer is no longer coming up */
|
|
chunk_transidx = INVALID_BUF_INDEX;
|
|
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)
|
|
{
|
|
/* Call with PCM lockout */
|
|
snip_buffer_tail(chunk_transidx, 1);
|
|
|
|
chunk_transidx = INVALID_BUF_INDEX;
|
|
|
|
if (!position)
|
|
return;
|
|
|
|
size_t index = chunk_ridx;
|
|
|
|
while (1)
|
|
{
|
|
index_chunkdesc(index)->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 (monitor)
|
|
pcmbuf_monitor_track_change_ex(chunk_widx);
|
|
else
|
|
pcmbuf_cancel_track_change(false);
|
|
|
|
pcm_play_unlock();
|
|
}
|
|
|
|
void pcmbuf_start_track_change(enum pcm_track_change_type type)
|
|
{
|
|
/* Commit all outstanding data before starting next track - tracks don't
|
|
comingle inside a single buffer chunk */
|
|
commit_if_needed(COMMIT_ALL_DATA);
|
|
|
|
if (type == TRACK_CHANGE_AUTO_PILEUP)
|
|
{
|
|
/* Fill might not have been above watermark */
|
|
start_audio_playback();
|
|
return;
|
|
}
|
|
|
|
#ifdef HAVE_CROSSFADE
|
|
bool crossfade = false;
|
|
#endif
|
|
bool auto_skip = type != TRACK_CHANGE_MANUAL;
|
|
|
|
/* 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 > POSITION_KEY_MAX)
|
|
position_key = 1;
|
|
|
|
if (type == TRACK_CHANGE_END_OF_DATA)
|
|
{
|
|
crossfade_cancel();
|
|
|
|
/* Fill might not have been above watermark */
|
|
start_audio_playback();
|
|
}
|
|
#ifdef HAVE_CROSSFADE
|
|
/* Determine whether this track change needs to crossfaded and how */
|
|
else if (crossfade_setting != CROSSFADE_ENABLE_OFF)
|
|
{
|
|
if (crossfade_status == CROSSFADE_INACTIVE &&
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (auto_skip && global_settings.single_mode != SINGLE_MODE_OFF && !global_settings.party_mode)
|
|
crossfade = false;
|
|
|
|
if (crossfade)
|
|
{
|
|
logf("crossfade track change");
|
|
|
|
/* 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_status = CROSSFADE_START;
|
|
|
|
pcmbuf_monitor_track_change(auto_skip);
|
|
|
|
trigger_cpu_boost();
|
|
}
|
|
else
|
|
#endif /* HAVE_CROSSFADE */
|
|
if (auto_skip)
|
|
{
|
|
/* The codec is moving on to the next track, but the current track will
|
|
* 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_ACTIVE)
|
|
crossfade_status = CROSSFADE_CONTINUE;
|
|
#endif
|
|
|
|
pcmbuf_monitor_track_change(true);
|
|
}
|
|
else
|
|
{
|
|
/* Discard old data; caller needs no transition notification */
|
|
logf("manual track change");
|
|
pcmbuf_play_stop();
|
|
}
|
|
}
|
|
|
|
|
|
/** Playback */
|
|
|
|
/* PCM driver callback */
|
|
static void pcmbuf_pcm_callback(const void **start, size_t *size)
|
|
{
|
|
/*- Process the chunk that just finished -*/
|
|
size_t index = chunk_ridx;
|
|
struct chunkdesc *desc = current_desc;
|
|
|
|
if (desc)
|
|
{
|
|
/* If last chunk in the track, notify of track change */
|
|
if (index == chunk_transidx)
|
|
{
|
|
chunk_transidx = INVALID_BUF_INDEX;
|
|
audio_pcmbuf_track_change(true);
|
|
}
|
|
|
|
/* Free it for reuse */
|
|
chunk_ridx = index = index_next(index);
|
|
}
|
|
|
|
/*- Process the new one -*/
|
|
if (index != chunk_widx && !fade_out_complete)
|
|
{
|
|
current_desc = desc = index_chunkdesc(index);
|
|
|
|
*start = index_buffer(index);
|
|
*size = desc->size;
|
|
|
|
if (desc->pos_key != 0)
|
|
{
|
|
/* Positioning chunk - notify playback */
|
|
audio_pcmbuf_position_callback(desc->elapsed, desc->offset,
|
|
desc->pos_key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Force playback */
|
|
void pcmbuf_play_start(void)
|
|
{
|
|
logf("pcmbuf_play_start");
|
|
|
|
if (mixer_channel_status(PCM_MIXER_CHAN_PLAYBACK) == CHANNEL_STOPPED &&
|
|
chunk_widx != chunk_ridx)
|
|
{
|
|
current_desc = NULL;
|
|
mixer_channel_play_data(PCM_MIXER_CHAN_PLAYBACK, pcmbuf_pcm_callback,
|
|
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);
|
|
|
|
/* Reset buffer */
|
|
init_buffer_state();
|
|
|
|
/* Revert to position updates by PCM */
|
|
pcmbuf_sync_position = false;
|
|
|
|
/* Fader OFF */
|
|
crossfade_cancel();
|
|
|
|
/* Can unboost the codec thread here no matter who's calling,
|
|
* pretend full pcm buffer to unboost */
|
|
boost_codec_thread(10);
|
|
}
|
|
|
|
void pcmbuf_pause(bool pause)
|
|
{
|
|
logf("pcmbuf_pause: %s", pause?"pause":"play");
|
|
|
|
if (mixer_channel_status(PCM_MIXER_CHAN_PLAYBACK) != CHANNEL_STOPPED)
|
|
mixer_channel_play_pause(PCM_MIXER_CHAN_PLAYBACK, !pause);
|
|
else if (!pause)
|
|
pcmbuf_play_start();
|
|
}
|
|
|
|
|
|
/** Crossfade */
|
|
|
|
#ifdef HAVE_CROSSFADE
|
|
|
|
/* Initialize a fader */
|
|
static void mixfader_init(struct mixfader *faderp, int32_t start_factor,
|
|
int32_t end_factor, size_t size, bool alloc)
|
|
{
|
|
/* Linear fade */
|
|
faderp->endfac = end_factor;
|
|
faderp->nsamp2 = size / PCMBUF_SAMPLE_SIZE * 2;
|
|
faderp->alloc = alloc;
|
|
|
|
if (faderp->nsamp2 == 0)
|
|
{
|
|
/* No data; set up as if fader finished the fade */
|
|
faderp->factor = end_factor;
|
|
return;
|
|
}
|
|
|
|
int32_t dfact2 = 2*abs(end_factor - start_factor);
|
|
faderp->factor = start_factor;
|
|
faderp->ferr = dfact2 / 2;
|
|
faderp->dfquo = dfact2 / faderp->nsamp2;
|
|
faderp->dfrem = dfact2 - faderp->dfquo*faderp->nsamp2;
|
|
faderp->dfinc = end_factor < start_factor ? -1 : +1;
|
|
faderp->dfquo *= faderp->dfinc;
|
|
}
|
|
|
|
/* Query if the fader has finished its envelope */
|
|
static inline bool mixfader_finished(const struct mixfader *faderp)
|
|
{
|
|
return faderp->factor == faderp->endfac;
|
|
}
|
|
|
|
/* Step fader by one sample */
|
|
static inline void mixfader_step(struct mixfader *faderp)
|
|
{
|
|
if (mixfader_finished(faderp))
|
|
return;
|
|
|
|
faderp->factor += faderp->dfquo;
|
|
faderp->ferr += faderp->dfrem;
|
|
|
|
if (faderp->ferr >= faderp->nsamp2)
|
|
{
|
|
faderp->factor += faderp->dfinc;
|
|
faderp->ferr -= faderp->nsamp2;
|
|
}
|
|
}
|
|
|
|
static FORCE_INLINE int32_t mixfade_sample(const struct mixfader *faderp, int32_t s)
|
|
{
|
|
return (faderp->factor * s + MIXFADE_UNITY/2) >> MIXFADE_UNITY_BITS;
|
|
}
|
|
|
|
/* Cancel crossfade operation */
|
|
static void crossfade_cancel(void)
|
|
{
|
|
crossfade_status = CROSSFADE_INACTIVE;
|
|
crossfade_widx = INVALID_BUF_INDEX;
|
|
}
|
|
|
|
/* 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 = index_chunk_offs(index, 0);
|
|
size += index - i;
|
|
|
|
while (i != chunk_widx)
|
|
{
|
|
size_t desc_size = index_chunkdesc(i)->size;
|
|
|
|
if (size < desc_size)
|
|
{
|
|
index = i + size;
|
|
break;
|
|
}
|
|
|
|
size -= desc_size;
|
|
i = index_next(i);
|
|
}
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
/* Align the needed buffer area up to the end of existing data */
|
|
static size_t crossfade_find_buftail(bool auto_skip, size_t buffer_rem,
|
|
size_t buffer_need, size_t *buffer_rem_outp)
|
|
{
|
|
size_t index = chunk_ridx;
|
|
|
|
if (buffer_rem > buffer_need)
|
|
{
|
|
size_t distance;
|
|
|
|
if (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 = MIN(BYTERATE / 5, buffer_rem);
|
|
buffer_rem -= distance;
|
|
}
|
|
|
|
index = crossfade_find_index(index, distance);
|
|
}
|
|
|
|
if (buffer_rem_outp)
|
|
*buffer_rem_outp = buffer_rem;
|
|
|
|
return index;
|
|
}
|
|
|
|
/* Run a fader on some buffers */
|
|
static void crossfade_mix_fade(struct mixfader *faderp, size_t size,
|
|
void *input_buf, size_t *out_index,
|
|
unsigned long elapsed, off_t offset)
|
|
{
|
|
if (size == 0)
|
|
return;
|
|
|
|
size_t index = *out_index;
|
|
|
|
if (index == INVALID_BUF_INDEX)
|
|
return;
|
|
|
|
int16_t *inbuf = input_buf;
|
|
|
|
bool alloced = inbuf && faderp->alloc &&
|
|
index_chunk_offs(index, 0) == chunk_widx;
|
|
|
|
while (size)
|
|
{
|
|
struct chunkdesc *desc = index_chunkdesc(index);
|
|
int16_t *outbuf = index_buffer(index);
|
|
|
|
switch (offset)
|
|
{
|
|
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);
|
|
}
|
|
|
|
size_t amount = (alloced ? PCMBUF_CHUNK_SIZE : desc->size)
|
|
- (index % PCMBUF_CHUNK_SIZE);
|
|
int16_t *chunkend = SKIPBYTES(outbuf, amount);
|
|
|
|
if (size < amount)
|
|
amount = size;
|
|
|
|
size -= amount;
|
|
|
|
if (alloced)
|
|
{
|
|
/* Fade the input buffer into the new destination chunk */
|
|
for (size_t s = amount; s != 0; s -= PCMBUF_SAMPLE_SIZE)
|
|
{
|
|
*outbuf++ = mixfade_sample(faderp, *inbuf++);
|
|
*outbuf++ = mixfade_sample(faderp, *inbuf++);
|
|
mixfader_step(faderp);
|
|
}
|
|
|
|
commit_write_buffer(amount);
|
|
}
|
|
else if (inbuf)
|
|
{
|
|
/* Fade the input buffer and mix into the destination chunk */
|
|
for (size_t s = amount; s != 0; s -= PCMBUF_SAMPLE_SIZE)
|
|
{
|
|
int32_t left = outbuf[0];
|
|
int32_t right = outbuf[1];
|
|
left += mixfade_sample(faderp, *inbuf++);
|
|
right += mixfade_sample(faderp, *inbuf++);
|
|
*outbuf++ = clip_sample_16(left);
|
|
*outbuf++ = clip_sample_16(right);
|
|
mixfader_step(faderp);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Fade the chunk in place */
|
|
for (size_t s = amount; s != 0; s -= PCMBUF_SAMPLE_SIZE)
|
|
{
|
|
int32_t left = outbuf[0];
|
|
int32_t right = outbuf[1];
|
|
*outbuf++ = mixfade_sample(faderp, left);
|
|
*outbuf++ = mixfade_sample(faderp, right);
|
|
mixfader_step(faderp);
|
|
}
|
|
}
|
|
|
|
if (outbuf < chunkend)
|
|
{
|
|
index += amount;
|
|
continue;
|
|
}
|
|
|
|
/* Move destination to next chunk as needed */
|
|
index = index_next(index);
|
|
|
|
if (index == chunk_widx)
|
|
{
|
|
/* End of existing data */
|
|
if (!inbuf || !faderp->alloc)
|
|
{
|
|
index = INVALID_BUF_INDEX;
|
|
break;
|
|
}
|
|
|
|
alloced = true;
|
|
}
|
|
}
|
|
|
|
*out_index = index;
|
|
}
|
|
|
|
/* Initializes crossfader, calculates all necessary parameters and performs
|
|
* fade-out with the PCM buffer. */
|
|
static void crossfade_start(void)
|
|
{
|
|
logf("crossfade_start");
|
|
|
|
pcm_play_lock();
|
|
|
|
if (crossfade_status == CROSSFADE_CONTINUE)
|
|
{
|
|
logf("fade-in continuing");
|
|
|
|
crossfade_status = CROSSFADE_ACTIVE;
|
|
|
|
if (crossfade_auto_skip)
|
|
pcmbuf_monitor_track_change_ex(crossfade_widx);
|
|
|
|
pcm_play_unlock();
|
|
return;
|
|
}
|
|
|
|
/* Initialize the crossfade buffer size to all of the buffered data that
|
|
* has not yet been sent to the DMA */
|
|
size_t unplayed = pcmbuf_unplayed_bytes();
|
|
|
|
/* Reject crossfade if less than .5s of data */
|
|
if (unplayed < DATA_LEVEL(2))
|
|
{
|
|
logf("crossfade rejected");
|
|
crossfade_cancel();
|
|
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 */
|
|
fade_out_delay -= MIN(fade_out_delay, fade_in_delay);
|
|
fade_in_delay = 0;
|
|
}
|
|
|
|
size_t fade_out_need = fade_out_delay + fade_out_rem;
|
|
|
|
if (!crossfade_mixmode)
|
|
{
|
|
/* Completely process the crossfade fade-out effect with current PCM buffer */
|
|
size_t buffer_rem;
|
|
size_t index = crossfade_find_buftail(crossfade_auto_skip, unplayed,
|
|
fade_out_need, &buffer_rem);
|
|
|
|
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_delay >= fade_out_short)
|
|
{
|
|
/* Truncate fade-out delay */
|
|
fade_out_delay -= fade_out_short;
|
|
}
|
|
else
|
|
{
|
|
/* Truncate fade-out and eliminate fade-out delay */
|
|
fade_out_rem = buffer_rem;
|
|
fade_out_delay = 0;
|
|
}
|
|
|
|
fade_out_need = fade_out_delay + fade_out_rem;
|
|
}
|
|
|
|
/* Find the right chunk and sample to start fading out */
|
|
index = crossfade_find_index(index, fade_out_delay);
|
|
|
|
/* Fade out the specified amount of the already processed audio */
|
|
struct mixfader outfader;
|
|
|
|
mixfader_init(&outfader, MIXFADE_UNITY, 0, fade_out_rem, false);
|
|
crossfade_mix_fade(&outfader, fade_out_rem, NULL, &index, 0,
|
|
MIXFADE_KEEP_POS);
|
|
|
|
/* Zero-out the rest of the buffer */
|
|
crossfade_mix_fade(&outfader, pcmbuf_size, NULL, &index, 0,
|
|
MIXFADE_NULLIFY_POS);
|
|
|
|
pcm_play_lock();
|
|
}
|
|
|
|
/* Initialize fade-in counters */
|
|
mixfader_init(&crossfade_infader, 0, MIXFADE_UNITY, fade_in_duration, true);
|
|
|
|
/* 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;
|
|
|
|
crossfade_widx = crossfade_find_buftail(crossfade_auto_skip, unplayed,
|
|
fade_out_need, NULL);
|
|
|
|
/* Move track transistion to chunk before the first one of incoming track */
|
|
if (crossfade_auto_skip)
|
|
pcmbuf_monitor_track_change_ex(crossfade_widx);
|
|
|
|
pcm_play_unlock();
|
|
|
|
logf("crossfade_start done!");
|
|
}
|
|
|
|
/* Perform fade-in of new track */
|
|
static void write_to_crossfade(size_t size, unsigned long elapsed, off_t offset)
|
|
{
|
|
/* Mix the data */
|
|
crossfade_mix_fade(&crossfade_infader, size, index_buffer(crossfade_bufidx),
|
|
&crossfade_widx, elapsed, offset);
|
|
|
|
/* If no more fading-in to do, stop the crossfade */
|
|
if (mixfader_finished(&crossfade_infader) &&
|
|
index_chunk_offs(crossfade_widx, 0) == chunk_widx)
|
|
{
|
|
crossfade_cancel();
|
|
}
|
|
}
|
|
|
|
static void pcmbuf_finish_crossfade_enable(void)
|
|
{
|
|
/* Copy the pending setting over now */
|
|
crossfade_setting = crossfade_enable_request;
|
|
|
|
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 */
|
|
PCMBUF_WATERMARK;
|
|
}
|
|
|
|
void pcmbuf_request_crossfade_enable(int setting)
|
|
{
|
|
/* Next setting to be used, not applied now */
|
|
crossfade_enable_request = setting;
|
|
}
|
|
|
|
bool pcmbuf_is_same_size(void)
|
|
{
|
|
/* 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)
|
|
pcmbuf_finish_crossfade_enable();
|
|
|
|
return same_size;
|
|
}
|
|
#endif /* HAVE_CROSSFADE */
|
|
|
|
|
|
/** Debug menu, other metrics */
|
|
|
|
/* Amount of bytes left in the buffer, accounting for uncommitted bytes */
|
|
size_t pcmbuf_free(void)
|
|
{
|
|
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)
|
|
{
|
|
return pcmbuf_unplayed_bytes() / PCMBUF_CHUNK_SIZE;
|
|
}
|
|
|
|
/* Total number of descriptors allocated */
|
|
int pcmbuf_descs(void)
|
|
{
|
|
return pcmbuf_desc_count;
|
|
}
|
|
|
|
|
|
/** Fading and channel volume control */
|
|
|
|
/* Sync the channel amplitude to all states */
|
|
static void pcmbuf_update_volume(void)
|
|
{
|
|
unsigned int vol = fade_vol;
|
|
|
|
if (soft_mode)
|
|
vol >>= 2;
|
|
|
|
mixer_channel_set_amplitude(PCM_MIXER_CHAN_PLAYBACK, vol);
|
|
}
|
|
|
|
/* Tick that does the fade for the playback channel */
|
|
static void pcmbuf_fade_tick(void)
|
|
{
|
|
/* ~1/3 second for full range fade */
|
|
const unsigned int fade_step = MIX_AMP_UNITY / (HZ / 3);
|
|
|
|
if (fade_state == PCM_FADING_IN)
|
|
fade_vol += MIN(fade_step, MIX_AMP_UNITY - fade_vol);
|
|
else if (fade_state == PCM_FADING_OUT)
|
|
fade_vol -= MIN(fade_step, fade_vol - MIX_AMP_MUTE);
|
|
|
|
pcmbuf_update_volume();
|
|
|
|
if (fade_vol == MIX_AMP_MUTE || fade_vol == MIX_AMP_UNITY)
|
|
{
|
|
/* Fade is complete */
|
|
tick_remove_task(pcmbuf_fade_tick);
|
|
if (fade_state == PCM_FADING_OUT)
|
|
{
|
|
/* Tell PCM to stop at its earliest convenience */
|
|
fade_out_complete = true;
|
|
}
|
|
|
|
fade_state = PCM_NOT_FADING;
|
|
}
|
|
}
|
|
|
|
/* Fade channel in or out in the background */
|
|
void pcmbuf_fade(bool fade, bool in)
|
|
{
|
|
/* Must pause any active fade */
|
|
pcm_play_lock();
|
|
|
|
if (fade_state != PCM_NOT_FADING)
|
|
tick_remove_task(pcmbuf_fade_tick);
|
|
|
|
fade_out_complete = false;
|
|
|
|
pcm_play_unlock();
|
|
|
|
if (!fade)
|
|
{
|
|
/* Simply set the level */
|
|
fade_state = PCM_NOT_FADING;
|
|
fade_vol = in ? MIX_AMP_UNITY : MIX_AMP_MUTE;
|
|
pcmbuf_update_volume();
|
|
}
|
|
else
|
|
{
|
|
/* Set direction and resume fade from current point */
|
|
fade_state = in ? PCM_FADING_IN : PCM_FADING_OUT;
|
|
tick_add_task(pcmbuf_fade_tick);
|
|
}
|
|
}
|
|
|
|
/* Return 'true' if fade is in progress */
|
|
bool pcmbuf_fading(void)
|
|
{
|
|
return fade_state != PCM_NOT_FADING;
|
|
}
|
|
|
|
/* Quiet-down the channel if 'shhh' is true or else play at normal level */
|
|
void pcmbuf_soft_mode(bool shhh)
|
|
{
|
|
/* Have to block the tick or improper order could leave volume in soft
|
|
mode if fading reads the old value first but updates after us. */
|
|
int res = fade_state != PCM_NOT_FADING ?
|
|
tick_remove_task(pcmbuf_fade_tick) : -1;
|
|
|
|
soft_mode = shhh;
|
|
pcmbuf_update_volume();
|
|
|
|
if (res == 0)
|
|
tick_add_task(pcmbuf_fade_tick);
|
|
}
|
|
|
|
|
|
/** Time and position */
|
|
|
|
/* Return the current position key value */
|
|
unsigned int pcmbuf_get_position_key(void)
|
|
{
|
|
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;
|
|
}
|
|
|
|
|
|
|
|
/** Misc */
|
|
|
|
bool pcmbuf_is_lowdata(void)
|
|
{
|
|
enum channel_status status = mixer_channel_status(PCM_MIXER_CHAN_PLAYBACK);
|
|
|
|
if (status != CHANNEL_PLAYING)
|
|
return false;
|
|
|
|
#ifdef HAVE_CROSSFADE
|
|
if (crossfade_status != CROSSFADE_INACTIVE)
|
|
return false;
|
|
#endif
|
|
|
|
return pcmbuf_data_critical();
|
|
}
|
|
|
|
void pcmbuf_set_low_latency(bool state)
|
|
{
|
|
low_latency_mode = state;
|
|
}
|
|
|
|
void pcmbuf_update_frequency(void)
|
|
{
|
|
pcmbuf_sampr = mixer_get_frequency();
|
|
}
|
|
|
|
unsigned int pcmbuf_get_frequency(void)
|
|
{
|
|
return pcmbuf_sampr;
|
|
}
|