From 9558c4956d3d603c4d132af88633767810f3ba62 Mon Sep 17 00:00:00 2001 From: Björn Stenberg Date: Tue, 14 Oct 2008 11:12:20 +0000 Subject: Moved pcm_record from firmware to apps. Cleaned up some. Now all code using struct mp3entry is in apps. git-svn-id: svn://svn.rockbox.org/rockbox/trunk@18807 a1c6a512-1295-4272-9138-f99709370657 --- apps/SOURCES | 3 + apps/id3.h | 246 ++++++ apps/mpeg.c | 25 +- apps/mpeg.h | 66 ++ apps/recorder/pcm_record.c | 1788 ++++++++++++++++++++++++++++++++++++++++++++ apps/recorder/pcm_record.h | 66 ++ apps/recorder/recording.c | 1 + apps/replaygain.h | 33 + 8 files changed, 2212 insertions(+), 16 deletions(-) create mode 100644 apps/id3.h create mode 100644 apps/mpeg.h create mode 100644 apps/recorder/pcm_record.c create mode 100644 apps/recorder/pcm_record.h create mode 100644 apps/replaygain.h (limited to 'apps') diff --git a/apps/SOURCES b/apps/SOURCES index 7622b416f7..3fce356092 100644 --- a/apps/SOURCES +++ b/apps/SOURCES @@ -116,6 +116,9 @@ codecs.c dsp.c #ifdef HAVE_RECORDING enc_config.c +#ifndef SIMULATOR +recorder/pcm_record.c +#endif /* SIMULATOR */ #endif eq.c #if defined(CPU_COLDFIRE) diff --git a/apps/id3.h b/apps/id3.h new file mode 100644 index 0000000000..da2faf1b12 --- /dev/null +++ b/apps/id3.h @@ -0,0 +1,246 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2002 by Daniel Stenberg + * + * 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. + * + ****************************************************************************/ +#ifndef ID3_H +#define ID3_H + +#include +#include "config.h" +#include "file.h" + +#define ID3V2_BUF_SIZE 300 + +/* Audio file types. */ +/* NOTE: The values of the AFMT_* items are used for the %fc tag in the WPS + - so new entries MUST be added to the end to maintain compatibility. + */ +enum +{ + AFMT_UNKNOWN = 0, /* Unknown file format */ + + /* start formats */ + + AFMT_MPA_L1, /* MPEG Audio layer 1 */ + AFMT_MPA_L2, /* MPEG Audio layer 2 */ + AFMT_MPA_L3, /* MPEG Audio layer 3 */ + +#if CONFIG_CODEC == SWCODEC + AFMT_AIFF, /* Audio Interchange File Format */ + AFMT_PCM_WAV, /* Uncompressed PCM in a WAV file */ + AFMT_OGG_VORBIS, /* Ogg Vorbis */ + AFMT_FLAC, /* FLAC */ + AFMT_MPC, /* Musepack */ + AFMT_A52, /* A/52 (aka AC3) audio */ + AFMT_WAVPACK, /* WavPack */ + AFMT_ALAC, /* Apple Lossless Audio Codec */ + AFMT_AAC, /* Advanced Audio Coding (AAC) in M4A container */ + AFMT_SHN, /* Shorten */ + AFMT_SID, /* SID File Format */ + AFMT_ADX, /* ADX File Format */ + AFMT_NSF, /* NESM (NES Sound Format) */ + AFMT_SPEEX, /* Ogg Speex speech */ + AFMT_SPC, /* SPC700 save state */ + AFMT_APE, /* Monkey's Audio (APE) */ + AFMT_WMA, /* WMAV1/V2 in ASF */ + AFMT_MOD, /* Amiga MOD File Format */ + AFMT_SAP, /* Amiga 8Bit SAP Format */ +#endif + + /* add new formats at any index above this line to have a sensible order - + specified array index inits are used */ + /* format arrays defined in id3.c */ + + AFMT_NUM_CODECS, + +#if CONFIG_CODEC == SWCODEC && defined(HAVE_RECORDING) + /* masks to decompose parts */ + CODEC_AFMT_MASK = 0x0fff, + CODEC_TYPE_MASK = 0x7000, + + /* switch for specifying codec type when requesting a filename */ + CODEC_TYPE_DECODER = (0 << 12), /* default */ + CODEC_TYPE_ENCODER = (1 << 12), +#endif /* CONFIG_CODEC == SWCODEC && defined(HAVE_RECORDING) */ +}; + +#if CONFIG_CODEC == SWCODEC +#define CODEC_EXTENSION "codec" + +#ifdef HAVE_RECORDING +#define ENCODER_SUFFIX "_enc" +enum rec_format_indexes +{ + __REC_FORMAT_START_INDEX = -1, + + /* start formats */ + + REC_FORMAT_PCM_WAV, + REC_FORMAT_AIFF, + REC_FORMAT_WAVPACK, + REC_FORMAT_MPA_L3, + + /* add new formats at any index above this line to have a sensible order - + specified array index inits are used + REC_FORMAT_CFG_NUM_BITS should allocate enough bits to hold the range + REC_FORMAT_CFG_VALUE_LIST should be in same order as indexes + */ + + REC_NUM_FORMATS, + + REC_FORMAT_DEFAULT = REC_FORMAT_PCM_WAV, + REC_FORMAT_CFG_NUM_BITS = 2 +}; + +#define REC_FORMAT_CFG_VAL_LIST "wave,aiff,wvpk,mpa3" + +/* get REC_FORMAT_* corresponding AFMT_* */ +extern const int rec_format_afmt[REC_NUM_FORMATS]; +/* get AFMT_* corresponding REC_FORMAT_* */ +extern const int afmt_rec_format[AFMT_NUM_CODECS]; + +#define AFMT_ENTRY(label, root_fname, enc_root_fname, ext_list) \ + { label, root_fname, enc_root_fname, ext_list } +#else /* !HAVE_RECORDING */ +#define AFMT_ENTRY(label, root_fname, enc_root_fname, ext_list) \ + { label, root_fname, ext_list } +#endif /* HAVE_RECORDING */ +#else /* !SWCODEC */ + +#define AFMT_ENTRY(label, root_fname, enc_root_fname, ext_list) \ + { label, ext_list } +#endif /* CONFIG_CODEC == SWCODEC */ + +/* record describing the audio format */ +struct afmt_entry +{ + char label[8]; /* format label */ +#if CONFIG_CODEC == SWCODEC + char *codec_root_fn; /* root codec filename (sans _enc and .codec) */ +#ifdef HAVE_RECORDING + char *codec_enc_root_fn; /* filename of encoder codec */ +#endif +#endif + char *ext_list; /* double NULL terminated extension + list for type with the first as + the default for recording */ +}; + +/* database of labels and codecs. add formats per above enum */ +extern const struct afmt_entry audio_formats[AFMT_NUM_CODECS]; + +struct mp3entry { + char path[MAX_PATH]; + char* title; + char* artist; + char* album; + char* genre_string; + char* disc_string; + char* track_string; + char* year_string; + char* composer; + char* comment; + char* albumartist; + char* grouping; + int discnum; + int tracknum; + int version; + int layer; + int year; + unsigned char id3version; + unsigned int codectype; + unsigned int bitrate; + unsigned long frequency; + unsigned long id3v2len; + unsigned long id3v1len; + unsigned long first_frame_offset; /* Byte offset to first real MP3 frame. + Used for skipping leading garbage to + avoid gaps between tracks. */ + unsigned long vbr_header_pos; + unsigned long filesize; /* without headers; in bytes */ + unsigned long length; /* song length in ms */ + unsigned long elapsed; /* ms played */ + + int lead_trim; /* Number of samples to skip at the beginning */ + int tail_trim; /* Number of samples to remove from the end */ + + /* Added for Vorbis */ + unsigned long samples; /* number of samples in track */ + + /* MP3 stream specific info */ + unsigned long frame_count; /* number of frames in the file (if VBR) */ + + /* Used for A52/AC3 */ + unsigned long bytesperframe; /* number of bytes per frame (if CBR) */ + + /* Xing VBR fields */ + bool vbr; + bool has_toc; /* True if there is a VBR header in the file */ + unsigned char toc[100]; /* table of contents */ + + /* these following two fields are used for local buffering */ + char id3v2buf[ID3V2_BUF_SIZE]; + char id3v1buf[4][92]; + + /* resume related */ + unsigned long offset; /* bytes played */ + int index; /* playlist index */ + + /* runtime database fields */ + long tagcache_idx; /* 0=invalid, otherwise idx+1 */ + int rating; + int score; + long playcount; + long lastplayed; + long playtime; + + /* replaygain support */ + +#if CONFIG_CODEC == SWCODEC + char* track_gain_string; + char* album_gain_string; + long track_gain; /* 7.24 signed fixed point. 0 for no gain. */ + long album_gain; + long track_peak; /* 7.24 signed fixed point. 0 for no peak. */ + long album_peak; +#endif + + /* Cuesheet support */ + int cuesheet_type; /* 0: none, 1: external, 2: embedded */ + + /* Musicbrainz Track ID */ + char* mb_track_id; +}; + +enum { + ID3_VER_1_0 = 1, + ID3_VER_1_1, + ID3_VER_2_2, + ID3_VER_2_3, + ID3_VER_2_4 +}; + +bool get_mp3_metadata(int fd, struct mp3entry *entry, const char *filename); +bool mp3info(struct mp3entry *entry, const char *filename); +char* id3_get_num_genre(unsigned int genre_num); +int getid3v2len(int fd); +void adjust_mp3entry(struct mp3entry *entry, void *dest, const void *orig); +void copy_mp3entry(struct mp3entry *dest, const struct mp3entry *orig); + +#endif diff --git a/apps/mpeg.c b/apps/mpeg.c index 2c65e46060..b570f41b28 100644 --- a/apps/mpeg.c +++ b/apps/mpeg.c @@ -2432,6 +2432,15 @@ void audio_set_recording_options(struct audio_recording_options *options) DEBUGF("mas_writemem(MAS_BANK_D0, ENCODER_CONTROL, %x)\n", shadow_encoder_control); +#if CONFIG_TUNER & S1A0903X01 + /* Store the (unpitched) MAS PLL frequency. Used for avoiding FM + interference with the Samsung tuner. */ + if (rec_frequency_index) + mas_store_pllfreq(24576000); + else + mas_store_pllfreq(22579000); +#endif + shadow_soft_mute = options->rec_editable?4:0; mas_writemem(MAS_BANK_D0, MAS_D0_SOFT_MUTE, &shadow_soft_mute,1); @@ -2484,22 +2493,6 @@ void audio_set_recording_gain(int left, int right, int type) mas_codec_writereg(0x0, shadow_codec_reg0); } -#if CONFIG_TUNER & S1A0903X01 -/* Get the (unpitched) MAS PLL frequency, for avoiding FM interference with the - * Samsung tuner. Zero means unknown. Currently handles recording from analog - * input only. */ -int mpeg_get_mas_pllfreq(void) -{ - if (mpeg_mode != MPEG_ENCODER) - return 0; - - if (rec_frequency_index == 0) /* 44.1 kHz / 22.05 kHz */ - return 22579000; - else - return 24576000; -} -#endif /* CONFIG_TUNER & S1A0903X01 */ - /* try to make some kind of beep, also in recording mode */ void audio_beep(int duration) { diff --git a/apps/mpeg.h b/apps/mpeg.h new file mode 100644 index 0000000000..ce2cff0069 --- /dev/null +++ b/apps/mpeg.h @@ -0,0 +1,66 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2002 by Linus Nielsen Feltzing + * + * 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. + * + ****************************************************************************/ +#ifndef _MPEG_H_ +#define _MPEG_H_ + +#include +#include "id3.h" + +#define MPEG_SWAP_CHUNKSIZE 0x2000 +#define MPEG_HIGH_WATER 2 /* We leave 2 bytes empty because otherwise we + wouldn't be able to see the difference between + an empty buffer and a full one. */ +#define MPEG_LOW_WATER 0x60000 +#define MPEG_RECORDING_LOW_WATER 0x80000 +#define MPEG_LOW_WATER_CHUNKSIZE 0x40000 +#define MPEG_LOW_WATER_SWAP_CHUNKSIZE 0x10000 +#ifdef HAVE_MMC +#define MPEG_PLAY_PENDING_THRESHOLD 0x20000 +#define MPEG_PLAY_PENDING_SWAPSIZE 0x20000 +#else +#define MPEG_PLAY_PENDING_THRESHOLD 0x10000 +#define MPEG_PLAY_PENDING_SWAPSIZE 0x10000 +#endif + +#define MPEG_MAX_PRERECORD_SECONDS 30 + +/* For ID3 info and VBR header */ +#define MPEG_RESERVED_HEADER_SPACE (4096 + 576) + +#if (CONFIG_CODEC == MAS3587F) || defined(SIMULATOR) + +#if CONFIG_TUNER & S1A0903X01 +int mpeg_get_mas_pllfreq(void); +#endif + +#endif +unsigned long mpeg_get_last_header(void); + +/* in order to keep the recording here, I have to expose this */ +void rec_tick(void); +void playback_tick(void); /* FixMe: get rid of this, use mp3_get_playtime() */ + +void audio_set_track_changed_event(void (*handler)(struct mp3entry *id3)); +void audio_set_track_buffer_event(void (*handler)(struct mp3entry *id3)); +void audio_set_track_unbuffer_event(void (*handler)(struct mp3entry *id3)); +void audio_set_cuesheet_callback(bool (*handler)(const char *filename)); + +#endif diff --git a/apps/recorder/pcm_record.c b/apps/recorder/pcm_record.c new file mode 100644 index 0000000000..b1ea535470 --- /dev/null +++ b/apps/recorder/pcm_record.c @@ -0,0 +1,1788 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2005 by Linus Nielsen Feltzing + * + * 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 "pcm_record.h" +#include "system.h" +#include "kernel.h" +#include "logf.h" +#include "thread.h" +#include +#include "ata.h" +#include "usb.h" +#include "buffer.h" +#include "general.h" +#include "audio.h" +#include "sound.h" +#include "id3.h" +#ifdef HAVE_SPDIF_IN +#include "spdif.h" +#endif + +/***************************************************************************/ + +extern struct thread_entry *codec_thread_p; + +/** General recording state **/ +static bool is_recording; /* We are recording */ +static bool is_paused; /* We have paused */ +static unsigned long errors; /* An error has occured */ +static unsigned long warnings; /* Warning */ +static int flush_interrupts = 0; /* Number of messages queued that + should interrupt a flush in + progress - + for a safety net and a prompt + response to stop, split and pause + requests - + only interrupts a flush initiated + by pcmrec_flush(0) */ + +/* Utility functions for setting/clearing flushing interrupt flag */ +static inline void flush_interrupt(void) +{ + flush_interrupts++; + logf("flush int: %d", flush_interrupts); +} + +static inline void clear_flush_interrupt(void) +{ + if (--flush_interrupts < 0) + flush_interrupts = 0; +} + +/** Stats on encoded data for current file **/ +static size_t num_rec_bytes; /* Num bytes recorded */ +static unsigned long num_rec_samples; /* Number of PCM samples recorded */ + +/** Stats on encoded data for all files from start to stop **/ +#if 0 +static unsigned long long accum_rec_bytes; /* total size written to chunks */ +static unsigned long long accum_pcm_samples; /* total pcm count processed */ +#endif + +/* Keeps data about current file and is sent as event data for codec */ +static struct enc_file_event_data rec_fdata IDATA_ATTR = +{ + .chunk = NULL, + .new_enc_size = 0, + .new_num_pcm = 0, + .rec_file = -1, + .num_pcm_samples = 0 +}; + +/** These apply to current settings **/ +static int rec_source; /* current rec_source setting */ +static int rec_frequency; /* current frequency setting */ +static unsigned long sample_rate; /* Sample rate in HZ */ +static int num_channels; /* Current number of channels */ +static int rec_mono_mode; /* how mono is created */ +static struct encoder_config enc_config; /* Current encoder configuration */ +static unsigned long pre_record_ticks; /* pre-record time in ticks */ + +/**************************************************************************** + 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.main: audio_load_encoder(); start the encoder + 2.encoder: enc_get_inputs(); get encoder recording settings + 3.encoder: enc_set_parameters(); set the encoder parameters + 4.encoder: enc_get_pcm_data(); get n bytes of unprocessed pcm data + 5.encoder: enc_unget_pcm_data(); put n bytes of data back (optional) + 6.encoder: enc_get_chunk(); get a ptr to next enc chunk + 7.encoder: compress and store data to enc chunk + 8.encoder: enc_finish_chunk(); inform main about chunk processed and + is available to be written to a file. + Encoder can place any number of chunks + of PCM data in a single output chunk + but must stay within its output chunk + size + 9.encoder: repeat 4. to 8. + A.pcmrec: enc_events_callback(); called for certain events + + (*) Optional step +****************************************************************************/ + +/** buffer parameters where incoming PCM data is placed **/ +#define PCM_NUM_CHUNKS 256 /* Power of 2 */ +#define PCM_CHUNK_SIZE 8192 /* Power of 2 */ +#define PCM_CHUNK_MASK (PCM_NUM_CHUNKS*PCM_CHUNK_SIZE - 1) + +#define GET_PCM_CHUNK(offset) ((long *)(pcm_buffer + (offset))) +#define GET_ENC_CHUNK(index) ENC_CHUNK_HDR(enc_buffer + enc_chunk_size*(index)) +#define INC_ENC_INDEX(index) \ + { if (++index >= enc_num_chunks) index = 0; } +#define DEC_ENC_INDEX(index) \ + { if (--index < 0) index = enc_num_chunks - 1; } + +static size_t rec_buffer_size; /* size of available buffer */ +static unsigned char *pcm_buffer; /* circular recording buffer */ +static unsigned char *enc_buffer; /* circular encoding buffer */ +#ifdef DEBUG +static unsigned long *wrap_id_p; /* magic at wrap position - a debugging + aid to check if the encoder data + spilled out of its chunk */ +#endif /* DEBUG */ +static volatile int dma_wr_pos; /* current DMA write pos */ +static int pcm_rd_pos; /* current PCM read pos */ +static int pcm_enc_pos; /* position encoder is processing */ +static volatile bool dma_lock; /* lock DMA write position */ +static int enc_wr_index; /* encoder chunk write index */ +static int enc_rd_index; /* encoder chunk read index */ +static int enc_num_chunks; /* number of chunks in ringbuffer */ +static size_t enc_chunk_size; /* maximum encoder chunk size */ +static unsigned long enc_sample_rate; /* sample rate used by encoder */ +static bool pcmrec_context = false; /* called by pcmrec thread? */ +static bool pcm_buffer_empty; /* all pcm chunks processed? */ + +/** file flushing **/ +static int low_watermark; /* Low watermark to stop flush */ +static int high_watermark; /* max chunk limit for data flush */ +static unsigned long spinup_time = 35*HZ/10; /* Fudged spinup time */ +static int last_ata_spinup_time = -1;/* previous spin time used */ +#ifdef HAVE_PRIORITY_SCHEDULING +static int flood_watermark; /* boost thread priority when here */ +#endif + +/* Constants that control watermarks */ +#define LOW_SECONDS 1 /* low watermark time till empty */ +#define MINI_CHUNKS 10 /* chunk count for mini flush */ +#ifdef HAVE_PRIORITY_SCHEDULING +#define PRIO_SECONDS 10 /* max flush time before priority boost */ +#endif +#if MEM <= 16 +#define PANIC_SECONDS 5 /* flood watermark time until full */ +#define FLUSH_SECONDS 7 /* flush watermark time until full */ +#else +#define PANIC_SECONDS 8 +#define FLUSH_SECONDS 10 +#endif /* MEM */ + +/** encoder events **/ +static void (*enc_events_callback)(enum enc_events event, void *data); + +/** Path queue for files to write **/ +#define FNQ_MIN_NUM_PATHS 16 /* minimum number of paths to hold */ +#define FNQ_MAX_NUM_PATHS 64 /* maximum number of paths to hold */ +static unsigned char *fn_queue; /* pointer to first filename */ +static ssize_t fnq_size; /* capacity of queue in bytes */ +static int fnq_rd_pos; /* current read position */ +static int fnq_wr_pos; /* current write position */ +#define FNQ_NEXT(pos) \ + ({ int p = (pos) + MAX_PATH; \ + if (p >= fnq_size) \ + p = 0; \ + p; }) +#define FNQ_PREV(pos) \ + ({ int p = (pos) - MAX_PATH; \ + if (p < 0) \ + p = fnq_size - MAX_PATH; \ + p; }) + +enum +{ + PCMREC_FLUSH_INTERRUPTABLE = 0x8000000, /* Flush can be interrupted by + incoming messages - combine + with other constants */ + PCMREC_FLUSH_ALL = 0x7ffffff, /* Flush all files */ + PCMREC_FLUSH_MINI = 0x7fffffe, /* Flush a small number of + chunks */ + PCMREC_FLUSH_IF_HIGH = 0x0000000, /* Flush if high watermark + reached */ +}; + +/***************************************************************************/ + +static struct event_queue pcmrec_queue SHAREDBSS_ATTR; +static struct queue_sender_list pcmrec_queue_send SHAREDBSS_ATTR; +static long pcmrec_stack[3*DEFAULT_STACK_SIZE/sizeof(long)]; +static const char pcmrec_thread_name[] = "pcmrec"; +static struct thread_entry *pcmrec_thread_p; + +static void pcmrec_thread(void); + +enum +{ + PCMREC_NULL = 0, + PCMREC_INIT, /* enable recording */ + PCMREC_CLOSE, /* close recording */ + PCMREC_OPTIONS, /* set recording options */ + PCMREC_RECORD, /* record a new file */ + PCMREC_STOP, /* stop the current recording */ + PCMREC_PAUSE, /* pause the current recording */ + PCMREC_RESUME, /* resume the current recording */ +#if 0 + PCMREC_FLUSH_NUM, /* flush a number of files out */ +#endif +}; + +/*******************************************************************/ +/* Functions that are not executing in the pcmrec_thread first */ +/*******************************************************************/ + +/* Callback for when more data is ready - called in interrupt context */ +static int pcm_rec_have_more(int status) +{ + if (status < 0) + { + /* some error condition */ + if (status == DMA_REC_ERROR_DMA) + { + /* Flush recorded data to disk and stop recording */ + queue_post(&pcmrec_queue, PCMREC_STOP, 0); + return -1; + } + /* else try again next transmission */ + } + else if (!dma_lock) + { + /* advance write position */ + int next_pos = (dma_wr_pos + PCM_CHUNK_SIZE) & PCM_CHUNK_MASK; + + /* set pcm ovf if processing start position is inside current + write chunk */ + if ((unsigned)(pcm_enc_pos - next_pos) < PCM_CHUNK_SIZE) + warnings |= PCMREC_W_PCM_BUFFER_OVF; + + dma_wr_pos = next_pos; + } + + pcm_record_more(GET_PCM_CHUNK(dma_wr_pos), PCM_CHUNK_SIZE); + return 0; +} /* pcm_rec_have_more */ + +static void reset_hardware(void) +{ + /* reset pcm to defaults (playback only) */ + pcm_set_frequency(HW_SAMPR_DEFAULT); + audio_set_output_source(AUDIO_SRC_PLAYBACK); + pcm_apply_settings(); +} + +/** pcm_rec_* group **/ + +/** + * Clear all errors and warnings + */ +void pcm_rec_error_clear(void) +{ + errors = warnings = 0; +} /* pcm_rec_error_clear */ + +/** + * Check mode, errors and warnings + */ +unsigned long pcm_rec_status(void) +{ + unsigned long ret = 0; + + if (is_recording) + ret |= AUDIO_STATUS_RECORD; + else if (pre_record_ticks) + ret |= AUDIO_STATUS_PRERECORD; + + if (is_paused) + ret |= AUDIO_STATUS_PAUSE; + + if (errors) + ret |= AUDIO_STATUS_ERROR; + + if (warnings) + ret |= AUDIO_STATUS_WARNING; + + return ret; +} /* pcm_rec_status */ + +/** + * Return warnings that have occured since recording started + */ +unsigned long pcm_rec_get_warnings(void) +{ + return warnings; +} + +#if 0 +int pcm_rec_current_bitrate(void) +{ + if (accum_pcm_samples == 0) + return 0; + + return (int)(8*accum_rec_bytes*enc_sample_rate / (1000*accum_pcm_samples)); +} /* pcm_rec_current_bitrate */ +#endif + +#if 0 +int pcm_rec_encoder_afmt(void) +{ + return enc_config.afmt; +} /* pcm_rec_encoder_afmt */ +#endif + +#if 0 +int pcm_rec_rec_format(void) +{ + return afmt_rec_format[enc_config.afmt]; +} /* pcm_rec_rec_format */ +#endif + +#ifdef HAVE_SPDIF_IN +unsigned long pcm_rec_sample_rate(void) +{ + /* Which is better ?? */ +#if 0 + return enc_sample_rate; +#endif + return sample_rate; +} /* audio_get_sample_rate */ +#endif + +/** + * Creates pcmrec_thread + */ +void pcm_rec_init(void) +{ + queue_init(&pcmrec_queue, true); + pcmrec_thread_p = + create_thread(pcmrec_thread, pcmrec_stack, sizeof(pcmrec_stack), + 0, pcmrec_thread_name IF_PRIO(, PRIORITY_RECORDING) + IF_COP(, CPU)); + queue_enable_queue_send(&pcmrec_queue, &pcmrec_queue_send, + pcmrec_thread_p); +} /* pcm_rec_init */ + +/** audio_* group **/ + +/** + * Initializes recording - call before calling any other recording function + */ +void audio_init_recording(unsigned int buffer_offset) +{ + logf("audio_init_recording"); + queue_send(&pcmrec_queue, PCMREC_INIT, 0); + logf("audio_init_recording done"); + (void)buffer_offset; +} /* audio_init_recording */ + +/** + * Closes recording - call audio_stop_recording first + */ +void audio_close_recording(void) +{ + logf("audio_close_recording"); + queue_send(&pcmrec_queue, PCMREC_CLOSE, 0); + logf("audio_close_recording done"); +} /* audio_close_recording */ + +/** + * Sets recording parameters + */ +void audio_set_recording_options(struct audio_recording_options *options) +{ + logf("audio_set_recording_options"); + queue_send(&pcmrec_queue, PCMREC_OPTIONS, (intptr_t)options); + logf("audio_set_recording_options done"); +} /* audio_set_recording_options */ + +/** + * Start recording if not recording or else split + */ +void audio_record(const char *filename) +{ + logf("audio_record: %s", filename); + flush_interrupt(); + queue_send(&pcmrec_queue, PCMREC_RECORD, (intptr_t)filename); + logf("audio_record_done"); +} /* audio_record */ + +/** + * audio_record wrapper for API compatibility with HW codec + */ +void audio_new_file(const char *filename) +{ + audio_record(filename); +} /* audio_new_file */ + +/** + * Stop current recording if recording + */ +void audio_stop_recording(void) +{ + logf("audio_stop_recording"); + flush_interrupt(); + queue_post(&pcmrec_queue, PCMREC_STOP, 0); + logf("audio_stop_recording done"); +} /* audio_stop_recording */ + +/** + * Pause current recording + */ +void audio_pause_recording(void) +{ + logf("audio_pause_recording"); + flush_interrupt(); + queue_post(&pcmrec_queue, PCMREC_PAUSE, 0); + logf("audio_pause_recording done"); +} /* audio_pause_recording */ + +/** + * Resume current recording if paused + */ +void audio_resume_recording(void) +{ + logf("audio_resume_recording"); + queue_post(&pcmrec_queue, PCMREC_RESUME, 0); + logf("audio_resume_recording done"); +} /* audio_resume_recording */ + +/** + * Note that microphone is mono, only left value is used + * See audiohw_set_recvol() for exact ranges. + * + * @param type AUDIO_GAIN_MIC, AUDIO_GAIN_LINEIN + * + */ +void audio_set_recording_gain(int left, int right, int type) +{ + //logf("rcmrec: t=%d l=%d r=%d", type, left, right); + audiohw_set_recvol(left, right, type); +} /* audio_set_recording_gain */ + +/** Information about current state **/ + +/** + * Return current recorded time in ticks (playback eqivalent time) + */ +unsigned long audio_recorded_time(void) +{ + if (!is_recording || enc_sample_rate == 0) + return 0; + + /* return actual recorded time a la encoded data even if encoder rate + doesn't match the pcm rate */ + return (long)(HZ*(unsigned long long)num_rec_samples / enc_sample_rate); +} /* audio_recorded_time */ + +/** + * Return number of bytes encoded to output + */ +unsigned long audio_num_recorded_bytes(void) +{ + if (!is_recording) + return 0; + + return num_rec_bytes; +} /* audio_num_recorded_bytes */ + +/***************************************************************************/ +/* */ +/* Functions that execute in the context of pcmrec_thread */ +/* */ +/***************************************************************************/ + +/** Filename Queue **/ + +/* returns true if the queue is empty */ +static inline bool pcmrec_fnq_is_empty(void) +{ + return fnq_rd_pos == fnq_wr_pos; +} /* pcmrec_fnq_is_empty */ + +/* empties the filename queue */ +static inline void pcmrec_fnq_set_empty(void) +{ + fnq_rd_pos = fnq_wr_pos; +} /* pcmrec_fnq_set_empty */ + +/* returns true if the queue is full */ +static bool pcmrec_fnq_is_full(void) +{ + ssize_t size = fnq_wr_pos - fnq_rd_pos; + if (size < 0) + size += fnq_size; + + return size >= fnq_size - MAX_PATH; +} /* pcmrec_fnq_is_full */ + +/* queue another filename - will overwrite oldest one if full */ +static bool pcmrec_fnq_add_filename(const char *filename) +{ + strncpy(fn_queue + fnq_wr_pos, filename, MAX_PATH); + fnq_wr_pos = FNQ_NEXT(fnq_wr_pos); + + if (fnq_rd_pos != fnq_wr_pos) + return true; + + /* queue full */ + fnq_rd_pos = FNQ_NEXT(fnq_rd_pos); + return true; +} /* pcmrec_fnq_add_filename */ + +/* replace the last filename added */ +static bool pcmrec_fnq_replace_tail(const char *filename) +{ + int pos; + + if (pcmrec_fnq_is_empty()) + return false; + + pos = FNQ_PREV(fnq_wr_pos); + + strncpy(fn_queue + pos, filename, MAX_PATH); + + return true; +} /* pcmrec_fnq_replace_tail */ + +/* pulls the next filename from the queue */ +static bool pcmrec_fnq_get_filename(char *filename) +{ + if (pcmrec_fnq_is_empty()) + return false; + + if (filename) + strncpy(filename, fn_queue + fnq_rd_pos, MAX_PATH); + + fnq_rd_pos = FNQ_NEXT(fnq_rd_pos); + return true; +} /* pcmrec_fnq_get_filename */ + +/* close the file number pointed to by fd_p */ +static void pcmrec_close_file(int *fd_p) +{ + if (*fd_p < 0) + return; /* preserve error */ + + if (close(*fd_p) != 0) + errors |= PCMREC_E_IO; + + *fd_p = -1; +} /* pcmrec_close_file */ + +/** Data Flushing **/ + +/** + * called after callback to update sizes if codec changed the amount of data + * a chunk represents + */ +static inline void pcmrec_update_sizes_inl(size_t prev_enc_size, + unsigned long prev_num_pcm) +{ + if (rec_fdata.new_enc_size != prev_enc_size) + { + ssize_t size_diff = rec_fdata.new_enc_size - prev_enc_size; + num_rec_bytes += size_diff; +#if 0 + accum_rec_bytes += size_diff; +#endif + } + + if (rec_fdata.new_num_pcm != prev_num_pcm) + { + unsigned long pcm_diff = rec_fdata.new_num_pcm - prev_num_pcm; + num_rec_samples += pcm_diff; +#if 0 + accum_pcm_samples += pcm_diff; +#endif + } +} /* pcmrec_update_sizes_inl */ + +/* don't need to inline every instance */ +static void pcmrec_update_sizes(size_t prev_enc_size, + unsigned long prev_num_pcm) +{ + pcmrec_update_sizes_inl(prev_enc_size, prev_num_pcm); +} /* pcmrec_update_sizes */ + +static void pcmrec_start_file(void) +{ + size_t enc_size = rec_fdata.new_enc_size; + unsigned long num_pcm = rec_fdata.new_num_pcm; + int curr_rec_file = rec_fdata.rec_file; + char filename[MAX_PATH]; + + /* must always pull the filename that matches with this queue */ + if (!pcmrec_fnq_get_filename(filename)) + { + logf("start file: fnq empty"); + *filename = '\0'; + errors |= PCMREC_E_FNQ_DESYNC; + } + else if (errors != 0) + { + logf("start file: error already"); + } + else if (curr_rec_file >= 0) + { + /* Any previous file should have been closed */ + logf("start file: file already open"); + errors |= PCMREC_E_FNQ_DESYNC; + } + + if (errors != 0) + rec_fdata.chunk->flags |= CHUNKF_ERROR; + + /* encoder can set error flag here and should increase + enc_new_size and pcm_new_size to reflect additional + data written if any */ + rec_fdata.filename = filename; + enc_events_callback(ENC_START_FILE, &rec_fdata); + + if (errors == 0 && (rec_fdata.chunk->flags & CHUNKF_ERROR)) + { + logf("start file: enc error"); + errors |= PCMREC_E_ENCODER; + } + + if (errors != 0) + { + pcmrec_close_file(&curr_rec_file); + /* Write no more to this file */ + rec_fdata.chunk->flags |= CHUNKF_END_FILE; + } + else + { + pcmrec_update_sizes(enc_size, num_pcm); + } + + rec_fdata.chunk->flags &= ~CHUNKF_START_FILE; +} /* pcmrec_start_file */ + +static inline void pcmrec_write_chunk(void) +{ + size_t enc_size = rec_fdata.new_enc_size; + unsigned long num_pcm = rec_fdata.new_num_pcm; + + if (errors != 0) + rec_fdata.chunk->flags |= CHUNKF_ERROR; + + enc_events_callback(ENC_WRITE_CHUNK, &rec_fdata); + + if ((long)rec_fdata.chunk->flags >= 0) + { + pcmrec_update_sizes_inl(enc_size, num_pcm); + } + else if (errors == 0) + { + logf("wr chk enc error %lu %lu", + rec_fdata.chunk->enc_size, rec_fdata.chunk->num_pcm); + errors |= PCMREC_E_ENCODER; + } +} /* pcmrec_write_chunk */ + +static void pcmrec_end_file(void) +{ + /* all data in output buffer for current file will have been + written and encoder can now do any nescessary steps to + finalize the written file */ + size_t enc_size = rec_fdata.new_enc_size; + unsigned long num_pcm = rec_fdata.new_num_pcm; + + enc_events_callback(ENC_END_FILE, &rec_fdata); + + if (errors == 0) + { + if (rec_fdata.chunk->flags & CHUNKF_ERROR) + { + logf("end file: enc error"); + errors |= PCMREC_E_ENCODER; + } + else + { + pcmrec_update_sizes(enc_size, num_pcm); + } + } + + /* Force file close if error */ + if (errors != 0) + pcmrec_close_file(&rec_fdata.rec_file); + + rec_fdata.chunk->flags &= ~CHUNKF_END_FILE; +} /* pcmrec_end_file */ + +/** + * Update buffer watermarks with spinup time compensation + * + * All this assumes reasonable data rates, chunk sizes and sufficient + * memory for the most part. Some dumb checks are included but perhaps + * are pointless since this all will break down at extreme limits that + * are currently not applicable to any supported device. + */ +static void pcmrec_refresh_watermarks(void) +{ + logf("ata spinup: %d", ata_spinup_time); + + /* set the low mark for when flushing stops if automatic */ + low_watermark = (LOW_SECONDS*4*sample_rate + (enc_chunk_size-1)) + / enc_chunk_size; + logf("low wmk: %d", low_watermark); + +#ifdef HAVE_PRIORITY_SCHEDULING + /* panic boost thread priority if 2 seconds of ground is lost - + this allows encoder to boost with just under a second of + pcm data (if not yet full enough to boost itself) + and not falsely trip the alarm. */ + flood_watermark = enc_num_chunks - + (PANIC_SECONDS*4*sample_rate + (enc_chunk_size-1)) + / enc_chunk_size; + + if (flood_watermark < low_watermark) + { + logf("warning: panic < low"); + flood_watermark = low_watermark; + } + + logf("flood at: %d", flood_watermark); +#endif + spinup_time = last_ata_spinup_time = ata_spinup_time; + + /* write at 8s + st remaining in enc_buffer - range 12s to + 20s total - default to 3.5s spinup. */ + if (spinup_time == 0) + spinup_time = 35*HZ/10; /* default - cozy */ + else if (spinup_time < 2*HZ) + spinup_time = 2*HZ; /* ludicrous - ramdisk? */ + else if (spinup_time > 10*HZ) + spinup_time = 10*HZ; /* do you have a functioning HD? */ + + /* try to start writing with 10s remaining after disk spinup */ + high_watermark = enc_num_chunks - + ((FLUSH_SECONDS*HZ + spinup_time)*4*sample_rate + + (enc_chunk_size-1)*HZ) / (enc_chunk_size*HZ); + + if (high_watermark < low_watermark) + { + high_watermark = low_watermark; + low_watermark /= 2; + logf("warning: low 'write at'"); + } + + logf("write at: %d", high_watermark); +} /* pcmrec_refresh_watermarks */ + +/** + * Process the chunks + * + * This function is called when queue_get_w_tmo times out. + * + * Set flush_num to the number of files to flush to disk or to + * a PCMREC_FLUSH_* constant. + */ +static void pcmrec_flush(unsigned flush_num) +{ +#ifdef HAVE_PRIORITY_SCHEDULING + static unsigned long last_flush_tick; /* tick when function returned */ + unsigned long start_tick; /* When flush started */ + unsigned long prio_tick; /* Timeout for auto boost */ + int prio_pcmrec; /* Current thread priority for pcmrec */ + int prio_codec; /* Current thread priority for codec */ +#endif + int num_ready; /* Number of chunks ready at start */ + unsigned remaining; /* Number of file starts remaining */ + unsigned chunks_flushed; /* Chunks flushed (for mini flush only) */ + bool interruptable; /* Flush can be interupted */ + + num_ready = enc_wr_index - enc_rd_index; + if (num_ready < 0) + num_ready += enc_num_chunks; + + /* save interruptable flag and remove it to get the actual count */ + interruptable = (flush_num & PCMREC_FLUSH_INTERRUPTABLE) != 0; + flush_num &= ~PCMREC_FLUSH_INTERRUPTABLE; + + if (flush_num == 0) + { + if (!is_recording) + return; + + if (ata_spinup_time != last_ata_spinup_time) + pcmrec_refresh_watermarks(); + + /* enough available? no? then leave */ + if (num_ready < high_watermark) + return; + } /* endif (flush_num == 0) */ + +#ifdef HAVE_PRIORITY_SCHEDULING + start_tick = current_tick; + prio_tick = start_tick + PRIO_SECONDS*HZ + spinup_time; + + if (flush_num == 0 && TIME_BEFORE(current_tick, last_flush_tick + HZ/2)) + { + /* if we're getting called too much and this isn't forced, + boost stat by expiring timeout in advance */ + logf("too frequent flush"); + prio_tick = current_tick - 1; + } + + prio_pcmrec = -1; + prio_codec = -1; /* GCC is too stoopid to figure out it doesn't + need init */ +#endif + + logf("writing:%d(%d):%s%s", num_ready, flush_num, + interruptable ? "i" : "", + flush_num == PCMREC_FLUSH_MINI ? "m" : ""); + + cpu_boost(true); + + remaining = flush_num; + chunks_flushed = 0; + + while (num_ready > 0) + { + /* check current number of encoder chunks */ + int num = enc_wr_index - enc_rd_index; + if (num < 0) + num += enc_num_chunks; + + if (num <= low_watermark && + (flush_num == PCMREC_FLUSH_IF_HIGH || num <= 0)) + { + logf("low data: %d", num); + break; /* data remaining is below threshold */ + } + + if (interruptable && flush_interrupts > 0) + { + logf("int at: %d", num); + break; /* interrupted */ + } + +#ifdef HAVE_PRIORITY_SCHEDULING + if (prio_pcmrec == -1 && (num >= flood_watermark || + TIME_AFTER(current_tick, prio_tick))) + { + /* losing ground or holding without progress - boost + priority until finished */ + logf("pcmrec: boost (%s)", + num >= flood_watermark ? "num" : "time"); + prio_pcmrec = thread_set_priority(NULL, + thread_get_priority(NULL) - 4); + prio_codec = thread_set_priority(codec_thread_p, + thread_get_priority(codec_thread_p) - 4); + } +#endif + + rec_fdata.chunk = GET_ENC_CHUNK(enc_rd_index); + rec_fdata.new_enc_size = rec_fdata.chunk->enc_size; + rec_fdata.new_num_pcm = rec_fdata.chunk->num_pcm; + + if (rec_fdata.chunk->flags & CHUNKF_START_FILE) + { + pcmrec_start_file(); + if (--remaining == 0) + num_ready = 0; /* stop on next loop - must write this + chunk if it has data */ + } + + pcmrec_write_chunk(); + + if (rec_fdata.chunk->flags & CHUNKF_END_FILE) + pcmrec_end_file(); + + INC_ENC_INDEX(enc_rd_index); + + if (errors != 0) + { + pcmrec_end_file(); + break; + } + + if (flush_num == PCMREC_FLUSH_MINI && + ++chunks_flushed >= MINI_CHUNKS) + { + logf("mini flush break"); + break; + } + /* no yielding; the file apis called in the codecs do that + sufficiently */ + } /* end while */ + + /* sync file */ + if (rec_fdata.rec_file >= 0 && fsync(rec_fdata.rec_file) != 0) + errors |= PCMREC_E_IO; + + cpu_boost(false); + +#ifdef HAVE_PRIORITY_SCHEDULING + if (prio_pcmrec != -1) + { + /* return to original priorities */ + logf("pcmrec: unboost priority"); + thread_set_priority(NULL, prio_pcmrec); + thread_set_priority(codec_thread_p, prio_codec); + } + + last_flush_tick = current_tick; /* save tick when we left */ +#endif + + logf("done"); +} /* pcmrec_flush */ + +/** + * Marks a new stream in the buffer and gives the encoder a chance for special + * handling of transition from one to the next. The encoder may change the + * chunk that ends the old stream by requesting more chunks and similiarly for + * the new but must always advance the position though the interface. It can + * later reject any data it cares to when writing the file but should mark the + * chunk so it can recognize this. ENC_WRITE_CHUNK event must be able to accept + * a NULL data pointer without error as well. + */ +static int pcmrec_get_chunk_index(struct enc_chunk_hdr *chunk) +{ + return ((char *)chunk - (char *)enc_buffer) / enc_chunk_size; +} /* pcmrec_get_chunk_index */ + +static struct enc_chunk_hdr * pcmrec_get_prev_chunk(int index) +{ + DEC_ENC_INDEX(index); + return GET_ENC_CHUNK(index); +} /* pcmrec_get_prev_chunk */ + +static void pcmrec_new_stream(const char *filename, /* next file name */ + unsigned long flags, /* CHUNKF_* flags */ + int pre_index) /* index for prerecorded data */ +{ + logf("pcmrec_new_stream"); + char path[MAX_PATH]; /* place to copy filename so sender can be released */ + + struct enc_buffer_event_data data; + bool (*fnq_add_fn)(const char *) = NULL; /* function to use to add + new filename */ + struct enc_chunk_hdr *start = NULL; /* pointer to starting chunk of + stream */ + bool did_flush = false; /* did a flush occurr? */ + + if (filename) + strncpy(path, filename, MAX_PATH); + queue_reply(&pcmrec_queue, 0); /* We have all we need */ + + data.pre_chunk = NULL; + data.chunk = GET_ENC_CHUNK(enc_wr_index); + + /* end chunk */ + if (flags & CHUNKF_END_FILE) + { + data.chunk->flags &= CHUNKF_START_FILE | CHUNKF_END_FILE; + + if (data.chunk->flags & CHUNKF_START_FILE) + { + /* cannot start and end on same unprocessed chunk */ + logf("file end on start"); + flags &= ~CHUNKF_END_FILE; + } + else if (enc_rd_index == enc_wr_index) + { + /* all data flushed but file not ended - chunk will be left + empty */ + logf("end on dead end"); + data.chunk->flags = 0; + data.chunk->enc_size = 0; + data.chunk->num_pcm = 0; + data.chunk->enc_data = NULL; + INC_ENC_INDEX(enc_wr_index); + data.chunk = GET_ENC_CHUNK(enc_wr_index); + } + else + { + struct enc_chunk_hdr *last = pcmrec_get_prev_chunk(enc_wr_index); + + if (last->flags & CHUNKF_END_FILE) + { + /* end already processed and marked - can't end twice */ + logf("file end again"); + flags &= ~CHUNKF_END_FILE; + } + } + } + + /* start chunk */ + if (flags & CHUNKF_START_FILE) + { + bool pre = flags & CHUNKF_PRERECORD; + + if (pre) + { + logf("stream prerecord start"); + start = data.pre_chunk = GET_ENC_CHUNK(pre_index); + start->flags &= CHUNKF_START_FILE | CHUNKF_PRERECORD; + } + else + { + logf("stream normal start"); + start = data.chunk; + start->flags &= CHUNKF_START_FILE; + } + + /* if encoder hasn't yet processed the last start - abort the start + of the previous file queued or else it will be empty and invalid */ + if (start->flags & CHUNKF_START_FILE) + { + logf("replacing fnq tail: %s", filename); + fnq_add_fn = pcmrec_fnq_replace_tail; + } + else + { + logf("adding filename: %s", filename); + fnq_add_fn = pcmrec_fnq_add_filename; + } + } + + data.flags = flags; + pcmrec_context = true; /* switch encoder context */ + enc_events_callback(ENC_REC_NEW_STREAM, &data); + pcmrec_context = false; /* switch back */ + + if (flags & CHUNKF_END_FILE) + { + int i = pcmrec_get_chunk_index(data.chunk); + pcmrec_get_prev_chunk(i)->flags |= CHUNKF_END_FILE; + } + + if (start) + { + if (!(flags & CHUNKF_PRERECORD)) + { + /* get stats on data added to start - sort of a prerecord + operation */ + int i = pcmrec_get_chunk_index(data.chunk); + struct enc_chunk_hdr *chunk = data.chunk; + + logf("start data: %d %d", i, enc_wr_index); + + num_rec_bytes = 0; + num_rec_samples = 0; + + while (i != enc_wr_index) + { + num_rec_bytes += chunk->enc_size; + num_rec_samples += chunk->num_pcm; + INC_ENC_INDEX(i); + chunk = GET_ENC_CHUNK(i); + } + + start->flags &= ~CHUNKF_START_FILE; + start = data.chunk; + } + + start->flags |= CHUNKF_START_FILE; + + /* flush all pending files out if full and adding */ + if (fnq_add_fn == pcmrec_fnq_add_filename && pcmrec_fnq_is_full()) + { + logf("fnq full"); + pcmrec_flush(PCMREC_FLUSH_ALL); + did_flush = true; + } + + fnq_add_fn(path); + } + + /* Make sure to complete any interrupted high watermark */ + if (!did_flush) + pcmrec_flush(PCMREC_FLUSH_IF_HIGH); +} /* pcmrec_new_stream */ + +/** event handlers for pcmrec thread */ + +/* PCMREC_INIT */ +static void pcmrec_init(void) +{ + unsigned char *buffer; + + /* warings and errors */ + warnings = + errors = 0; + + pcmrec_close_file(&rec_fdata.rec_file); + rec_fdata.rec_file = -1; + + /* pcm FIFO */ + dma_lock = true; + pcm_rd_pos = 0; + dma_wr_pos = 0; + pcm_enc_pos = 0; + + /* encoder FIFO */ + enc_wr_index = 0; + enc_rd_index = 0; + + /* filename queue */ + fnq_rd_pos = 0; + fnq_wr_pos = 0; + + /* stats */ + num_rec_bytes = 0; + num_rec_samples = 0; +#if 0 + accum_rec_bytes = 0; + accum_pcm_samples = 0; +#endif + + pre_record_ticks = 0; + + is_recording = false; + is_paused = false; + + buffer = audio_get_recording_buffer(&rec_buffer_size); + + /* Line align pcm_buffer 2^4=16 bytes */ + pcm_buffer = (unsigned char *)ALIGN_UP_P2((uintptr_t)buffer, 4); + enc_buffer = pcm_buffer + ALIGN_UP_P2(PCM_NUM_CHUNKS*PCM_CHUNK_SIZE + + PCM_MAX_FEED_SIZE, 2); + /* Adjust available buffer for possible align advancement */ + rec_buffer_size -= pcm_buffer - buffer; + + pcm_init_recording(); +} /* pcmrec_init */ + +/* PCMREC_CLOSE */ +static void pcmrec_close(void) +{ + dma_lock = true; + pre_record_ticks = 0; /* Can't be prerecording any more */ + warnings = 0; + pcm_close_recording(); + reset_hardware(); + audio_remove_encoder(); +} /* pcmrec_close */ + +/* PCMREC_OPTIONS */ +static void pcmrec_set_recording_options( + struct audio_recording_options *options) +{ + /* stop DMA transfer */ + dma_lock = true; + pcm_stop_recording(); + + rec_frequency = options->rec_frequency; + rec_source = options->rec_source; + num_channels = options->rec_channels == 1 ? 1 : 2; + rec_mono_mode = options->rec_mono_mode; + pre_record_ticks = options->rec_prerecord_time * HZ; + enc_config = options->enc_config; + enc_config.afmt = rec_format_afmt[enc_config.rec_format]; + +#ifdef HAVE_SPDIF_IN + if (rec_source == AUDIO_SRC_SPDIF) + { + /* must measure SPDIF sample rate before configuring codecs */ + unsigned long sr = spdif_measure_frequency(); + /* round to master list for SPDIF rate */ + int index = round_value_to_list32(sr, audio_master_sampr_list, + SAMPR_NUM_FREQ, false); + sample_rate = audio_master_sampr_list[index]; + /* round to HW playback rates for monitoring */ + index = round_value_to_list32(sr, hw_freq_sampr, + HW_NUM_FREQ, false); + pcm_set_frequency(hw_freq_sampr[index]); + /* encoders with a limited number of rates do their own rounding */ + } + else +#endif + { + /* set sample rate from frequency selection */ + sample_rate = rec_freq_sampr[rec_frequency]; + pcm_set_frequency(sample_rate); + } + + /* set monitoring */ + audio_set_output_source(rec_source); + + /* apply hardware setting to start monitoring now */ + pcm_apply_settings(); + + queue_reply(&pcmrec_queue, 0); /* Release sender */ + + if (audio_load_encoder(enc_config.afmt)) + { + /* start DMA transfer */ + dma_lock = pre_record_ticks == 0; + pcm_record_data(pcm_rec_have_more, GET_PCM_CHUNK(dma_wr_pos), + PCM_CHUNK_SIZE); + } + else + { + logf("set rec opt: enc load failed"); + errors |= PCMREC_E_LOAD_ENCODER; + } +} /* pcmrec_set_recording_options */ + +/* PCMREC_RECORD - start recording (not gapless) + or split stream (gapless) */ +static void pcmrec_record(const char *filename) +{ + unsigned long pre_sample_ticks; + int rd_start; + unsigned long flags; + int pre_index; + + logf("pcmrec_record: %s", filename); + + /* reset stats */ + num_rec_bytes = 0; + num_rec_samples = 0; + + if (!is_recording) + { +#if 0 + accum_rec_bytes = 0; + accum_pcm_samples = 0; +#endif + warnings = 0; /* reset warnings */ + + rd_start = enc_wr_index; + pre_sample_ticks = 0; + + pcmrec_refresh_watermarks(); + + if (pre_record_ticks) + { + int i = rd_start; + /* calculate number of available chunks */ + unsigned long avail_pre_chunks = (enc_wr_index - enc_rd_index + + enc_num_chunks) % enc_num_chunks; + /* overflow at 974 seconds of prerecording at 44.1kHz */ + unsigned long pre_record_sample_ticks = + enc_sample_rate*pre_record_ticks; + int pre_chunks = 0; /* Counter to limit prerecorded time to + prevent flood state at outset */ + + logf("pre-st: %ld", pre_record_sample_ticks); + + /* Get exact measure of recorded data as number of samples aren't + nescessarily going to be the max for each chunk */ + for (; avail_pre_chunks-- > 0;) + { + struct enc_chunk_hdr *chunk; + unsigned long chunk_sample_ticks; + + DEC_ENC_INDEX(i); + + chunk = GET_ENC_CHUNK(i); + + /* must have data to be counted */ + if (chunk->enc_data == NULL) + continue; + + chunk_sample_ticks = chunk->num_pcm*HZ; + + rd_start = i; + pre_sample_ticks += chunk_sample_ticks; + num_rec_bytes += chunk->enc_size; + num_rec_samples += chunk->num_pcm; + pre_chunks++; + + /* stop here if enough already */ + if (pre_chunks >= high_watermark || + pre_sample_ticks >= pre_record_sample_ticks) + { + logf("pre-chks: %d", pre_chunks); + break; + } + } + +#if 0 + accum_rec_bytes = num_rec_bytes; + accum_pcm_samples = num_rec_samples; +#endif + } + + enc_rd_index = rd_start; + + /* filename queue should be empty */ + if (!pcmrec_fnq_is_empty()) + { + logf("fnq: not empty!"); + pcmrec_fnq_set_empty(); + } + + flags = CHUNKF_START_FILE; + if (pre_sample_ticks > 0) + flags |= CHUNKF_PRERECORD; + + pre_index = enc_rd_index; + + dma_lock = false; + is_paused = false; + is_recording = true; + } + else + { + /* already recording, just split the stream */ + logf("inserting split"); + flags = CHUNKF_START_FILE | CHUNKF_END_FILE; + pre_index = 0; + } + + pcmrec_new_stream(filename, flags, pre_index); + logf("pcmrec_record done"); +} /* pcmrec_record */ + +/* PCMREC_STOP */ +static void pcmrec_stop(void) +{ + logf("pcmrec_stop"); + + if (is_recording) + { + dma_lock = true; /* lock dma write position */ + + /* flush all available data first to avoid overflow while waiting + for encoding to finish */ + pcmrec_flush(PCMREC_FLUSH_ALL); + + /* wait for encoder to finish remaining data */ + while (errors == 0 && !pcm_buffer_empty) + yield(); + + /* end stream at last data */ + pcmrec_new_stream(NULL, CHUNKF_END_FILE, 0); + + /* flush anything else encoder added */ + pcmrec_flush(PCMREC_FLUSH_ALL); + + /* remove any pending file start not yet processed - should be at + most one at enc_wr_index */ + pcmrec_fnq_get_filename(NULL); + /* encoder should abort any chunk it was in midst of processing */ + GET_ENC_CHUNK(enc_wr_index)->flags = CHUNKF_ABORT; + + /* filename queue should be empty */ + if (!pcmrec_fnq_is_empty()) + { + logf("fnq: not empty!"); + pcmrec_fnq_set_empty(); + } + + /* be absolutely sure the file is closed */ + if (errors != 0) + pcmrec_close_file(&rec_fdata.rec_file); + rec_fdata.rec_file = -1; + + is_recording = false; + is_paused = false; + dma_lock = pre_record_ticks == 0; + } + else + { + logf("not recording"); + } + + logf("pcmrec_stop done"); +} /* pcmrec_stop */ + +/* PCMREC_PAUSE */ +static void pcmrec_pause(void) +{ + logf("pcmrec_pause"); + + if (!is_recording) + { + logf("not recording"); + } + else if (is_paused) + { + logf("already paused"); + } + else + { + dma_lock = true; + is_paused = true; + } + + logf("pcmrec_pause done"); +} /* pcmrec_pause */ + +/* PCMREC_RESUME */ +static void pcmrec_resume(void) +{ + logf("pcmrec_resume"); + + if (!is_recording) + { + logf("not recording"); + } + else if (!is_paused) + { + logf("not paused"); + } + else + { + is_paused = false; + is_recording = true; + dma_lock = false; + } + + logf("pcmrec_resume done"); +} /* pcmrec_resume */ + +static void pcmrec_thread(void) __attribute__((noreturn)); +static void pcmrec_thread(void) +{ + struct queue_event ev; + + logf("thread pcmrec start"); + + while(1) + { + if (is_recording) + { + /* Poll periodically to flush data */ + queue_wait_w_tmo(&pcmrec_queue, &ev, HZ/5); + + if (ev.id == SYS_TIMEOUT) + { + /* Messages that interrupt this will complete it */ + pcmrec_flush(PCMREC_FLUSH_IF_HIGH | + PCMREC_FLUSH_INTERRUPTABLE); + continue; + } + } + else + { + /* Not doing anything - sit and wait for commands */ + queue_wait(&pcmrec_queue, &ev); + } + + switch (ev.id) + { + case PCMREC_INIT: + pcmrec_init(); + break; + + case PCMREC_CLOSE: + pcmrec_close(); + break; + + case PCMREC_OPTIONS: + pcmrec_set_recording_options( + (struct audio_recording_options *)ev.data); + break; + + case PCMREC_RECORD: + clear_flush_interrupt(); + pcmrec_record((const char *)ev.data); + break; + + case PCMREC_STOP: + clear_flush_interrupt(); + pcmrec_stop(); + break; + + case PCMREC_PAUSE: + clear_flush_interrupt(); + pcmrec_pause(); + break; + + case PCMREC_RESUME: + pcmrec_resume(); + break; +#if 0 + case PCMREC_FLUSH_NUM: + pcmrec_flush((unsigned)ev.data); + break; +#endif + case SYS_USB_CONNECTED: + if (is_recording) + break; + pcmrec_close(); + usb_acknowledge(SYS_USB_CONNECTED_ACK); + usb_wait_for_disconnect(&pcmrec_queue); + flush_interrupts = 0; + break; + } /* end switch */ + } /* end while */ +} /* pcmrec_thread */ + +/****************************************************************************/ +/* */ +/* following functions will be called by the encoder codec */ +/* in a free-threaded manner */ +/* */ +/****************************************************************************/ + +/* pass the encoder settings to the encoder */ +void enc_get_inputs(struct enc_inputs *inputs) +{ + inputs->sample_rate = sample_rate; + inputs->num_channels = num_channels; + inputs->rec_mono_mode = rec_mono_mode; + inputs->config = &enc_config; +} /* enc_get_inputs */ + +/* set the encoder dimensions (called by encoder codec at initialization and + termination) */ +void enc_set_parameters(struct enc_parameters *params) +{ + size_t bufsize, resbytes; + + logf("enc_set_parameters"); + + if (!params) + { + logf("reset"); + /* Encoder is terminating */ + memset(&enc_config, 0, sizeof (enc_config)); + enc_sample_rate = 0; + cancel_cpu_boost(); /* Make sure no boost remains */ + return; + } + + enc_sample_rate = params->enc_sample_rate; + logf("enc sampr:%lu", enc_sample_rate); + + pcm_rd_pos = dma_wr_pos; + pcm_enc_pos = pcm_rd_pos; + + enc_config.afmt = params->afmt; + /* addition of the header is always implied - chunk size 4-byte aligned */ + enc_chunk_size = + ALIGN_UP_P2(ENC_CHUNK_HDR_SIZE + params->chunk_size, 2); + enc_events_callback = params->events_callback; + + logf("chunk size:%lu", enc_chunk_size); + + /*** Configure the buffers ***/ + + /* Layout of recording buffer: + * [ax] = possible alignment x multiple + * [sx] = possible size alignment of x multiple + * |[a16]|[s4]:PCM Buffer+PCM Guard|[s4 each]:Encoder Chunks|-> + * |[[s4]:Reserved Bytes]|Filename Queue->|[space]| + */ + resbytes = ALIGN_UP_P2(params->reserve_bytes, 2); + logf("resbytes:%lu", resbytes); + + bufsize = rec_buffer_size - (enc_buffer - pcm_buffer) - + resbytes - FNQ_MIN_NUM_PATHS*MAX_PATH +#ifdef DEBUG + - sizeof (*wrap_id_p) +#endif + ; + + enc_num_chunks = bufsize / enc_chunk_size; + logf("num chunks:%d", enc_num_chunks); + + /* get real amount used by encoder chunks */ + bufsize = enc_num_chunks*enc_chunk_size; + logf("enc size:%lu", bufsize); + +#ifdef DEBUG + /* add magic at wraparound for spillover checks */ + wrap_id_p = SKIPBYTES((unsigned long *)enc_buffer, bufsize); + bufsize += sizeof (*wrap_id_p); + *wrap_id_p = ENC_CHUNK_MAGIC; +#endif + + /** set OUT parameters **/ + params->enc_buffer = enc_buffer; + params->buf_chunk_size = enc_chunk_size; + params->num_chunks = enc_num_chunks; + + /* calculate reserve buffer start and return pointer to encoder */ + params->reserve_buffer = NULL; + if (resbytes > 0) + { + params->reserve_buffer = enc_buffer + bufsize; + bufsize += resbytes; + } + + /* place filename queue at end of buffer using up whatever remains */ + fnq_rd_pos = 0; /* reset */ + fnq_wr_pos = 0; /* reset */ + fn_queue = enc_buffer + bufsize; + fnq_size = pcm_buffer + rec_buffer_size - fn_queue; + fnq_size /= MAX_PATH; + if (fnq_size > FNQ_MAX_NUM_PATHS) + fnq_size = FNQ_MAX_NUM_PATHS; + fnq_size *= MAX_PATH; + logf("fnq files:%ld", fnq_size / MAX_PATH); + +#if defined(DEBUG) + logf("ab :%08lX", (uintptr_t)audiobuf); + logf("pcm:%08lX", (uintptr_t)pcm_buffer); + logf("enc:%08lX", (uintptr_t)enc_buffer); + logf("res:%08lX", (uintptr_t)params->reserve_buffer); + logf("wip:%08lX", (uintptr_t)wrap_id_p); + logf("fnq:%08lX", (uintptr_t)fn_queue); + logf("end:%08lX", (uintptr_t)fn_queue + fnq_size); + logf("abe:%08lX", (uintptr_t)audiobufend); +#endif + + /* init all chunk headers and reset indexes */ + enc_rd_index = 0; + for (enc_wr_index = enc_num_chunks; enc_wr_index > 0; ) + { + struct enc_chunk_hdr *chunk = GET_ENC_CHUNK(--enc_wr_index); +#ifdef DEBUG + chunk->id = ENC_CHUNK_MAGIC; +#endif + chunk->flags = 0; + } + + logf("enc_set_parameters done"); +} /* enc_set_parameters */ + +/* return encoder chunk at current write position - + NOTE: can be called by pcmrec thread when splitting streams */ +struct enc_chunk_hdr * enc_get_chunk(void) +{ + struct enc_chunk_hdr *chunk = GET_ENC_CHUNK(enc_wr_index); + +#ifdef DEBUG + if (chunk->id != ENC_CHUNK_MAGIC || *wrap_id_p != ENC_CHUNK_MAGIC) + { + errors |= PCMREC_E_CHUNK_OVF; + logf("finish chk ovf: %d", enc_wr_index); + } +#endif + + chunk->flags &= CHUNKF_START_FILE; + + if (!is_recording) + chunk->flags |= CHUNKF_PRERECORD; + + return chunk; +} /* enc_get_chunk */ + +/* releases the current chunk into the available chunks - + NOTE: can be called by pcmrec thread when splitting streams */ +void enc_finish_chunk(void) +{ + struct enc_chunk_hdr *chunk = GET_ENC_CHUNK(enc_wr_index); + + if ((long)chunk->flags < 0) + { + /* encoder set error flag */ + errors |= PCMREC_E_ENCODER; + logf("finish chk enc error"); + } + + /* advance enc_wr_index to the next encoder chunk */ + INC_ENC_INDEX(enc_wr_index); + + if (enc_rd_index != enc_wr_index) + { + num_rec_bytes += chunk->enc_size; + num_rec_samples += chunk->num_pcm; +#if 0 + accum_rec_bytes += chunk->enc_size; + accum_pcm_samples += chunk->num_pcm; +#endif + } + else if (is_recording) /* buffer full */ + { + /* keep current position and put up warning flag */ + warnings |= PCMREC_W_ENC_BUFFER_OVF; + logf("enc_buffer ovf"); + DEC_ENC_INDEX(enc_wr_index); + if (pcmrec_context) + { + /* if stream splitting, keep this out of circulation and + flush a small number, then readd - cannot risk losing + stream markers */ + logf("mini flush"); + pcmrec_flush(PCMREC_FLUSH_MINI); + INC_ENC_INDEX(enc_wr_index); + } + } + else + { + /* advance enc_rd_index for prerecording */ + INC_ENC_INDEX(enc_rd_index); + } +} /* enc_finish_chunk */ + +/* passes a pointer to next chunk of unprocessed wav data */ +/* TODO: this really should give the actual size returned */ +unsigned char * enc_get_pcm_data(size_t size) +{ + int wp = dma_wr_pos; + size_t avail = (wp - pcm_rd_pos) & PCM_CHUNK_MASK; + + /* limit the requested pcm data size */ + if (size > PCM_MAX_FEED_SIZE) + size = PCM_MAX_FEED_SIZE; + + if (avail >= size) + { + unsigned char *ptr = pcm_buffer + pcm_rd_pos; + int next_pos = (pcm_rd_pos + size) & PCM_CHUNK_MASK; + + pcm_enc_pos = pcm_rd_pos; + pcm_rd_pos = next_pos; + + /* ptr must point to continous data at wraparound position */ + if ((size_t)pcm_rd_pos < size) + { + memcpy(pcm_buffer + PCM_NUM_CHUNKS*PCM_CHUNK_SIZE, + pcm_buffer, pcm_rd_pos); + } + + if (avail >= (sample_rate << 2)) + { + /* Filling up - boost codec */ + trigger_cpu_boost(); + } + + pcm_buffer_empty = false; + return ptr; + } + + /* not enough data available - encoder should idle */ + pcm_buffer_empty = true; + + cancel_cpu_boost(); + + /* Sleep long enough to allow one frame on average */ + sleep(0); + + return NULL; +} /* enc_get_pcm_data */ + +/* puts some pcm data back in the queue */ +size_t enc_unget_pcm_data(size_t size) +{ + int wp = dma_wr_pos; + size_t old_avail = ((pcm_rd_pos - wp) & PCM_CHUNK_MASK) - + 2*PCM_CHUNK_SIZE; + + /* allow one interrupt to occur during this call and not have the + new read position inside the DMA destination chunk */ + if ((ssize_t)old_avail > 0) + { + /* limit size to amount of old data remaining */ + if (size > old_avail) + size = old_avail; + + pcm_enc_pos = (pcm_rd_pos - size) & PCM_CHUNK_MASK; + pcm_rd_pos = pcm_enc_pos; + + return size; + } + + return 0; +} /* enc_unget_pcm_data */ diff --git a/apps/recorder/pcm_record.h b/apps/recorder/pcm_record.h new file mode 100644 index 0000000000..f805313fe5 --- /dev/null +++ b/apps/recorder/pcm_record.h @@ -0,0 +1,66 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2005 by Linus Nielsen Feltzing + * + * 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. + * + ****************************************************************************/ + +#ifndef PCM_RECORD_H +#define PCM_RECORD_H + +#define DMA_REC_ERROR_DMA (-1) +#ifdef HAVE_SPDIF_REC +#define DMA_REC_ERROR_SPDIF (-2) +#endif + +/** Warnings **/ +/* pcm (dma) buffer has overflowed */ +#define PCMREC_W_PCM_BUFFER_OVF 0x00000001 +/* encoder output buffer has overflowed */ +#define PCMREC_W_ENC_BUFFER_OVF 0x00000002 +/** Errors **/ +/* failed to load encoder */ +#define PCMREC_E_LOAD_ENCODER 0x80001000 +/* error originating in encoder */ +#define PCMREC_E_ENCODER 0x80002000 +/* filename queue has desynced from stream markers */ +#define PCMREC_E_FNQ_DESYNC 0x80004000 +/* I/O error has occurred */ +#define PCMREC_E_IO 0x80008000 +#ifdef DEBUG +/* encoder has written past end of allotted space */ +#define PCMREC_E_CHUNK_OVF 0x80010000 +#endif /* DEBUG */ + +/** General functions for high level codec recording **/ +/* pcm_rec_error_clear is deprecated for general use. audio_error_clear + should be used */ +void pcm_rec_error_clear(void); +/* pcm_rec_status is deprecated for general use. audio_status merges the + results for consistency with the hardware codec version */ +unsigned long pcm_rec_status(void); +unsigned long pcm_rec_get_warnings(void); +void pcm_rec_init(void); +int pcm_rec_current_bitrate(void); +int pcm_rec_encoder_afmt(void); /* AFMT_* value, AFMT_UNKNOWN if none */ +int pcm_rec_rec_format(void); /* Format index or -1 otherwise */ +unsigned long pcm_rec_sample_rate(void); +int pcm_get_num_unprocessed(void); + +/* audio.h contains audio_* recording functions */ + +#endif /* PCM_RECORD_H */ diff --git a/apps/recorder/recording.c b/apps/recorder/recording.c index 6f5c7df09a..d29db390c7 100644 --- a/apps/recorder/recording.c +++ b/apps/recorder/recording.c @@ -40,6 +40,7 @@ #include "spdif.h" #endif #endif /* CONFIG_CODEC == SWCODEC */ +#include "pcm_record.h" #include "recording.h" #include "mp3_playback.h" #include "mas.h" diff --git a/apps/replaygain.h b/apps/replaygain.h new file mode 100644 index 0000000000..dbc079b1d3 --- /dev/null +++ b/apps/replaygain.h @@ -0,0 +1,33 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2005 Magnus Holmgren + * + * 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. + * + ****************************************************************************/ + +#ifndef _REPLAYGAIN_H +#define _REPLAYGAIN_H + +#include "id3.h" + +long get_replaygain_int(long int_gain); +long parse_replaygain(const char* key, const char* value, + struct mp3entry* entry, char* buffer, int length); +long parse_replaygain_int(bool album, long gain, long peak, + struct mp3entry* entry, char* buffer, int length); + +#endif -- cgit v1.2.3