rockbox/apps/recorder/pcm_record.c
Aidan MacDonald 525eb15864 recording: fix mono mode mixdown functions
Rewrite copy_buffer_mono_* functions for correctness.

Bad pointer arithmetic in copy_buffer_mono_l produced
wrong results, or panics on archs which can't handle
the unaligned pointer.

None of the functions handled zero size copies properly
though this probably wasn't an issue in practice.

Change-Id: I81c894e1b8a3440cb409092bec07fe3778a78959
2022-01-17 00:37:12 +00:00

2031 lines
54 KiB
C

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2005 Linus Nielsen Feltzing
* Copyright (C) 2006 Antonius Hellmann
* Copyright (C) 2006-2013 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 "config.h"
#include "system.h"
#include "kernel.h"
#include "panic.h"
#include "string-extra.h"
#include "pcm_record.h"
#include "codecs.h"
#include "logf.h"
#include "thread.h"
#include "storage.h"
#include "general.h"
#include "codec_thread.h"
#include "audio.h"
#include "sound.h"
#include "metadata.h"
#include "appevents.h"
#ifdef HAVE_SPDIF_IN
#include "spdif.h"
#endif
#include "audio_thread.h"
#include "core_alloc.h"
#include "talk.h"
/* Macros to enable logf for queues
logging on SYS_TIMEOUT can be disabled */
#ifdef SIMULATOR
/* Define this for logf output of all queuing except SYS_TIMEOUT */
#define PCMREC_LOGQUEUES
/* Define this to logf SYS_TIMEOUT messages */
/*#define PCMREC_LOGQUEUES_SYS_TIMEOUT*/
#endif /* SIMULATOR */
#ifdef PCMREC_LOGQUEUES
#define LOGFQUEUE logf
#else
#define LOGFQUEUE(...)
#endif
#ifdef PCMREC_LOGQUEUES_SYS_TIMEOUT
#define LOGFQUEUE_SYS_TIMEOUT logf
#else
#define LOGFQUEUE_SYS_TIMEOUT(...)
#endif
/** Target-related configuration **/
/**
* PCM_NUM_CHUNKS: Number of PCM chunks
* PCM_CHUNK_SAMP: Number of samples in a PCM chunk
* PCM_BOOST_SECONDS: PCM level at which to boost CPU
* PANIC_SECONDS: Flood watermark time until full
* FLUSH_SECONDS: Flush watermark time until full
* STREAM_BUF_SIZE: Size of stream write buffer
* PRIO_SECONDS: Max flush time before prio boost
*
* Total PCM buffer size should be mem aligned
*
* Fractions should be left without parentheses so the multiplier is
* multiplied by the numerator first.
*/
#if MEMORYSIZE <= 2
#define PCM_NUM_CHUNKS 56
#define PCM_CHUNK_SAMP 1024
#define PCM_BOOST_SECONDS 1/2
#define PANIC_SECONDS 1/2
#define FLUSH_SECONDS 1
#define FLUSH_MON_INTERVAL 1/6
#define STREAM_BUF_SIZE 32768
#elif MEMORYSIZE <= 16
#define PANIC_SECONDS 5
#define FLUSH_SECONDS 7
#else /* MEMORYSIZE > 16 */
#define PANIC_SECONDS 8
#define FLUSH_SECONDS 10
#endif /* MEMORYSIZE */
/* Default values if not overridden above */
#ifndef PCM_NUM_CHUNKS
#define PCM_NUM_CHUNKS 256
#endif
#ifndef PCM_CHUNK_SAMP
#define PCM_CHUNK_SAMP 2048
#endif
#ifndef PCM_BOOST_SECONDS
#define PCM_BOOST_SECONDS 1
#endif
#ifndef FLUSH_MON_INTERVAL
#define FLUSH_MON_INTERVAL 1/4
#endif
#ifndef STREAM_BUF_SIZE
#define STREAM_BUF_SIZE 65536
#endif
#ifndef PRIO_SECONDS
#define PRIO_SECONDS 10
#endif
/* FAT limit for filesize. Recording will accept no further data from the
* codec if this limit is reached in order to preserve its own data
* integrity. A split should have made by the higher-ups long before this
* point.
*
* Leave a generous 64k margin for metadata being added to file. */
#define MAX_NUM_REC_BYTES ((size_t)0x7fff0000u)
/***************************************************************************/
extern struct codec_api ci; /* in codec_thread.c */
extern struct event_queue audio_queue; /* in audio_thread.c */
extern unsigned int audio_thread_id; /* in audio_thread.c */
/** General recording state **/
/* Recording action being performed */
static enum record_status
{
RECORD_STOPPED = 0,
RECORD_PRERECORDING = AUDIO_STATUS_PRERECORD,
RECORD_RECORDING = AUDIO_STATUS_RECORD,
RECORD_PAUSED = (AUDIO_STATUS_RECORD | AUDIO_STATUS_PAUSE),
} record_status = RECORD_STOPPED;
/* State of engine operations */
static enum record_state
{
REC_STATE_IDLE, /* Stopped or prerecording */
REC_STATE_MONITOR, /* Monitoring buffer status */
REC_STATE_FLUSH, /* Flushing buffer */
} record_state = REC_STATE_IDLE;
static uint32_t errors; /* An error has occured (bitmask) */
static uint32_t warnings; /* Non-fatal warnings (bitmask) */
static uint32_t rec_errors; /* Mirror of errors but private to
* avoid race with controlling
* thread. Engine uses this
* internally. */
/** Stats on encoded data for current file **/
static int rec_fd = -1; /* Currently open file descriptor */
static size_t num_rec_bytes; /* Number of bytes recorded */
static uint64_t num_rec_samples; /* Number of PCM samples recorded */
static uint64_t encbuf_rec_count; /* Count of slots written to buffer
for current file */
/** These apply to current settings **/
static int rec_source; /* Current rec_source setting */
static unsigned long sample_rate; /* Samplerate setting in HZ */
static int num_channels; /* Current number of channels */
static struct encoder_config enc_config; /* Current encoder configuration */
static unsigned int pre_record_seconds; /* Pre-record time in seconds */
/****************************************************************************
Use 2 circular buffers:
pcm_buffer=DMA output buffer: chunks (8192 Bytes) of raw pcm audio data
enc_buffer=encoded audio buffer: storage for encoder output data
Flow:
1. When entering recording_screen DMA feeds the ringbuffer pcm_buffer
2. If enough pcm data are available the encoder codec does encoding of pcm
chunks (4-8192 Bytes) into ringbuffer enc_buffer in codec_thread
3. pcmrec_callback detects enc_buffer 'near full' and writes data to disk
Functions calls (basic encoder steps):
1.audio: codec_load(); load the encoder
2.encoder: enc_init_parameters(); set the encoder parameters (at load)
3.audio: enc_callback(); configure encoder recording settings
4.audio: codec_go(); start encoding the new stream
5.encoder: enc_encbuf_get_buffer(); obtain an output buffer of size n
6.encoder: enc_pcmbuf_read(); read n bytes of unprocessed pcm data
7.encoder: enc_encbuf_finish_buffer(); add the obtained buffer to output
8.encoder: enc_pcmbuf_advance(); advance pcm by n samples
9.encoder: while more PCM available, repeat 5. to 9.
10.audio: codec_finish_stream(); finish the output for current stream
Function calls (basic stream flushing steps through enc_callback()):
1.audio: flush_stream_start(); stream flush destination is opening
2.audio: flush_stream_data(); flush encoded audio to stream
3.audio: while encoded data available, repeat 2.
4.audio: flush_stream_end(); stream flush destination is closing
****************************************************************************/
/** Buffer parameters where incoming PCM data is placed **/
#define PCM_DEPTH_BYTES (sizeof (int16_t))
#define PCM_SAMP_SIZE (2*PCM_DEPTH_BYTES)
#define PCM_CHUNK_SIZE (PCM_CHUNK_SAMP*PCM_SAMP_SIZE)
#define PCM_BUF_SIZE (PCM_NUM_CHUNKS*PCM_CHUNK_SIZE)
/* Convert byte sizes into buffer slot counts */
#define CHUNK_SIZE_COUNT(size) \
(((size) + ENC_HDR_SIZE - 1) / ENC_HDR_SIZE)
#define CHUNK_FILE_COUNT(size) \
({ typeof (size) __size = (size); \
CHUNK_SIZE_COUNT(MIN(__size, MAX_PATH) + ENC_HDR_SIZE); })
#define CHUNK_FILE_COUNT_PATH(path) \
CHUNK_FILE_COUNT(strlen(path) + 1)
#define CHUNK_DATA_COUNT(size) \
CHUNK_SIZE_COUNT((size) + sizeof (struct enc_chunk_data))
/* Min margin to write stream split headers without overwrap risk */
#define ENCBUF_MIN_SPLIT_MARGIN \
(2*(1 + CHUNK_FILE_COUNT(MAX_PATH)) - 1)
static void *rec_buffer; /* Root rec buffer pointer */
static size_t rec_buffer_size; /* Root rec buffer size */
static void *pcm_buffer; /* Circular buffer for PCM samples */
static volatile bool pcm_pause; /* Freeze DMA write position */
static volatile size_t pcm_widx; /* Current DMA write position */
static volatile size_t pcm_ridx; /* Current PCM read position */
static union enc_chunk_hdr *enc_buffer; /* Circular encoding buffer */
static size_t enc_widx; /* Encoder chunk write index */
static size_t enc_ridx; /* Encoder chunk read index */
static size_t enc_buflen; /* Length of buffer in slots */
static unsigned char *stream_buffer; /* Stream-to-disk write buffer */
static ssize_t stream_buf_used; /* Stream write buffer occupancy */
static struct enc_chunk_file *fname_buf;/* Buffer with next file to create */
static unsigned long enc_sample_rate; /* Samplerate used by encoder */
static bool pcm_buffer_empty; /* All PCM chunks processed? */
static typeof (memcpy) *pcm_copyfn; /* PCM memcpy or copy_buffer_mono */
static enc_callback_t enc_cb; /* Encoder's recording callback */
/** File flushing **/
static unsigned long encbuf_datarate; /* Rate of data per second */
#if (CONFIG_STORAGE & STORAGE_ATA)
static int spinup_time; /* Last spinup time */
#endif
static size_t high_watermark; /* Max limit for data flush */
#ifdef HAVE_PRIORITY_SCHEDULING
static size_t flood_watermark; /* Max limit for thread prio boost */
static bool prio_boosted;
#endif
/** Stream marking **/
enum mark_stream_action
{
MARK_STREAM_END = 0x1, /* Mark end current stream */
MARK_STREAM_START = 0x2, /* Mark start of new stream */
MARK_STREAM_SPLIT = 0x3, /* Insert split; orr of above values */
MARK_STREAM_PRE = 0x4, /* Do prerecord data tally */
MARK_STREAM_START_PRE = MARK_STREAM_PRE | MARK_STREAM_START,
};
/***************************************************************************/
/* Buffer pointer (p) to PCM sample memory address */
static inline void * pcmbuf_ptr(size_t p)
{
return pcm_buffer + p;
}
/* Buffer pointer (p) plus value (v), wrapped if necessary */
static size_t pcmbuf_add(size_t p, size_t v)
{
size_t res = p + v;
if (res >= PCM_BUF_SIZE)
res -= PCM_BUF_SIZE;
return res;
}
/* Size of data in PCM buffer */
size_t pcmbuf_used(void)
{
size_t p1 = pcm_ridx;
size_t p2 = pcm_widx;
if (p1 > p2)
p2 += PCM_BUF_SIZE;
return p2 - p1;
}
/* Buffer pointer (p) to memory address of header */
static inline union enc_chunk_hdr * encbuf_ptr(size_t p)
{
return enc_buffer + p;
}
/* Buffer pointer (p) plus value (v), wrapped if necessary */
static size_t encbuf_add(size_t p, size_t v)
{
size_t res = p + v;
if (res >= enc_buflen)
res -= enc_buflen;
return res;
}
/* Number of free buffer slots */
static size_t encbuf_free(void)
{
size_t p1 = enc_ridx;
size_t p2 = enc_widx;
if (p2 >= p1)
p1 += enc_buflen;
return p1 - p2;
}
/* Number of used buffer slots */
static size_t encbuf_used(void)
{
size_t p1 = enc_ridx;
size_t p2 = enc_widx;
if (p1 > p2)
p2 += enc_buflen;
return p2 - p1;
}
/* Is the encoder buffer empty? */
static bool encbuf_empty(void)
{
return enc_ridx == enc_widx;
}
/* Buffer pointer (p) plus size (v), written to enc_widx, new widx
* zero-initialized */
static void encbuf_widx_advance(size_t widx, size_t v)
{
widx = encbuf_add(widx, v);
encbuf_ptr(widx)->zero = 0;
enc_widx = widx;
}
/* Buffer pointer (p) plus size of chunk at (p), wrapped to (0) if
* necessary.
*
* pout points to variable to receive increment result
*
* Returns NULL if it was a wrap marker */
static void * encbuf_read_ptr_incr(size_t p, size_t *pout)
{
union enc_chunk_hdr *hdr = encbuf_ptr(p);
size_t v;
switch (hdr->type)
{
case CHUNK_T_DATA:
v = CHUNK_DATA_COUNT(hdr->size);
break;
case CHUNK_T_STREAM_START:
v = hdr->size;
break;
case CHUNK_T_STREAM_END:
default:
v = 1;
break;
case CHUNK_T_WRAP:
/* Wrap markers are not returned but caller may have to know that
the index was changed since it impacts available space */
*pout = 0;
return NULL;
}
*pout = encbuf_add(p, v);
return hdr;
}
/* Buffer pointer (p) of contiguous free space (v), wrapped to (0) if
* necessary.
*
* pout points to variable to receive possible-adjusted p
*
* Returns header at (p) or wrapped header at (0) if wrap was
* required in order to provide contiguous space. Header is zero-
* initialized.
*
* Marks the wrap point if a wrap is required to make the allocation. */
static void * encbuf_get_write_ptr(size_t p, size_t v, size_t *pout)
{
union enc_chunk_hdr *hdr = encbuf_ptr(p);
if (p + v > enc_buflen)
{
hdr->type = CHUNK_T_WRAP; /* All other fields ignored */
p = 0;
hdr = encbuf_ptr(0);
}
*pout = p;
hdr->zero = 0;
return hdr;
}
/* Post a flush request to audio thread, if none is currently queued */
static void encbuf_request_flush(void)
{
if (!queue_peek_ex(&audio_queue, NULL, 0,
&(const long [2]){ Q_AUDIO_RECORD_FLUSH,
Q_AUDIO_RECORD_FLUSH }))
queue_post(&audio_queue, Q_AUDIO_RECORD_FLUSH, 0);
}
/* Set the error bits in (e): no lock */
static inline void set_error_bits(uint32_t e)
{
errors |= e;
rec_errors |= e;
}
/* Clear the error bits in (e): no lock */
static inline void clear_error_bits(uint32_t e)
{
errors &= ~e;
}
/* Set the error bits in (e) */
static void raise_error_status(uint32_t e)
{
pcm_rec_lock();
set_error_bits(e);
pcm_rec_unlock();
}
/* Clear the error bits in (e) */
static void clear_error_status(uint32_t e)
{
pcm_rec_lock();
clear_error_bits(e);
pcm_rec_unlock();
}
/* Set the warning bits in (w): no lock */
static inline void set_warning_bits(uint32_t w)
{
warnings |= w;
}
/* Clear the warning bits in (w): no lock */
static inline void clear_warning_bits(uint32_t w)
{
warnings &= ~w;
}
/* Set the warning bits in (w) */
static void raise_warning_status(uint32_t w)
{
pcm_rec_lock();
set_warning_bits(w);
pcm_rec_unlock();
}
/* Clear the warning bits in (w) */
static void clear_warning_status(uint32_t w)
{
pcm_rec_lock();
clear_warning_bits(w);
pcm_rec_unlock();
}
/* Callback for when more data is ready - called by DMA ISR */
static void pcm_rec_have_more(void **start, size_t *size)
{
size_t next_idx = pcm_widx;
if (!pcm_pause)
{
/* One empty chunk must remain after widx is advanced */
if (pcmbuf_used() <= PCM_BUF_SIZE - 2*PCM_CHUNK_SIZE)
next_idx = pcmbuf_add(next_idx, PCM_CHUNK_SIZE);
else
set_warning_bits(PCMREC_W_PCM_BUFFER_OVF);
}
*start = pcmbuf_ptr(next_idx);
*size = PCM_CHUNK_SIZE;
pcm_widx = next_idx;
}
static enum pcm_dma_status pcm_rec_status_callback(enum pcm_dma_status status)
{
if (status < PCM_DMAST_OK)
{
/* Some error condition */
if (status == PCM_DMAST_ERR_DMA)
{
set_error_bits(PCMREC_E_DMA);
return status;
}
else
{
/* Try again next transmission - frame is invalid */
set_warning_bits(PCMREC_W_DMA);
}
}
return PCM_DMAST_OK;
}
/* Start DMA transfer */
static void pcm_start_recording(void)
{
pcm_record_data(pcm_rec_have_more, pcm_rec_status_callback,
pcmbuf_ptr(pcm_widx), PCM_CHUNK_SIZE);
}
/* Initialize the various recording buffers */
static void init_rec_buffers(void)
{
/* Layout of recording buffer: |PCMBUF|STREAMBUF|FILENAME|ENCBUF| */
void *buf = rec_buffer;
size_t size = rec_buffer_size;
/* PCMBUF */
pcm_buffer = CACHEALIGN_UP(buf); /* Line align */
size -= pcm_buffer + PCM_BUF_SIZE - buf;
buf = pcm_buffer + PCM_BUF_SIZE;
/* STREAMBUF */
stream_buffer = buf; /* Also line-aligned */
buf += STREAM_BUF_SIZE;
size -= STREAM_BUF_SIZE;
/* FILENAME */
fname_buf = buf;
buf += CHUNK_FILE_COUNT(MAX_PATH)*ENC_HDR_SIZE;
size -= CHUNK_FILE_COUNT(MAX_PATH)*ENC_HDR_SIZE;
fname_buf->hdr.zero = 0;
/* ENCBUF */
enc_buffer = buf;
enc_buflen = size;
ALIGN_BUFFER(enc_buffer, enc_buflen, ENC_HDR_SIZE);
enc_buflen = CHUNK_SIZE_COUNT(enc_buflen);
}
/* Reset the circular buffers */
static void reset_fifos(bool hard)
{
/* PCM FIFO */
pcm_pause = true;
if (hard)
pcm_widx = 0; /* Don't just empty but reset it */
pcm_ridx = pcm_widx;
/* Encoder FIFO */
encbuf_widx_advance(0, 0);
enc_ridx = 0;
/* No overflow-related warnings now */
clear_warning_status(PCMREC_W_PCM_BUFFER_OVF | PCMREC_W_ENC_BUFFER_OVF);
}
/* Initialize file statistics */
static void reset_rec_stats(void)
{
num_rec_bytes = 0;
num_rec_samples = 0;
encbuf_rec_count = 0;
clear_warning_status(PCMREC_W_FILE_SIZE);
}
/* Boost or unboost recording threads' priorities */
static void do_prio_boost(bool boost)
{
#ifdef HAVE_PRIORITY_SCHEDULING
prio_boosted = boost;
int prio = PRIORITY_RECORDING;
if (boost)
prio -= 4;
codec_thread_set_priority(prio);
thread_set_priority(audio_thread_id, prio);
#endif
(void)boost;
}
/* Reset all relevant state */
static void init_state(void)
{
reset_fifos(true);
reset_rec_stats();
do_prio_boost(false);
cancel_cpu_boost();
record_state = REC_STATE_IDLE;
record_status = RECORD_STOPPED;
}
/* Set hardware samplerate and save it */
static void update_samplerate_config(unsigned long sampr)
{
/* PCM samplerate is either the same as the setting or the nearest
one hardware supports if using S/PDIF */
unsigned long pcm_sampr = sampr;
#ifdef HAVE_SPDIF_IN
if (rec_source == AUDIO_SRC_SPDIF)
{
int index = round_value_to_list32(sampr, hw_freq_sampr,
HW_NUM_FREQ, false);
pcm_sampr = hw_freq_sampr[index];
}
#endif /* HAVE_SPDIF_IN */
pcm_set_frequency(pcm_sampr | SAMPR_TYPE_REC);
sample_rate = sampr;
}
/* Calculate the average data rate */
static unsigned long get_encbuf_datarate(void)
{
/* If not yet calculable, start with uncompressed PCM byterate */
if (num_rec_samples && sample_rate && encbuf_rec_count)
{
return (encbuf_rec_count*sample_rate + num_rec_samples - 1)
/ num_rec_samples;
}
else
{
return CHUNK_SIZE_COUNT(sample_rate*num_channels*PCM_DEPTH_BYTES);
}
}
/* Returns true if the watermarks should be updated due to data rate
change */
static bool monitor_encbuf_datarate(void)
{
unsigned long rate = get_encbuf_datarate();
long diff = rate - encbuf_datarate;
/* Off by more than 1/2 FLUSH_MON_INTERVAL? */
return 2*(unsigned long)abs(diff) > encbuf_datarate*FLUSH_MON_INTERVAL;
}
/* Get adjusted spinup time */
static int get_spinup_time(void)
{
int spin = storage_spinup_time();
#if (CONFIG_STORAGE & STORAGE_ATA)
/* Write at FLUSH_SECONDS + st remaining in enc_buffer - range fs+2s to
fs+10s total - default to 3.5s spinup. */
if (spin == 0)
spin = 35*HZ/10; /* default - cozy */
else if (spin < 2*HZ)
spin = 2*HZ; /* ludicrous - ramdisk? */
else if (spin > 10*HZ)
spin = 10*HZ; /* do you have a functioning HD? */
#endif /* (CONFIG_STORAGE & STORAGE_ATA) */
return spin;
}
/* Returns true if the watermarks should be updated due to spinup time
change */
static inline bool monitor_spinup_time(void)
{
#if (CONFIG_STORAGE & STORAGE_ATA)
return get_spinup_time() != spinup_time;
#else
return false;
#endif
}
/* Update buffer watermarks with spinup time compensation */
static void refresh_watermarks(void)
{
int spin = get_spinup_time();
#if (CONFIG_STORAGE & STORAGE_ATA)
logf("ata spinup: %d", spin);
spinup_time = spin;
#endif
unsigned long rate = get_encbuf_datarate();
logf("byterate: %lu", rate * ENC_HDR_SIZE);
encbuf_datarate = rate;
/* Try to start writing with FLUSH_SECONDS remaining after disk spinup */
high_watermark = (uint64_t)rate*(FLUSH_SECONDS*HZ + spin) / HZ;
if (high_watermark > enc_buflen)
high_watermark = enc_buflen;
high_watermark = enc_buflen - high_watermark;
logf("high wm: %lu", (unsigned long)high_watermark);
#ifdef HAVE_PRIORITY_SCHEDULING
/* Boost thread priority if enough ground is lost since flushing started
or is taking an unreasonably long time */
flood_watermark = rate*PANIC_SECONDS;
if (flood_watermark > enc_buflen)
flood_watermark = enc_buflen;
flood_watermark = enc_buflen - flood_watermark;
logf("flood wm: %lu", (unsigned long)flood_watermark);
#endif /* HAVE_PRIORITY_SCHEDULING */
}
/* Tell encoder the stream parameters and get information back */
static bool configure_encoder_stream(void)
{
struct enc_inputs inputs;
inputs.sample_rate = sample_rate;
inputs.num_channels = num_channels;
inputs.config = &enc_config;
/* encoder can change these - init with defaults */
inputs.enc_sample_rate = sample_rate;
if (enc_cb(ENC_CB_INPUTS, &inputs) < 0)
{
raise_error_status(PCMREC_E_ENC_SETUP);
return false;
}
enc_sample_rate = inputs.enc_sample_rate;
if (enc_sample_rate != sample_rate)
{
/* Codec doesn't want to/can't use the setting and has chosen a
different sample rate */
raise_warning_status(PCMREC_W_SAMPR_MISMATCH);
logf("enc sampr:%lu", enc_sample_rate);
}
else
{
clear_warning_status(PCMREC_W_SAMPR_MISMATCH);
}
refresh_watermarks();
return true;
}
#ifdef HAVE_SPDIF_IN
/* Return the S/PDIF sample rate closest to a value in the master list */
static unsigned long get_spdif_samplerate(void)
{
unsigned long sr = spdif_measure_frequency();
int index = round_value_to_list32(sr, audio_master_sampr_list,
SAMPR_NUM_FREQ, false);
return audio_master_sampr_list[index];
}
/* Check the S/PDIF rate and compare to current setting. Apply the new
* rate if it changed. */
static void check_spdif_samplerate(void)
{
unsigned long sampr = get_spdif_samplerate();
if (sampr == sample_rate)
return;
codec_stop();
pcm_stop_recording();
reset_fifos(true);
reset_rec_stats();
update_samplerate_config(sampr);
pcm_apply_settings();
if (!configure_encoder_stream() || rec_errors)
return;
pcm_start_recording();
if (record_status == RECORD_PRERECORDING)
{
codec_go();
pcm_pause = false;
}
}
#endif /* HAVE_SPDIF_IN */
/* Discard the stream buffer contents */
static inline void stream_discard_buf(void)
{
stream_buf_used = 0;
}
/* Flush stream buffer to disk */
static bool stream_flush_buf(void)
{
if (stream_buf_used == 0)
return true;
ssize_t rc = write(rec_fd, stream_buffer, stream_buf_used);
if (LIKELY(rc == stream_buf_used))
{
stream_discard_buf();
return true;
}
if (rc > 0)
{
/* Some was written; keep in sync */
stream_buf_used -= rc;
memmove(stream_buffer, stream_buffer + rc, stream_buf_used);
}
return false;
}
/* Close the output file */
static void close_rec_file(void)
{
if (rec_fd < 0)
return;
bool ok = stream_flush_buf();
if (close(rec_fd) != 0 || !ok)
raise_error_status(PCMREC_E_IO);
rec_fd = -1;
}
/* Creates or opens the current path */
static bool open_rec_file(bool create)
{
if (rec_fd >= 0)
{
/* Any previous file should have been closed */
logf("open file: file already open");
close_rec_file();
}
stream_discard_buf();
int oflags = create ? O_CREAT|O_TRUNC : 0;
rec_fd = open(fname_buf->path, O_RDWR|oflags, 0666);
if (rec_fd < 0)
{
raise_error_status(PCMREC_E_IO);
return false;
}
return true;
}
/* Copy with mono conversion - output 1/2 size of input */
static void * ICODE_ATTR
copy_buffer_mono_lr(void *dst, const void *src, size_t src_size)
{
int16_t *d = (int16_t*) dst;
int16_t const *s = (int16_t const*) src;
ssize_t copy_size = src_size;
/* mono = (L + R) / 2 */
while(copy_size > 0) {
*d++ = ((int32_t)s[0] + (int32_t)s[1] + 1) >> 1;
s += 2;
copy_size -= PCM_SAMP_SIZE;
}
return dst;
}
static void * ICODE_ATTR
copy_buffer_mono_l(void *dst, const void *src, size_t src_size)
{
int16_t *d = (int16_t*) dst;
int16_t const *s = (int16_t const*) src;
ssize_t copy_size = src_size;
/* mono = L */
while(copy_size > 0) {
*d++ = *s;
s += 2;
copy_size -= PCM_SAMP_SIZE;
}
return dst;
}
static void * ICODE_ATTR
copy_buffer_mono_r(void *dst, const void *src, size_t src_size)
{
return copy_buffer_mono_l(dst, src + 2, src_size);
}
/** pcm_rec_* group **/
/* Clear all errors and warnings */
void pcm_rec_error_clear(void)
{
clear_error_status(PCMREC_E_ALL);
clear_warning_status(PCMREC_W_ALL);
}
/* Check mode, errors and warnings */
unsigned int pcm_rec_status(void)
{
unsigned int ret = record_status;
if (errors)
ret |= AUDIO_STATUS_ERROR;
if (warnings)
ret |= AUDIO_STATUS_WARNING;
return ret;
}
/* Return warnings that have occured since recording started */
uint32_t pcm_rec_get_warnings(void)
{
return warnings;
}
#ifdef HAVE_SPDIF_IN
/* Return the currently-configured sample rate */
unsigned long pcm_rec_sample_rate(void)
{
return sample_rate;
}
#endif
/** audio_* group **/
/* Initializes recording - call before calling any other recording function */
void audio_init_recording(void)
{
LOGFQUEUE("audio >| pcmrec Q_AUDIO_INIT_RECORDING");
audio_queue_send(Q_AUDIO_INIT_RECORDING, 1);
}
/* Closes recording - call audio_stop_recording first or risk data loss */
void audio_close_recording(void)
{
LOGFQUEUE("audio >| pcmrec Q_AUDIO_CLOSE_RECORDING");
audio_queue_send(Q_AUDIO_CLOSE_RECORDING, 0);
}
/* Sets recording parameters */
void audio_set_recording_options(struct audio_recording_options *options)
{
LOGFQUEUE("audio >| pcmrec Q_AUDIO_RECORDING_OPTIONS");
audio_queue_send(Q_AUDIO_RECORDING_OPTIONS, (intptr_t)options);
}
/* Start recording if not recording or else split */
void audio_record(const char *filename)
{
LOGFQUEUE("audio >| pcmrec Q_AUDIO_RECORD: %s", filename);
audio_queue_send(Q_AUDIO_RECORD, (intptr_t)filename);
}
/* audio_record alias for API compatibility with HW codec */
void audio_new_file(const char *filename)
__attribute__((alias("audio_record")));
/* Stop current recording if recording */
void audio_stop_recording(void)
{
LOGFQUEUE("audio > pcmrec Q_AUDIO_RECORD_STOP");
audio_queue_post(Q_AUDIO_RECORD_STOP, 0);
}
/* Pause current recording */
void audio_pause_recording(void)
{
LOGFQUEUE("audio > pcmrec Q_AUDIO_RECORD_PAUSE");
audio_queue_post(Q_AUDIO_RECORD_PAUSE, 0);
}
/* Resume current recording if paused */
void audio_resume_recording(void)
{
LOGFQUEUE("audio > pcmrec Q_AUDIO_RECORD_RESUME");
audio_queue_post(Q_AUDIO_RECORD_RESUME, 0);
}
/* Set the input source gain. For mono sources, only left gain is used */
void audio_set_recording_gain(int left, int right, int type)
{
#if 0
logf("pcmrec: t=%d l=%d r=%d", type, left, right);
#endif
audiohw_set_recvol(left, right, type);
}
/** Information about current state **/
/* Return sample clock in HZ */
static unsigned long get_samples_time(void)
{
if (enc_sample_rate == 0)
return 0;
return (unsigned long)(HZ*num_rec_samples / enc_sample_rate);
}
/* Return current prerecorded time in ticks (playback equivalent time) */
unsigned long audio_prerecorded_time(void)
{
if (record_status != RECORD_PRERECORDING)
return 0;
unsigned long t = get_samples_time();
return MIN(t, pre_record_seconds*HZ);
}
/* Return current recorded time in ticks (playback equivalent time) */
unsigned long audio_recorded_time(void)
{
if (record_state == REC_STATE_IDLE)
return 0;
return get_samples_time();
}
/* Return number of bytes encoded to output */
unsigned long audio_num_recorded_bytes(void)
{
if (record_state == REC_STATE_IDLE)
return 0;
return num_rec_bytes;
}
/** Data Flushing **/
/* Stream start chunk with path was encountered */
static void flush_stream_start(struct enc_chunk_file *file)
{
/* Save filename; don't open file here which avoids creating files
with no audio content. Splitting while paused can create those
in large numbers. */
fname_buf->hdr = file->hdr;
/* Correct size if this was wrap-padded */
fname_buf->hdr.size = CHUNK_FILE_COUNT(
strlcpy(fname_buf->path, file->path, MAX_PATH) + 1);
}
/* Data chunk was encountered */
static bool flush_stream_data(struct enc_chunk_data *data)
{
if (fname_buf->hdr.zero)
{
/* First data chunk; create the file */
if (open_rec_file(true))
{
/* Inherit some flags from initial data chunk */
fname_buf->hdr.err = data->hdr.err;
fname_buf->hdr.pre = data->hdr.pre;
fname_buf->hdr.aux0 = data->hdr.aux0;
if (enc_cb(ENC_CB_STREAM, fname_buf) < 0)
raise_error_status(PCMREC_E_ENCODER_STREAM);
}
fname_buf->hdr.zero = 0;
if (rec_errors)
return false;
}
if (rec_fd < 0)
return true; /* Just keep discarding */
if (enc_cb(ENC_CB_STREAM, data) < 0)
{
raise_error_status(PCMREC_E_ENCODER_STREAM);
return false;
}
return true;
}
/* Stream end chunk was encountered */
static bool flush_stream_end(union enc_chunk_hdr *hdr)
{
if (rec_fd < 0)
return true;
if (enc_cb(ENC_CB_STREAM, hdr) < 0)
{
raise_error_status(PCMREC_E_ENCODER_STREAM);
return false;
}
close_rec_file();
return true;
}
/* Discard remainder of stream in encoder buffer */
static void discard_stream(void)
{
/* Discard everything up until the next non-data chunk */
while (!encbuf_empty())
{
size_t ridx;
union enc_chunk_hdr *hdr = encbuf_read_ptr_incr(enc_ridx, &ridx);
if (hdr && hdr->type != CHUNK_T_DATA)
{
if (hdr->type != CHUNK_T_STREAM_START)
enc_ridx = ridx;
break;
}
enc_ridx = ridx;
}
/* Try to finish header by closing and reopening the file. A seek or
other operation will likely fail because buffers will need to be
flushed (here and in file code). That will likely fail but a close
will just close the fd and discard everything. We reopen with what
actually made it to disk. Modifying existing file contents will
more than likely succeed even on a full disk. The result might not
be entirely correct as far as the headers' sizes and counts unless
the codec can correct that but the sample format information
should be. */
if (rec_fd >= 0 && open_rec_file(false))
{
/* Synthesize a special end chunk here */
union enc_chunk_hdr end;
end.zero = 0;
end.err = 1; /* Codec should try to correct anything that's off */
end.type = CHUNK_T_STREAM_END;
if (!flush_stream_end(&end))
close_rec_file();
}
}
/* Flush a chunk to disk
*
* Transitions state from REC_STATE_MONITOR to REC_STATE_FLUSH when buffer
* is filling. 'margin' is fullness threshold that transitions to flush state.
*
* Call with REC_STATE_IDLE to indicate a forced flush which flushes buffer
* to less than 'margin'.
*/
static enum record_state flush_chunk(enum record_state state, size_t margin)
{
#ifdef HAVE_PRIORITY_SCHEDULING
static unsigned long prio_tick; /* Timeout for auto boost */
#endif
size_t used = encbuf_used();
switch (state)
{
case REC_STATE_MONITOR:
if (monitor_encbuf_datarate() || monitor_spinup_time())
refresh_watermarks();
if (used < margin)
return REC_STATE_MONITOR;
state = REC_STATE_FLUSH;
trigger_cpu_boost();
#ifdef HAVE_PRIORITY_SCHEDULING
prio_tick = current_tick + PRIO_SECONDS*HZ;
#if (CONFIG_STORAGE & STORAGE_ATA)
prio_tick += spinup_time;
#endif
#endif /* HAVE_PRIORITY_SCHEDULING */
/* Fall-through */
case REC_STATE_IDLE: /* As a hint for "forced" */
if (used < margin)
break;
/* Fall-through */
case REC_STATE_FLUSH:
#ifdef HAVE_PRIORITY_SCHEDULING
if (!prio_boosted && state != REC_STATE_IDLE &&
(used >= flood_watermark || TIME_AFTER(current_tick, prio_tick)))
do_prio_boost(true);
#endif /* HAVE_PRIORITY_SCHEDULING */
while (used)
{
union enc_chunk_hdr *hdr = encbuf_ptr(enc_ridx);
size_t count = 0;
switch (hdr->type)
{
case CHUNK_T_DATA:
if (flush_stream_data(ENC_DATA_HDR(hdr)))
count = CHUNK_DATA_COUNT(hdr->size);
break;
case CHUNK_T_STREAM_START:
/* Doesn't do stream writes */
flush_stream_start(ENC_FILE_HDR(hdr));
count = hdr->size;
break;
case CHUNK_T_STREAM_END:
if (flush_stream_end(hdr))
count = 1;
break;
case CHUNK_T_WRAP:
enc_ridx = 0;
used = encbuf_used();
continue;
}
if (count)
enc_ridx = encbuf_add(enc_ridx, count);
else
discard_stream();
break;
}
if (!encbuf_empty())
return state;
break;
}
if (encbuf_empty())
{
do_prio_boost(false);
cancel_cpu_boost();
}
return REC_STATE_MONITOR;
}
/* Monitor buffer and finish stream, freeing-up space at the same time */
static void finish_stream(bool stopping)
{
size_t threshold = stopping ? 1 : enc_buflen - ENCBUF_MIN_SPLIT_MARGIN;
enum record_state state = REC_STATE_MONITOR;
size_t need = 1;
while (1)
{
switch (state)
{
case REC_STATE_IDLE:
state = flush_chunk(state, threshold);
continue;
default:
if (!need)
break;
if (!stopping || pcm_buffer_empty)
{
need = codec_finish_stream();
if (need)
{
need = 2*CHUNK_DATA_COUNT(need) - 1;
if (need >= enc_buflen)
{
need = 0;
codec_stop();
threshold = 1;
}
else if (threshold > enc_buflen - need)
{
threshold = enc_buflen - need;
}
}
}
if (!need || encbuf_used() >= threshold)
state = REC_STATE_IDLE; /* Start flush */
else
sleep(HZ/10); /* Don't flood with pings */
continue;
}
break;
}
}
/* Start a new stream, transistion to a new one or end the current one */
static void mark_stream(const char *path, enum mark_stream_action action)
{
if (action & MARK_STREAM_END)
{
size_t widx;
union enc_chunk_hdr *hdr = encbuf_get_write_ptr(enc_widx, 1, &widx);
hdr->type = CHUNK_T_STREAM_END;
encbuf_widx_advance(widx, 1);
}
if (action & MARK_STREAM_START)
{
size_t count = CHUNK_FILE_COUNT_PATH(path);
struct enc_chunk_file *file;
size_t widx;
if (action & MARK_STREAM_PRE)
{
/* Prerecord: START marker goes first or before existing data */
if (enc_ridx < count)
{
/* Adjust to occupy end of buffer and pad accordingly */
count += enc_ridx;
enc_ridx += enc_buflen;
}
enc_ridx -= count;
/* Won't adjust p since enc_ridx is already set as non-wrapping */
file = encbuf_get_write_ptr(enc_ridx, count, &widx);
}
else
{
/* The usual: START marker goes first or after existing data */
file = encbuf_get_write_ptr(enc_widx, count, &widx);
encbuf_widx_advance(widx, count);
}
file->hdr.type = CHUNK_T_STREAM_START;
file->hdr.size = count;
strlcpy(file->path, path, MAX_PATH);
}
}
/* Tally-up and keep the required amount of prerecord data.
* Updates record stats accordingly. */
static void tally_prerecord_data(void)
{
unsigned long count = 0;
size_t bytes = 0;
unsigned long samples = 0;
/* Find out how much is there */
for (size_t idx = enc_ridx; idx != enc_widx;)
{
struct enc_chunk_data *data = encbuf_read_ptr_incr(idx, &idx);
if (!data)
continue;
count += CHUNK_DATA_COUNT(data->hdr.size);
bytes += data->hdr.size;
samples += data->pcm_count;
}
/* Have too much? Discard oldest data. */
unsigned long pre_samples = enc_sample_rate*pre_record_seconds;
while (samples > pre_samples)
{
struct enc_chunk_data *data =
encbuf_read_ptr_incr(enc_ridx, &enc_ridx);
if (!data)
continue;
count -= CHUNK_DATA_COUNT(data->hdr.size);
bytes -= data->hdr.size;
samples -= data->pcm_count;
}
encbuf_rec_count = count;
num_rec_bytes = bytes;
num_rec_samples = samples;
}
/** Event handlers for recording thread **/
static int pcmrec_handle;
/* Q_AUDIO_INIT_RECORDING */
static void on_init_recording(void)
{
send_event(RECORDING_EVENT_START, NULL);
/* dummy ops with no callbacks, needed because by
* default buflib buffers can be moved around which must be avoided
* FIXME: This buffer should play nicer and be shrinkable/movable */
static struct buflib_callbacks dummy_ops;
talk_buffer_set_policy(TALK_BUFFER_LOOSE);
pcmrec_handle = core_alloc_maximum("pcmrec", &rec_buffer_size, &dummy_ops);
if (pcmrec_handle <= 0)
/* someone is abusing core_alloc_maximum(). Fix this evil guy instead of
* trying to handle OOM without hope */
panicf("%s(): OOM\n", __func__);
rec_buffer = core_get_data(pcmrec_handle);
init_rec_buffers();
init_state();
pcm_init_recording();
}
/* Q_AUDIO_CLOSE_RECORDING */
static void on_close_recording(void)
{
/* Simply shut down the recording system. Whatever wasn't saved is
lost. */
codec_unload();
pcm_close_recording();
close_rec_file();
init_state();
rec_errors = 0;
pcm_rec_error_clear();
/* Reset PCM to defaults */
pcm_set_frequency(HW_SAMPR_RESET | SAMPR_TYPE_REC);
audio_set_output_source(AUDIO_SRC_PLAYBACK);
pcm_apply_settings();
if (pcmrec_handle > 0)
pcmrec_handle = core_free(pcmrec_handle);
talk_buffer_set_policy(TALK_BUFFER_DEFAULT);
send_event(RECORDING_EVENT_STOP, NULL);
}
/* Q_AUDIO_RECORDING_OPTIONS */
static void on_recording_options(struct audio_recording_options *options)
{
if (!options)
{
logf("options: option NULL!");
return;
}
if (record_state != REC_STATE_IDLE)
{
/* This would ruin things */
logf("options: still recording!");
return;
}
/* Stop everything else that might be running */
pcm_stop_recording();
int afmt = rec_format_afmt[options->enc_config.rec_format];
bool enc_load = true;
if (codec_loaded() != AFMT_UNKNOWN)
{
if (get_audio_base_codec_type(enc_config.afmt) !=
get_audio_base_codec_type(afmt))
{
/* New format, new encoder; unload this one */
codec_unload();
}
else
{
/* Keep current encoder */
codec_stop();
enc_load = false;
}
}
init_state();
/* Read recording options, remember the ones used elsewhere */
unsigned frequency = options->rec_frequency;
rec_source = options->rec_source;
num_channels = options->rec_channels == 1 ? 1 : 2;
unsigned mono_mode = options->rec_mono_mode;
pre_record_seconds = options->rec_prerecord_time;
enc_config = options->enc_config;
enc_config.afmt = afmt;
queue_reply(&audio_queue, 0); /* Let caller go */
/* Pick appropriate PCM copy routine */
pcm_copyfn = memcpy;
if (num_channels == 1)
{
static typeof (memcpy) * const copy_buffer_mono[] =
{
copy_buffer_mono_lr,
copy_buffer_mono_l,
copy_buffer_mono_r
};
if (mono_mode >= ARRAYLEN(copy_buffer_mono))
mono_mode = 0;
pcm_copyfn = copy_buffer_mono[mono_mode];
}
/* Get the hardware samplerate to be used */
unsigned long sampr;
#ifdef HAVE_SPDIF_IN
if (rec_source == AUDIO_SRC_SPDIF)
sampr = get_spdif_samplerate(); /* Determined by source */
else
#endif /* HAVE_SPDIF_IN */
sampr = rec_freq_sampr[frequency];
update_samplerate_config(sampr);
/* Set monitoring */
audio_set_output_source(rec_source);
/* Apply hardware setting to start monitoring now */
pcm_apply_settings();
if (!enc_load || codec_load(-1, afmt | CODEC_TYPE_ENCODER))
{
enc_cb = codec_get_enc_callback();
if (!enc_cb || !configure_encoder_stream())
{
codec_unload();
return;
}
if (pre_record_seconds != 0)
{
record_status = RECORD_PRERECORDING;
codec_go();
pcm_pause = false;
}
pcm_start_recording();
}
else
{
logf("set rec opt: enc load failed");
raise_error_status(PCMREC_E_LOAD_ENCODER);
}
}
/* Q_AUDIO_RECORD - start recording (not gapless)
or split stream (gapless) */
static void on_record(const char *filename)
{
if (rec_errors)
{
logf("on_record: errors not cleared");
return;
}
if (!filename)
{
logf("on_record: No filename");
return;
}
if (codec_loaded() == AFMT_UNKNOWN)
{
logf("on_record: Recording options not set");
return;
}
logf("on_record: new file '%s'", filename);
/* Copy path and let caller go */
char path[MAX_PATH];
strlcpy(path, filename, MAX_PATH);
queue_reply(&audio_queue, 0);
enum mark_stream_action mark_action;
if (record_state == REC_STATE_IDLE)
{
mark_action = MARK_STREAM_START;
if (pre_record_seconds)
{
codec_pause();
tally_prerecord_data();
mark_action = MARK_STREAM_START_PRE;
}
clear_warning_status(PCMREC_W_ALL &
~(PCMREC_W_SAMPR_MISMATCH|PCMREC_W_DMA));
record_state = REC_STATE_MONITOR;
record_status = RECORD_RECORDING;
}
else
{
/* Already recording, just split the stream */
logf("inserting split");
mark_action = MARK_STREAM_SPLIT;
finish_stream(false);
reset_rec_stats();
}
if (rec_errors)
{
pcm_pause = true;
codec_stop();
reset_fifos(false);
return;
}
mark_stream(path, mark_action);
codec_go();
pcm_pause = record_status != RECORD_RECORDING;
}
/* Q_AUDIO_RECORD_STOP */
static void on_record_stop(void)
{
if (record_state == REC_STATE_IDLE)
return;
trigger_cpu_boost();
/* Drain encoder and PCM buffers */
pcm_pause = true;
finish_stream(true);
/* End stream at last data and flush end marker */
mark_stream(NULL, MARK_STREAM_END);
while (flush_chunk(REC_STATE_IDLE, 1) == REC_STATE_IDLE);
reset_fifos(false);
bool prerecord = pre_record_seconds != 0;
if (rec_errors)
{
codec_stop();
prerecord = false;
}
close_rec_file();
rec_errors = 0;
record_state = REC_STATE_IDLE;
record_status = prerecord ? RECORD_PRERECORDING : RECORD_STOPPED;
reset_rec_stats();
if (prerecord)
{
codec_go();
pcm_pause = false;
}
}
/* Q_AUDIO_RECORD_PAUSE */
static void on_record_pause(void)
{
if (record_status != RECORD_RECORDING)
return;
pcm_pause = true;
record_status = RECORD_PAUSED;
}
/* Q_AUDIO_RECORD_RESUME */
static void on_record_resume(void)
{
if (record_status != RECORD_PAUSED)
return;
record_status = RECORD_RECORDING;
pcm_pause = !!rec_errors;
}
/* Called by audio thread when recording is initialized */
void audio_recording_handler(struct queue_event *ev)
{
#ifdef HAVE_PRIORITY_SCHEDULING
/* Get current priorities since they get changed */
int old_prio = thread_get_priority(audio_thread_id);
int old_cod_prio = codec_thread_get_priority();
#endif
LOGFQUEUE("record < Q_AUDIO_INIT_RECORDING");
on_init_recording();
while (1)
{
int watermark = high_watermark;
switch (ev->id)
{
case Q_AUDIO_CLOSE_RECORDING:
LOGFQUEUE("record < Q_AUDIO_CLOSE_RECORDING");
goto recording_done;
case Q_AUDIO_RECORDING_OPTIONS:
LOGFQUEUE("record < Q_AUDIO_RECORDING_OPTIONS");
on_recording_options((struct audio_recording_options *)ev->data);
break;
case Q_AUDIO_RECORD:
LOGFQUEUE("record < Q_AUDIO_RECORD: %s", (const char *)ev->data);
on_record((const char *)ev->data);
break;
case Q_AUDIO_RECORD_STOP:
LOGFQUEUE("record < Q_AUDIO_RECORD_STOP");
on_record_stop();
break;
case Q_AUDIO_RECORD_PAUSE:
LOGFQUEUE("record < Q_AUDIO_RECORD_PAUSE");
on_record_pause();
break;
case Q_AUDIO_RECORD_RESUME:
LOGFQUEUE("record < Q_AUDIO_RECORD_RESUME");
on_record_resume();
break;
case Q_AUDIO_RECORD_FLUSH:
watermark = 1;
break;
case SYS_USB_CONNECTED:
LOGFQUEUE("record < SYS_USB_CONNECTED");
if (record_state != REC_STATE_IDLE)
{
LOGFQUEUE(" still recording");
break;
}
goto recording_done;
} /* switch */
int timeout;
switch (record_state)
{
case REC_STATE_FLUSH:
case REC_STATE_MONITOR:
do
record_state = flush_chunk(record_state, watermark);
while (record_state == REC_STATE_FLUSH &&
queue_empty(&audio_queue));
timeout = record_state == REC_STATE_FLUSH ?
HZ*0 : HZ*FLUSH_MON_INTERVAL;
break;
case REC_STATE_IDLE:
#ifdef HAVE_SPDIF_IN
if (rec_source == AUDIO_SRC_SPDIF)
{
check_spdif_samplerate();
timeout = HZ/2;
break;
}
#endif /* HAVE_SPDIF_IN */
default:
timeout = TIMEOUT_BLOCK;
break;
}
queue_wait_w_tmo(&audio_queue, ev, timeout);
} /* while */
recording_done:
on_close_recording();
#ifdef HAVE_PRIORITY_SCHEDULING
/* Restore normal thread priorities */
thread_set_priority(audio_thread_id, old_prio);
codec_thread_set_priority(old_cod_prio);
#endif
}
/** Encoder callbacks **/
/* Read a block of unprocessed PCM data, with mono conversion if
* num_channels == 1
*
* NOTE: Request must be less than the PCM buffer length in samples in order
* to progress.
* (ie. count <= PCM_NUM_CHUNKS*PCM_CHUNK_SAMP)
*/
static int enc_pcmbuf_read(void *buffer, int count)
{
size_t avail = pcmbuf_used();
size_t size = count*PCM_SAMP_SIZE;
if (count > 0 && avail >= size)
{
size_t endidx = pcm_ridx + size;
if (endidx > PCM_BUF_SIZE)
{
size_t wrap = endidx - PCM_BUF_SIZE;
size_t offset = size -= wrap;
if (num_channels == 1)
offset /= 2; /* src offset -> dst offset */
pcm_copyfn(buffer + offset, pcmbuf_ptr(0), wrap);
}
pcm_copyfn(buffer, pcmbuf_ptr(pcm_ridx), size);
if (avail >= sample_rate*PCM_SAMP_SIZE*PCM_BOOST_SECONDS ||
avail >= PCM_BUF_SIZE*1/2)
{
/* Filling up - boost threshold data available or more or 1/2 full
or more - boost codec */
trigger_cpu_boost();
}
pcm_buffer_empty = false;
return count;
}
/* Not enough data available - encoder should idle */
pcm_buffer_empty = true;
cancel_cpu_boost();
/* Sleep a little bit */
sleep(0);
return 0;
}
/* Advance PCM buffer by count samples */
static int enc_pcmbuf_advance(int count)
{
if (count <= 0)
return 0;
size_t avail = pcmbuf_used();
size_t size = count*PCM_SAMP_SIZE;
if (avail < size)
{
size = avail;
count = size / PCM_SAMP_SIZE;
}
pcm_ridx = pcmbuf_add(pcm_ridx, size);
return count;
}
/* Return encoder chunk at current write position, wrapping to 0 if
* requested size demands it.
*
* NOTE: No request should be more than 1/2 the buffer length, all elements
* included, or progress will not be guaranteed.
* (ie. CHUNK_DATA_COUNT(need) <= enc_buflen / 2)
*/
static struct enc_chunk_data * enc_encbuf_get_buffer(size_t need)
{
/* Convert to buffer slot count, including the header */
need = CHUNK_DATA_COUNT(need);
enum record_state state = record_state;
size_t avail = encbuf_free();
/* Must have the split margin as well but it does not have to be
continuous with the request */
while (avail <= need + ENCBUF_MIN_SPLIT_MARGIN ||
(enc_widx + need > enc_buflen &&
enc_ridx <= need + ENCBUF_MIN_SPLIT_MARGIN))
{
if (UNLIKELY(state == REC_STATE_IDLE))
{
/* Prerecording - delete some old data */
size_t ridx;
struct enc_chunk_data *data =
encbuf_read_ptr_incr(enc_ridx, &ridx);
if (data)
{
encbuf_rec_count -= CHUNK_DATA_COUNT(data->hdr.size);
num_rec_bytes -= data->hdr.size;
num_rec_samples -= data->pcm_count;
}
enc_ridx = ridx;
avail = encbuf_free();
continue;
}
else if (avail == enc_buflen)
{
/* Empty but request larger than any possible space */
raise_warning_status(PCMREC_W_ENC_BUFFER_OVF);
}
else if (state != REC_STATE_FLUSH && encbuf_used() < high_watermark)
{
/* Not yet even at high watermark but what's needed won't fit */
encbuf_request_flush();
}
sleep(0);
return NULL;
}
struct enc_chunk_data *data =
encbuf_get_write_ptr(enc_widx, need, &enc_widx);
if (state == REC_STATE_IDLE)
data->hdr.pre = 1;
return data;
}
/* Releases the current buffer into the available chunks */
static void enc_encbuf_finish_buffer(void)
{
struct enc_chunk_data *data = ENC_DATA_HDR(encbuf_ptr(enc_widx));
if (data->hdr.err)
{
/* Encoder set error flag */
raise_error_status(PCMREC_E_ENCODER);
return;
}
size_t data_size = data->hdr.size;
if (data_size == 0)
return; /* Claims nothing was written */
size_t count = CHUNK_DATA_COUNT(data_size);
size_t avail = encbuf_free();
if (avail <= count || enc_widx + count > enc_buflen)
{
/* Claims it wrote too much? */
raise_warning_status(PCMREC_W_ENC_BUFFER_OVF);
return;
}
if (num_rec_bytes + data_size > MAX_NUM_REC_BYTES)
{
/* Would exceed filesize limit; should have split sooner.
This chunk will be dropped. :'( */
raise_warning_status(PCMREC_W_FILE_SIZE);
return;
}
encbuf_widx_advance(enc_widx, count);
encbuf_rec_count += count;
num_rec_bytes += data_size;
num_rec_samples += data->pcm_count;
}
/* Read from the output stream */
static ssize_t enc_stream_read(void *buf, size_t count)
{
if (!stream_flush_buf())
return -1;
return read(rec_fd, buf, count);
}
/* Seek the output steam */
static off_t enc_stream_lseek(off_t offset, int whence)
{
if (!stream_flush_buf())
return -1;
return lseek(rec_fd, offset, whence);
}
/* Write to the output stream */
static ssize_t enc_stream_write(const void *buf, size_t count)
{
if (UNLIKELY(count >= STREAM_BUF_SIZE))
{
/* Too big to buffer */
if (stream_flush_buf())
return write(rec_fd, buf, count);
}
if (!count)
return 0;
if (stream_buf_used + count > STREAM_BUF_SIZE)
{
if (!stream_flush_buf() && stream_buf_used + count > STREAM_BUF_SIZE)
count = STREAM_BUF_SIZE - stream_buf_used;
}
memcpy(stream_buffer + stream_buf_used, buf, count);
stream_buf_used += count;
return count;
}
/* One-time init at startup */
void INIT_ATTR recording_init(void)
{
/* Init API */
ci.enc_pcmbuf_read = enc_pcmbuf_read;
ci.enc_pcmbuf_advance = enc_pcmbuf_advance;
ci.enc_encbuf_get_buffer = enc_encbuf_get_buffer;
ci.enc_encbuf_finish_buffer = enc_encbuf_finish_buffer;
ci.enc_stream_read = enc_stream_read;
ci.enc_stream_lseek = enc_stream_lseek;
ci.enc_stream_write = enc_stream_write;
}