| /*************************************************************************** |
| * __________ __ ___. |
| * 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 = dst; |
| int16_t const *s = src; |
| |
| /* mono = (L + R) / 2 */ |
| do |
| *d++ = ((int32_t){ *s++ } + *s++ + 1) >> 1; |
| while (src_size -= PCM_SAMP_SIZE); |
| |
| return dst; |
| } |
| |
| /* Copy with mono conversion - output 1/2 size of input */ |
| static void * ICODE_ATTR |
| copy_buffer_mono_l(void *dst, const void *src, size_t src_size) |
| { |
| int16_t *d = dst; |
| int16_t const *s = (int16_t *)src - 2; |
| |
| /* mono = L */ |
| do |
| *d++ = *(s += 2); |
| while (src_size -= PCM_SAMP_SIZE); |
| |
| return dst; |
| } |
| |
| /* Copy with mono conversion - output 1/2 size of input */ |
| static void * ICODE_ATTR |
| copy_buffer_mono_r(void *dst, const void *src, size_t src_size) |
| { |
| int16_t *d = dst; |
| int16_t const *s = (int16_t *)src - 1; |
| |
| /* mono = R */ |
| do |
| *d++ = *(s += 2); |
| while (src_size -= PCM_SAMP_SIZE); |
| |
| return dst; |
| } |
| |
| |
| /** 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; |
| } |