diff options
author | Kyle McMartin <kyle@debian.org> | 2004-06-06 01:23:28 +0200 |
---|---|---|
committer | Kyle McMartin <kyle@debian.org> | 2004-06-06 01:23:28 +0200 |
commit | 194c0bbde98196ce6871bd21a9e3d52a771f8dfd (patch) | |
tree | f77af65ebcb4c1fa9d33dce45d206407a98c646d /player.c |
Import madplay_0.15.2b.orig.tar.gz
[dgit import orig madplay_0.15.2b.orig.tar.gz]
Diffstat (limited to 'player.c')
-rw-r--r-- | player.c | 2818 |
1 files changed, 2818 insertions, 0 deletions
diff --git a/player.c b/player.c new file mode 100644 index 0000000..370646a --- /dev/null +++ b/player.c @@ -0,0 +1,2818 @@ +/* + * madplay - MPEG audio decoder and player + * Copyright (C) 2000-2004 Robert Leslie + * + * 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 program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * $Id: player.c,v 1.69 2004/02/23 21:34:53 rob Exp $ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include "global.h" + +# include <stdio.h> +# include <stdarg.h> +# include <stdlib.h> + +# ifdef HAVE_SYS_TYPES_H +# include <sys/types.h> +# endif + +# include <sys/stat.h> + +# ifdef HAVE_FCNTL_H +# include <fcntl.h> +# endif + +# ifdef HAVE_UNISTD_H +# include <unistd.h> +# endif + +# include <string.h> + +# ifdef HAVE_ERRNO_H +# include <errno.h> +# endif + +# include <time.h> +# include <locale.h> +# include <math.h> + +# ifdef HAVE_TERMIOS_H +# include <termios.h> +# endif + +# ifdef _WIN32 +# include <windows.h> +# endif + +# include <signal.h> + +# ifdef HAVE_ASSERT_H +# include <assert.h> +# endif + +# if defined(HAVE_MMAP) +# include <sys/mman.h> +# endif + +# if !defined(O_BINARY) +# define O_BINARY 0 +# endif + +# include <mad.h> +# include <id3tag.h> + +# include "gettext.h" + +# include "player.h" +# include "audio.h" +# include "resample.h" +# include "filter.h" +# include "tag.h" +# include "rgain.h" + +# define MPEG_BUFSZ 40000 /* 2.5 s at 128 kbps; 1 s at 320 kbps */ +# define FREQ_TOLERANCE 2 /* percent sampling frequency tolerance */ + +# define TTY_DEVICE "/dev/tty" + +# define KEY_CTRL(key) ((key) & 0x1f) + +enum { + KEY_PAUSE = 'p', + KEY_STOP = 's', + KEY_FORWARD = 'f', + KEY_BACK = 'b', + KEY_TIME = 't', + KEY_QUIT = 'q', + KEY_INFO = 'i', + KEY_GAINDECR = '-', + KEY_GAININCR = '+', + KEY_GAINZERO = '_', + KEY_GAININFO = '=' +}; + +static int on_same_line; + +# if defined(USE_TTY) && !defined(_WIN32) +static int tty_fd = -1; +static struct termios save_tty; +static struct sigaction save_sigtstp, save_sigint; +# endif + +/* + * NAME: player_init() + * DESCRIPTION: initialize player structure + */ +void player_init(struct player *player) +{ + player->verbosity = 0; + + player->options = 0; + player->repeat = 1; + + player->control = PLAYER_CONTROL_DEFAULT; + + player->playlist.entries = 0; + player->playlist.length = 0; + player->playlist.current = 0; + + player->global_start = mad_timer_zero; + player->global_stop = mad_timer_zero; + + player->fade_in = mad_timer_zero; + player->fade_out = mad_timer_zero; + player->gap = mad_timer_zero; + + player->input.path = 0; + player->input.fd = -1; +# if defined(HAVE_MMAP) + player->input.fdm = 0; +# endif + player->input.data = 0; + player->input.length = 0; + player->input.eof = 0; + + tag_init(&player->input.tag); + + player->output.mode = AUDIO_MODE_DITHER; + player->output.voladj_db = 0; + player->output.attamp_db = 0; + player->output.gain = MAD_F_ONE; + player->output.replay_gain = 0; + player->output.filters = 0; + player->output.channels_in = 0; + player->output.channels_out = 0; + player->output.select = PLAYER_CHANNEL_DEFAULT; + player->output.speed_in = 0; + player->output.speed_out = 0; + player->output.speed_request = 0; + player->output.precision_in = 0; + player->output.precision_out = 0; + player->output.path = 0; + player->output.command = 0; + /* player->output.resample */ + player->output.resampled = 0; + + player->ancillary.path = 0; + player->ancillary.file = 0; + player->ancillary.buffer = 0; + player->ancillary.length = 0; + + player->stats.show = STATS_SHOW_OVERALL; + player->stats.label = 0; + player->stats.total_bytes = 0; + player->stats.total_time = mad_timer_zero; + player->stats.global_timer = mad_timer_zero; + player->stats.absolute_timer = mad_timer_zero; + player->stats.play_timer = mad_timer_zero; + player->stats.global_framecount = 0; + player->stats.absolute_framecount = 0; + player->stats.play_framecount = 0; + player->stats.error_frame = -1; + player->stats.mute_frame = 0; + player->stats.vbr = 0; + player->stats.bitrate = 0; + player->stats.vbr_frames = 0; + player->stats.vbr_rate = 0; + player->stats.nsecs = 0; + player->stats.audio.clipped_samples = 0; + player->stats.audio.peak_clipping = 0; + player->stats.audio.peak_sample = 0; +} + +/* + * NAME: player_finish() + * DESCRIPTION: destroy a player structure + */ +void player_finish(struct player *player) +{ + if (player->output.filters) + filter_free(player->output.filters); + + if (player->output.resampled) { + resample_finish(&player->output.resample[0]); + resample_finish(&player->output.resample[1]); + + free(player->output.resampled); + player->output.resampled = 0; + } +} + +/* + * NAME: message() + * DESCRIPTION: show a message, possibly overwriting a previous w/o newline + */ +static +int message(char const *format, ...) +{ + int len, newline, result; + va_list args; + + len = strlen(format); + newline = (len > 0 && format[len - 1] == '\n'); + + if (on_same_line && newline && len > 1) + fputc('\n', stderr); + + va_start(args, format); + result = vfprintf(stderr, format, args); + va_end(args); + + if (on_same_line && !newline && result < on_same_line) { + unsigned int i; + + i = on_same_line - result; + while (i--) + putc(' ', stderr); + } + + on_same_line = newline ? 0 : result; + + if (!newline) { + fputc('\r', stderr); + fflush(stderr); + } + + return result; +} + +/* + * NAME: detail() + * DESCRIPTION: show right-aligned label and line-wrap corresponding text + */ +static +void detail(char const *label, char const *format, ...) +{ + char const spaces[] = " "; + va_list args; + +# define LINEWRAP (80 - sizeof(spaces) - 2 - 2) + + if (on_same_line) + message("\n"); + + if (label) { + unsigned int len; + + len = strlen(label); + assert(len < sizeof(spaces)); + + fprintf(stderr, "%s%s: ", &spaces[len], label); + } + else + fprintf(stderr, "%s ", spaces); + + va_start(args, format); + + if (format) { + vfprintf(stderr, format, args); + fputc('\n', stderr); + } + else { + char *ptr, *newline, *linebreak; + + /* N.B. this argument must be mutable! */ + ptr = va_arg(args, char *); + + do { + newline = strchr(ptr, '\n'); + if (newline) + *newline = 0; + + if (strlen(ptr) > LINEWRAP) { + linebreak = ptr + LINEWRAP; + + while (linebreak > ptr && *linebreak != ' ') + --linebreak; + + if (*linebreak == ' ') { + if (newline) + *newline = '\n'; + + *(newline = linebreak) = 0; + } + } + + fprintf(stderr, "%s\n", ptr); + + if (newline) { + ptr = newline + 1; + fprintf(stderr, "%s ", spaces); + } + } + while (newline); + } + + va_end(args); +} + +/* + * NAME: error() + * DESCRIPTION: show an error using proper interaction with message() + */ +static +void error(char const *id, char const *format, ...) +{ + int err; + va_list args; + + err = errno; + + if (on_same_line) + message("\n"); + + if (id) + fprintf(stderr, "%s: ", id); + + va_start(args, format); + + if (*format == ':') { + if (format[1] == 0) { + format = va_arg(args, char const *); + errno = err; + perror(format); + } + else { + errno = err; + perror(format + 1); + } + } + else { + vfprintf(stderr, format, args); + fputc('\n', stderr); + } + + va_end(args); +} + +# if defined(HAVE_MMAP) +/* + * NAME: map_file() + * DESCRIPTION: map the contents of a file into memory + */ +static +void *map_file(int fd, unsigned long length) +{ + void *fdm; + + fdm = mmap(0, length, PROT_READ, MAP_SHARED, fd, 0); + if (fdm == MAP_FAILED) + return 0; + +# if defined(HAVE_MADVISE) + madvise(fdm, length, MADV_SEQUENTIAL); +# endif + + return fdm; +} + +/* + * NAME: unmap_file() + * DESCRIPTION: undo a file mapping + */ +static +int unmap_file(void *fdm, unsigned long length) +{ + if (munmap(fdm, length) == -1) + return -1; + + return 0; +} + +/* + * NAME: decode->input_mmap() + * DESCRIPTION: (re)fill decoder input buffer from a memory map + */ +static +enum mad_flow decode_input_mmap(void *data, struct mad_stream *stream) +{ + struct player *player = data; + struct input *input = &player->input; + + if (input->eof) + return MAD_FLOW_STOP; + + if (stream->next_frame) { + struct stat stat; + unsigned long posn, left; + + if (fstat(input->fd, &stat) == -1) + return MAD_FLOW_BREAK; + + posn = stream->next_frame - input->fdm; + + /* check for file size change and update map */ + + if (stat.st_size > input->length) { + if (unmap_file(input->fdm, input->length) == -1) { + input->fdm = 0; + input->data = 0; + return MAD_FLOW_BREAK; + } + + player->stats.total_bytes += stat.st_size - input->length; + + input->length = stat.st_size; + + input->fdm = map_file(input->fd, input->length); + if (input->fdm == 0) { + input->data = 0; + return MAD_FLOW_BREAK; + } + + mad_stream_buffer(stream, input->fdm + posn, input->length - posn); + + return MAD_FLOW_CONTINUE; + } + + /* end of memory map; append MAD_BUFFER_GUARD zero bytes */ + + left = input->length - posn; + + input->data = malloc(left + MAD_BUFFER_GUARD); + if (input->data == 0) + return MAD_FLOW_BREAK; + + input->eof = 1; + + memcpy(input->data, input->fdm + posn, left); + memset(input->data + left, 0, MAD_BUFFER_GUARD); + + mad_stream_buffer(stream, input->data, left + MAD_BUFFER_GUARD); + + return MAD_FLOW_CONTINUE; + } + + /* first call */ + + mad_stream_buffer(stream, input->fdm, input->length); + + return MAD_FLOW_CONTINUE; +} +# endif + +/* + * NAME: decode->input_read() + * DESCRIPTION: (re)fill decoder input buffer by reading a file descriptor + */ +static +enum mad_flow decode_input_read(void *data, struct mad_stream *stream) +{ + struct player *player = data; + struct input *input = &player->input; + int len; + + if (input->eof) + return MAD_FLOW_STOP; + + if (stream->next_frame) { + memmove(input->data, stream->next_frame, + input->length = &input->data[input->length] - stream->next_frame); + } + + do { + len = read(input->fd, input->data + input->length, + MPEG_BUFSZ - input->length); + } + while (len == -1 && errno == EINTR); + + if (len == -1) { + error("input", ":read"); + return MAD_FLOW_BREAK; + } + else if (len == 0) { + input->eof = 1; + + assert(MPEG_BUFSZ - input->length >= MAD_BUFFER_GUARD); + + while (len < MAD_BUFFER_GUARD) + input->data[input->length + len++] = 0; + } + + mad_stream_buffer(stream, input->data, input->length += len); + + return MAD_FLOW_CONTINUE; +} + +/* + * NAME: decode->header() + * DESCRIPTION: decide whether to continue decoding based on header + */ +static +enum mad_flow decode_header(void *data, struct mad_header const *header) +{ + struct player *player = data; + + if ((player->options & PLAYER_OPTION_TIMED) && + mad_timer_compare(player->stats.global_timer, player->global_stop) > 0) + return MAD_FLOW_STOP; + + /* accounting (except first frame) */ + + if (player->stats.absolute_framecount) { + ++player->stats.absolute_framecount; + mad_timer_add(&player->stats.absolute_timer, header->duration); + + ++player->stats.global_framecount; + mad_timer_add(&player->stats.global_timer, header->duration); + + if ((player->options & PLAYER_OPTION_SKIP) && + mad_timer_compare(player->stats.global_timer, + player->global_start) < 0) + return MAD_FLOW_IGNORE; + } + + return MAD_FLOW_CONTINUE; +} + +/* + * NAME: write_ancillary() + * DESCRIPTION: pack ancillary data into octets and output + */ +static +int write_ancillary(struct ancillary *ancillary, + struct mad_bitptr ptr, unsigned int length) +{ + if (ancillary->length) { + unsigned int balance; + + balance = 8 - ancillary->length; + if (balance > length) { + ancillary->buffer = + (ancillary->buffer << length) | mad_bit_read(&ptr, length); + ancillary->length += length; + + return 0; + } + else { + if (fputc((ancillary->buffer << balance) | mad_bit_read(&ptr, balance), + ancillary->file) == EOF) { + error("ancillary", ":fputc"); + return -1; + } + ancillary->length = 0; + + length -= balance; + } + } + + while (length >= 8) { + int byte; + + byte = mad_bit_read(&ptr, 8); + if (putc(byte, ancillary->file) == EOF) { + error("ancillary", ":putc"); + return -1; + } + + length -= 8; + } + + if (length) { + ancillary->buffer = mad_bit_read(&ptr, length); + ancillary->length = length; + } + + if (fflush(ancillary->file) == EOF) { + error("ancillary", ":fflush"); + return -1; + } + + return 0; +} + +/* + * NAME: show_id3() + * DESCRIPTION: display ID3 tag information + */ +static +void show_id3(struct id3_tag const *tag) +{ + unsigned int i; + struct id3_frame const *frame; + id3_ucs4_t const *ucs4; + id3_latin1_t *latin1; + + static struct { + char const *id; + char const *label; + } const info[] = { + { ID3_FRAME_TITLE, N_("Title") }, + { "TIT3", 0 }, /* Subtitle */ + { "TCOP", 0 }, /* Copyright */ + { "TPRO", 0 }, /* Produced */ + { "TCOM", N_("Composer") }, + { ID3_FRAME_ARTIST, N_("Artist") }, + { "TPE2", N_("Orchestra") }, + { "TPE3", N_("Conductor") }, + { "TEXT", N_("Lyricist") }, + { ID3_FRAME_ALBUM, N_("Album") }, + { ID3_FRAME_TRACK, N_("Track") }, + { ID3_FRAME_YEAR, N_("Year") }, + { "TPUB", N_("Publisher") }, + { ID3_FRAME_GENRE, N_("Genre") }, + { "TRSN", N_("Station") }, + { "TENC", N_("Encoder") } + }; + + /* text information */ + + for (i = 0; i < sizeof(info) / sizeof(info[0]); ++i) { + union id3_field const *field; + unsigned int nstrings, j; + + frame = id3_tag_findframe(tag, info[i].id, 0); + if (frame == 0) + continue; + + field = id3_frame_field(frame, 1); + nstrings = id3_field_getnstrings(field); + + for (j = 0; j < nstrings; ++j) { + ucs4 = id3_field_getstrings(field, j); + assert(ucs4); + + if (strcmp(info[i].id, ID3_FRAME_GENRE) == 0) + ucs4 = id3_genre_name(ucs4); + + latin1 = id3_ucs4_latin1duplicate(ucs4); + if (latin1 == 0) + goto fail; + + if (j == 0 && info[i].label) + detail(gettext(info[i].label), 0, latin1); + else { + if (strcmp(info[i].id, "TCOP") == 0 || + strcmp(info[i].id, "TPRO") == 0) { + detail(0, "%s %s", (info[i].id[1] == 'C') ? + _("Copyright (C)") : _("Produced (P)"), latin1); + } + else + detail(0, 0, latin1); + } + + free(latin1); + } + } + + /* comments */ + + i = 0; + while ((frame = id3_tag_findframe(tag, ID3_FRAME_COMMENT, i++))) { + ucs4 = id3_field_getstring(id3_frame_field(frame, 2)); + assert(ucs4); + + if (*ucs4) + continue; + + ucs4 = id3_field_getfullstring(id3_frame_field(frame, 3)); + assert(ucs4); + + latin1 = id3_ucs4_latin1duplicate(ucs4); + if (latin1 == 0) + goto fail; + + detail(_("Comment"), 0, latin1); + + free(latin1); + break; + } + + if (0) { + fail: + error("id3", _("not enough memory to display tag")); + } +} + +/* + * NAME: show_rgain() + * DESCRIPTION: display Replay Gain information + */ +static +void show_rgain(struct rgain const *rgain) +{ + char const *label, *source; + + if (!RGAIN_VALID(rgain)) + return; + + label = 0; + switch (rgain->name) { + case RGAIN_NAME_NOT_SET: + break; + case RGAIN_NAME_RADIO: + label = _("Radio Gain"); + break; + case RGAIN_NAME_AUDIOPHILE: + label = _("Audiophile Gain"); + break; + } + + source = rgain_originator(rgain); + + assert(label && source); + + detail(label, "%+.1f dB => %d dB SPL (%s)", + RGAIN_DB(rgain), (int) RGAIN_REFERENCE, source); +} + +/* + * NAME: show_tag() + * DESCRIPTION: display Xing/LAME tag information + */ +static +void show_tag(struct tag const *tag) +{ + char ident[22]; + int i; + + memcpy(ident, tag->encoder, 21); + + /* separate version number from encoder name */ + + for (i = 0; i < 20; ++i) { + if (ident[i] == 0) + break; + + if (ident[i] >= '0' && ident[i] <= '9') { + if (i > 0 && ident[i - 1] != ' ' && ident[i - 1] != 'v') { + memmove(&ident[i + 1], &ident[i], 21 - i); + ident[i] = ' '; + } + + break; + } + } + + if (ident[0]) + detail(_("Encoder Version"), "%s", ident); + + if (tag->flags & TAG_LAME) { + char const *text; + +# if 0 + detail(_("Tag Revision"), "%u", tag->lame.revision); +# endif + + text = 0; + switch (tag->lame.vbr_method) { + case TAG_LAME_VBR_CONSTANT: + text = _("constant"); + break; + case TAG_LAME_VBR_ABR: + text = _("ABR"); + break; + case TAG_LAME_VBR_METHOD1: + text = _("1 (old/rh)"); + break; + case TAG_LAME_VBR_METHOD2: + text = _("2 (mtrh)"); + break; + case TAG_LAME_VBR_METHOD3: + text = _("3 (mt)"); + break; + case TAG_LAME_VBR_METHOD4: + text = _("4"); + break; + case TAG_LAME_VBR_CONSTANT2PASS: + text = _("constant (two-pass)"); + break; + case TAG_LAME_VBR_ABR2PASS: + text = _("ABR (two-pass)"); + break; + } + detail(_("VBR Method"), "%s", text ? text : _("unknown")); + + text = 0; + switch (tag->lame.vbr_method) { + case TAG_LAME_VBR_CONSTANT: + case TAG_LAME_VBR_CONSTANT2PASS: + text = _("Bitrate"); + break; + case TAG_LAME_VBR_ABR: + case TAG_LAME_VBR_ABR2PASS: + text = _("Target Bitrate"); + break; + case TAG_LAME_VBR_METHOD1: + case TAG_LAME_VBR_METHOD2: + case TAG_LAME_VBR_METHOD3: + case TAG_LAME_VBR_METHOD4: + text = _("Minimum Bitrate"); + break; + } + if (text) { + detail(text, _("%u%s kbps"), tag->lame.bitrate, + tag->lame.bitrate == 255 ? "+" : ""); + } + + text = 0; + switch (tag->lame.stereo_mode) { + case TAG_LAME_MODE_MONO: + text = _("mono"); + break; + case TAG_LAME_MODE_STEREO: + text = _("normal"); + break; + case TAG_LAME_MODE_DUAL: + text = _("dual channel"); + break; + case TAG_LAME_MODE_JOINT: + text = _("joint"); + break; + case TAG_LAME_MODE_FORCE: + text = _("force"); + break; + case TAG_LAME_MODE_AUTO: + text = _("auto"); + break; + case TAG_LAME_MODE_INTENSITY: + text = _("intensity"); + break; + case TAG_LAME_MODE_UNDEFINED: + text = _("undefined"); + break; + } + if (text) + detail(_("Stereo Mode"), "%s", text); + + if (tag->lame.preset >= 8 && tag->lame.preset <= 320) + detail(_("Preset"), _("ABR %u"), tag->lame.preset); + else { + text = 0; + switch (tag->lame.preset) { + case TAG_LAME_PRESET_NONE: + text = _("none"); + break; + case TAG_LAME_PRESET_V9: + text = _("V9"); + break; + case TAG_LAME_PRESET_V8: + text = _("V8"); + break; + case TAG_LAME_PRESET_V7: + text = _("V7"); + break; + case TAG_LAME_PRESET_V6: + text = _("V6"); + break; + case TAG_LAME_PRESET_V5: + text = _("V5"); + break; + case TAG_LAME_PRESET_V4: + text = _("V4"); + break; + case TAG_LAME_PRESET_V3: + text = _("V3"); + break; + case TAG_LAME_PRESET_V2: + text = _("V2"); + break; + case TAG_LAME_PRESET_V1: + text = _("V1"); + break; + case TAG_LAME_PRESET_V0: + text = _("V0"); + break; + case TAG_LAME_PRESET_R3MIX: + text = _("r3mix"); + break; + case TAG_LAME_PRESET_STANDARD: + text = _("standard"); + break; + case TAG_LAME_PRESET_EXTREME: + text = _("extreme"); + break; + case TAG_LAME_PRESET_INSANE: + text = _("insane"); + break; + case TAG_LAME_PRESET_STANDARD_FAST: + text = _("standard/fast"); + break; + case TAG_LAME_PRESET_EXTREME_FAST: + text = _("extreme/fast"); + break; + case TAG_LAME_PRESET_MEDIUM: + text = _("medium"); + break; + case TAG_LAME_PRESET_MEDIUM_FAST: + text = _("medium/fast"); + break; + } + detail(_("Preset"), "%s", text ? text : _("unknown")); + } + + detail(_("Unwise Settings"), "%s", + (tag->lame.flags & TAG_LAME_UNWISE) ? _("yes") : _("no")); + + detail(_("Encoding Flags"), "%s%s%s", + (tag->lame.flags & TAG_LAME_NSPSYTUNE) ? "--nspsytune " : "", + (tag->lame.flags & TAG_LAME_NSSAFEJOINT) ? "--nssafejoint " : "", + (tag->lame.flags & (TAG_LAME_NOGAP_NEXT | TAG_LAME_NOGAP_PREV)) ? + "--nogap" : ""); + + text = 0; + switch (tag->lame.flags & (TAG_LAME_NOGAP_NEXT | TAG_LAME_NOGAP_PREV)) { + case TAG_LAME_NOGAP_NEXT: + text = _("following"); + break; + case TAG_LAME_NOGAP_PREV: + text = _("preceding"); + break; + case TAG_LAME_NOGAP_NEXT | TAG_LAME_NOGAP_PREV: + text = _("following or preceding"); + break; + } + if (text) + detail(_("No Gap"), "%s", text); + + text = _("Lowpass Filter"); + if (tag->lame.lowpass_filter == 0) + detail(text, "%s", _("unknown")); + else + detail(text, _("%u Hz"), tag->lame.lowpass_filter); + + detail(_("ATH Type"), "%u", tag->lame.ath_type); + + detail(_("Noise Shaping"), "%u", tag->lame.noise_shaping); + + switch (tag->lame.surround) { + case TAG_LAME_SURROUND_NONE: + text = _("none"); + break; + case TAG_LAME_SURROUND_DPL: + text = _("DPL"); + break; + case TAG_LAME_SURROUND_DPL2: + text = _("DPL2"); + break; + case TAG_LAME_SURROUND_AMBISONIC: + text = _("Ambisonic"); + break; + default: + text = _("unknown"); + } + detail(_("Surround"), "%s", text); + + detail(_("Start Delay"), _("%u samples"), tag->lame.start_delay); + detail(_("End Padding"), _("%u samples"), tag->lame.end_padding); + + text = 0; + switch (tag->lame.source_samplerate) { + case TAG_LAME_SOURCE_32LOWER: + text = _("32 kHz or lower"); + break; + case TAG_LAME_SOURCE_44_1: + text = _("44.1 kHz"); + break; + case TAG_LAME_SOURCE_48: + text = _("48 kHz"); + break; + case TAG_LAME_SOURCE_HIGHER48: + text = _("higher than 48 kHz"); + break; + } + if (text) + detail(_("Source Rate"), "%s", text); + + if (tag->lame.gain != 0) + detail(_("Gain"), _("%+.1f dB"), tag->lame.gain * 1.5); + + /* Replay Gain */ + + if (tag->lame.peak > 0) { + double peak = mad_f_todouble(tag->lame.peak); + + detail(_("Peak Amplitude"), _("%.8f (%+.1f dB)"), + peak, 20 * log10(peak)); + } + + if (tag->lame.replay_gain[0].name == RGAIN_NAME_RADIO) + show_rgain(&tag->lame.replay_gain[0]); + if (tag->lame.replay_gain[1].name == RGAIN_NAME_AUDIOPHILE) + show_rgain(&tag->lame.replay_gain[1]); + + detail(_("Music Length"), _("%lu bytes"), tag->lame.music_length); +# if 0 + detail(_("Music CRC"), "0x%04x", tag->lame.music_crc); +# endif + } + + if (tag->flags & TAG_XING) { + if (tag->xing.flags & TAG_XING_FRAMES) + detail(_("Audio Frames"), "%lu", tag->xing.frames); + + if ((tag->xing.flags & TAG_XING_BYTES) && + (!(tag->flags & TAG_LAME) || + tag->lame.music_length != tag->xing.bytes)) + detail(_("Data Bytes"), "%lu", tag->xing.bytes); + + if ((tag->flags & TAG_VBR) && (tag->xing.flags & TAG_XING_SCALE)) + detail(_("VBR Scale"), _("%ld/100"), 100 - tag->xing.scale); + } +} + +enum { + GAIN_VOLADJ = 0x0001, + GAIN_ATTAMP = 0x0002, + GAIN_RELATIVE = 0x0010 +}; + +/* + * NAME: set_gain() + * DESCRIPTION: modify player gain information + */ +static +double set_gain(struct player *player, int how, double db) +{ + double *gain_db = 0; + + if (how & GAIN_ATTAMP) + gain_db = &player->output.attamp_db; + else if (how & GAIN_VOLADJ) + gain_db = &player->output.voladj_db; + + if (gain_db) { + if (how & GAIN_RELATIVE) + *gain_db += db; + else + *gain_db = db; + } + + db = player->output.voladj_db + player->output.attamp_db; + if (db > DB_MAX || db < DB_MIN) { + db = (db > DB_MAX) ? DB_MAX : DB_MIN; + player->output.attamp_db = db - player->output.voladj_db; + } + + player->output.gain = db ? mad_f_tofixed(pow(10, db / 20)) : MAD_F_ONE; + + return db; +} + +/* + * NAME: use_rgain() + * DESCRIPTION: select and employ a Replay Gain volume adjustment + */ +static +void use_rgain(struct player *player, struct rgain *list) +{ + struct rgain *rgain = &list[0]; + + if ((player->output.replay_gain & PLAYER_RGAIN_AUDIOPHILE) && + list[1].name == RGAIN_NAME_AUDIOPHILE && + list[1].originator != RGAIN_ORIGINATOR_UNSPECIFIED) + rgain = &list[1]; + + if (RGAIN_VALID(rgain)) { + double gain = RGAIN_DB(rgain); + + set_gain(player, GAIN_VOLADJ, gain); + + if (player->verbosity >= 0 || + (player->options & PLAYER_OPTION_SHOWTAGSONLY)) { + char const *source; + + source = rgain_originator(rgain); + assert(source); + + detail(_("Replay Gain"), _("%+.1f dB %s adjustment (%s)"), + gain, (rgain->name == RGAIN_NAME_RADIO) ? + _("radio") : _("audiophile"), source); + } + + player->output.replay_gain |= PLAYER_RGAIN_SET; + } +} + +/* + * NAME: decode->filter() + * DESCRIPTION: perform filtering on decoded frame + */ +static +enum mad_flow decode_filter(void *data, struct mad_stream const *stream, + struct mad_frame *frame) +{ + struct player *player = data; + + /* output ancillary data */ + + if (player->ancillary.file && stream->anc_bitlen && + write_ancillary(&player->ancillary, + stream->anc_ptr, stream->anc_bitlen) == -1) + return MAD_FLOW_BREAK; + + /* first frame accounting */ + + if (player->stats.absolute_framecount == 0) { + if (player->input.tag.flags == 0 && + tag_parse(&player->input.tag, stream) == 0) { + struct tag *tag = &player->input.tag; + unsigned int frame_size; + + if (player->options & PLAYER_OPTION_SHOWTAGSONLY) { + if (player->verbosity > 0) + show_tag(tag); + } + else { + if ((tag->flags & TAG_LAME) && + (player->output.replay_gain & PLAYER_RGAIN_ENABLED) && + !(player->output.replay_gain & PLAYER_RGAIN_SET)) + use_rgain(player, tag->lame.replay_gain); + } + + if ((tag->flags & TAG_XING) && + (tag->xing.flags & TAG_XING_FRAMES)) { + player->stats.total_time = frame->header.duration; + mad_timer_multiply(&player->stats.total_time, tag->xing.frames); + } + + /* total stream byte size adjustment */ + + frame_size = stream->next_frame - stream->this_frame; + + if (player->stats.total_bytes == 0) { + if ((tag->flags & TAG_XING) && (tag->xing.flags & TAG_XING_BYTES) && + tag->xing.bytes > frame_size) + player->stats.total_bytes = tag->xing.bytes - frame_size; + } + else if (player->stats.total_bytes >= + stream->next_frame - stream->this_frame) + player->stats.total_bytes -= frame_size; + + return (player->options & PLAYER_OPTION_SHOWTAGSONLY) ? + MAD_FLOW_STOP : MAD_FLOW_IGNORE; + } + else if (player->options & PLAYER_OPTION_SHOWTAGSONLY) + return MAD_FLOW_STOP; + + ++player->stats.absolute_framecount; + mad_timer_add(&player->stats.absolute_timer, frame->header.duration); + + ++player->stats.global_framecount; + mad_timer_add(&player->stats.global_timer, frame->header.duration); + + if ((player->options & PLAYER_OPTION_SKIP) && + mad_timer_compare(player->stats.global_timer, + player->global_start) < 0) + return MAD_FLOW_IGNORE; + } + + /* run the filter chain */ + + return filter_run(player->output.filters, frame); +} + +/* + * NAME: process_id3() + * DESCRIPTION: display and process ID3 tag information + */ +static +void process_id3(struct id3_tag const *tag, struct player *player) +{ + struct id3_frame const *frame; + + /* display the tag */ + + if (player->verbosity >= 0 || (player->options & PLAYER_OPTION_SHOWTAGSONLY)) + show_id3(tag); + + /* + * The following is based on information from the + * ID3 tag version 2.4.0 Native Frames informal standard. + */ + + /* length information */ + + frame = id3_tag_findframe(tag, "TLEN", 0); + if (frame) { + union id3_field const *field; + unsigned int nstrings; + + field = id3_frame_field(frame, 1); + nstrings = id3_field_getnstrings(field); + + if (nstrings > 0) { + id3_latin1_t *latin1; + + latin1 = id3_ucs4_latin1duplicate(id3_field_getstrings(field, 0)); + if (latin1) { + signed long ms; + + /* + * "The 'Length' frame contains the length of the audio file + * in milliseconds, represented as a numeric string." + */ + + ms = atol(latin1); + if (ms > 0) + mad_timer_set(&player->stats.total_time, 0, ms, 1000); + + free(latin1); + } + } + } + + /* relative volume adjustment information */ + + if ((player->options & PLAYER_OPTION_SHOWTAGSONLY) || + !(player->options & PLAYER_OPTION_IGNOREVOLADJ)) { + frame = id3_tag_findframe(tag, "RVA2", 0); + if (frame) { + id3_latin1_t const *id; + id3_byte_t const *data; + id3_length_t length; + + enum { + CHANNEL_OTHER = 0x00, + CHANNEL_MASTER_VOLUME = 0x01, + CHANNEL_FRONT_RIGHT = 0x02, + CHANNEL_FRONT_LEFT = 0x03, + CHANNEL_BACK_RIGHT = 0x04, + CHANNEL_BACK_LEFT = 0x05, + CHANNEL_FRONT_CENTRE = 0x06, + CHANNEL_BACK_CENTRE = 0x07, + CHANNEL_SUBWOOFER = 0x08 + }; + + id = id3_field_getlatin1(id3_frame_field(frame, 0)); + data = id3_field_getbinarydata(id3_frame_field(frame, 1), &length); + + assert(id && data); + + /* + * "The 'identification' string is used to identify the situation + * and/or device where this adjustment should apply. The following is + * then repeated for every channel + * + * Type of channel $xx + * Volume adjustment $xx xx + * Bits representing peak $xx + * Peak volume $xx (xx ...)" + */ + + while (length >= 4) { + unsigned int peak_bytes; + + peak_bytes = (data[3] + 7) / 8; + if (4 + peak_bytes > length) + break; + + if (data[0] == CHANNEL_MASTER_VOLUME) { + signed int voladj_fixed; + double voladj_float; + + /* + * "The volume adjustment is encoded as a fixed point decibel + * value, 16 bit signed integer representing (adjustment*512), + * giving +/- 64 dB with a precision of 0.001953125 dB." + */ + + voladj_fixed = (data[1] << 8) | (data[2] << 0); + voladj_fixed |= -(voladj_fixed & 0x8000); + + voladj_float = (double) voladj_fixed / 512; + + set_gain(player, GAIN_VOLADJ, voladj_float); + + if (player->verbosity >= 0) { + detail(_("Relative Volume"), + _("%+.1f dB adjustment (%s)"), voladj_float, id); + } + + break; + } + + data += 4 + peak_bytes; + length -= 4 + peak_bytes; + } + } + } + + /* Replay Gain */ + + if ((player->options & PLAYER_OPTION_SHOWTAGSONLY) || + ((player->output.replay_gain & PLAYER_RGAIN_ENABLED) && + !(player->output.replay_gain & PLAYER_RGAIN_SET))) { + frame = id3_tag_findframe(tag, "RGAD", 0); + if (frame) { + id3_byte_t const *data; + id3_length_t length; + + data = id3_field_getbinarydata(id3_frame_field(frame, 0), &length); + assert(data); + + /* + * Peak Amplitude $xx $xx $xx $xx + * Radio Replay Gain Adjustment $xx $xx + * Audiophile Replay Gain Adjustment $xx $xx + */ + + if (length >= 8) { + struct mad_bitptr ptr; + mad_fixed_t peak; + struct rgain rgain[2]; + + mad_bit_init(&ptr, data); + + peak = mad_bit_read(&ptr, 32) << 5; + + rgain_parse(&rgain[0], &ptr); + rgain_parse(&rgain[1], &ptr); + + use_rgain(player, rgain); + + mad_bit_finish(&ptr); + } + } + } +} + +/* + * NAME: show_status() + * DESCRIPTION: generate and output stream statistics + */ +static +void show_status(struct stats *stats, + struct mad_header const *header, char const *label, int now) +{ + signed long seconds; + static char const *const layer_str[3] = { N_("I"), N_("II"), N_("III") }; + static char const *const mode_str[4] = { + N_("single channel"), N_("dual channel"), N_("joint stereo"), N_("stereo") + }; + + if (header) { + unsigned int bitrate; + + bitrate = header->bitrate / 1000; + + stats->vbr_rate += bitrate; + stats->vbr_frames++; + + stats->vbr += (stats->bitrate && stats->bitrate != bitrate) ? 128 : -1; + if (stats->vbr < 0) + stats->vbr = 0; + else if (stats->vbr > 512) + stats->vbr = 512; + + stats->bitrate = bitrate; + } + + seconds = mad_timer_count(stats->global_timer, MAD_UNITS_SECONDS); + if (seconds != stats->nsecs || !on_same_line || now) { + mad_timer_t timer; + char time_str[18]; + char const *joint_str = ""; + + stats->nsecs = seconds; + + switch (stats->show) { + case STATS_SHOW_OVERALL: + timer = stats->global_timer; + break; + + case STATS_SHOW_CURRENT: + timer = stats->absolute_timer; + break; + + case STATS_SHOW_REMAINING: + timer = stats->total_time; + + if (mad_timer_sign(timer) == 0 && stats->total_bytes) { + unsigned long rate; + + /* estimate based on size and bitrate */ + + rate = stats->vbr ? + stats->vbr_rate * 125 / stats->vbr_frames : stats->bitrate * 125UL; + + mad_timer_set(&timer, 0, stats->total_bytes, rate); + } + + mad_timer_negate(&timer); + mad_timer_add(&timer, stats->absolute_timer); + break; + } + + mad_timer_string(timer, time_str, " %02lu:%02u:%02u", + MAD_UNITS_HOURS, 0, 0); + if (mad_timer_sign(timer) < 0) + time_str[0] = '-'; + + if (label || stats->label) { + message("%s %s", time_str, label ? label : stats->label); + stats->label = now ? label : 0; + } + else if (header) { + if (header->mode == MAD_MODE_JOINT_STEREO) { + switch (header->flags & (MAD_FLAG_MS_STEREO | MAD_FLAG_I_STEREO)) { + case 0: + joint_str = _(" (LR)"); + break; + + case MAD_FLAG_MS_STEREO: + joint_str = _(" (MS)"); + break; + + case MAD_FLAG_I_STEREO: + joint_str = _(" (I)"); + break; + + case (MAD_FLAG_MS_STEREO | MAD_FLAG_I_STEREO): + joint_str = _(" (MS+I)"); + break; + } + } + + message(_("%s Layer %s, %s%u kbps%s, %u Hz, %s%s, %s"), + time_str, gettext(layer_str[header->layer - 1]), + stats->vbr ? _("VBR (avg ") : "", + stats->vbr ? ((stats->vbr_rate * 2) / + stats->vbr_frames + 1) / 2 : stats->bitrate, + stats->vbr ? _(")") : "", + header->samplerate, gettext(mode_str[header->mode]), joint_str, + (header->flags & MAD_FLAG_PROTECTION) ? _("CRC") : _("no CRC")); + } + else + message("%s", time_str); + } +} + +/* + * NAME: decode->output() + * DESCRIPTION: configure audio module and output decoded samples + */ +static +enum mad_flow decode_output(void *data, struct mad_header const *header, + struct mad_pcm *pcm) +{ + struct player *player = data; + struct output *output = &player->output; + mad_fixed_t const *ch1, *ch2; + unsigned int nchannels; + union audio_control control; + + ch1 = pcm->samples[0]; + ch2 = pcm->samples[1]; + + switch (nchannels = pcm->channels) { + case 1: + ch2 = 0; + if (output->select == PLAYER_CHANNEL_STEREO) { + ch2 = ch1; + nchannels = 2; + } + break; + + case 2: + switch (output->select) { + case PLAYER_CHANNEL_RIGHT: + ch1 = ch2; + /* fall through */ + + case PLAYER_CHANNEL_LEFT: + ch2 = 0; + nchannels = 1; + /* fall through */ + + case PLAYER_CHANNEL_STEREO: + break; + + default: + if (header->mode == MAD_MODE_DUAL_CHANNEL) { + if (output->select == PLAYER_CHANNEL_DEFAULT) { + if (player->verbosity >= -1) { + error("output", + _("no channel selected for dual channel; using first")); + } + + output->select = -PLAYER_CHANNEL_LEFT; + } + + ch2 = 0; + nchannels = 1; + } + } + } + + if (output->channels_in != nchannels || + output->speed_in != pcm->samplerate) { + unsigned int speed_request; + + if (player->verbosity >= 1 && + pcm->samplerate != header->samplerate) { + error("output", _("decoded sample frequency %u Hz"), + pcm->samplerate); + } + + speed_request = output->speed_request ? + output->speed_request : pcm->samplerate; + + audio_control_init(&control, AUDIO_COMMAND_CONFIG); + + control.config.channels = nchannels; + control.config.speed = speed_request; + control.config.precision = output->precision_in; + + if (output->command(&control) == -1) { + error("output", audio_error); + return MAD_FLOW_BREAK; + } + + output->channels_in = nchannels; + output->speed_in = pcm->samplerate; + + output->channels_out = control.config.channels; + output->speed_out = control.config.speed; + output->precision_out = control.config.precision; + + if (player->verbosity >= -1 && + output->channels_in != output->channels_out) { + if (output->channels_in == 1) + error("output", _("mono output not available; forcing stereo")); + else { + error("output", _("stereo output not available; using first channel " + "(use -m to mix)")); + } + } + + if (player->verbosity >= -1 && + output->precision_in && + output->precision_in != output->precision_out) { + error("output", _("bit depth %u not available; using %u"), + output->precision_in, output->precision_out); + } + + if (player->verbosity >= -1 && + speed_request != output->speed_out) { + error("output", _("sample frequency %u Hz not available; using %u Hz"), + speed_request, output->speed_out); + } + + /* check whether resampling is necessary */ + if (abs(output->speed_out - output->speed_in) < + (long) FREQ_TOLERANCE * output->speed_in / 100) { + if (output->resampled) { + resample_finish(&output->resample[0]); + resample_finish(&output->resample[1]); + + free(output->resampled); + output->resampled = 0; + } + } + else { + if (output->resampled) { + resample_finish(&output->resample[0]); + resample_finish(&output->resample[1]); + } + else { + output->resampled = malloc(sizeof(*output->resampled)); + if (output->resampled == 0) { + error("output", + _("not enough memory to allocate resampling buffer")); + + output->speed_in = 0; + return MAD_FLOW_BREAK; + } + } + + if (resample_init(&output->resample[0], + output->speed_in, output->speed_out) == -1 || + resample_init(&output->resample[1], + output->speed_in, output->speed_out) == -1) { + error("output", _("cannot resample %u Hz to %u Hz"), + output->speed_in, output->speed_out); + + free(output->resampled); + output->resampled = 0; + + output->speed_in = 0; + return MAD_FLOW_BREAK; + } + else if (player->verbosity >= -1) { + error("output", _("resampling %u Hz to %u Hz"), + output->speed_in, output->speed_out); + } + } + } + + audio_control_init(&control, AUDIO_COMMAND_PLAY); + + if (output->channels_in != output->channels_out) + ch2 = (output->channels_out == 2) ? ch1 : 0; + + if (output->resampled) { + control.play.nsamples = resample_block(&output->resample[0], + pcm->length, ch1, + (*output->resampled)[0]); + control.play.samples[0] = (*output->resampled)[0]; + + if (ch2 == ch1) + control.play.samples[1] = control.play.samples[0]; + else if (ch2) { + resample_block(&output->resample[1], pcm->length, ch2, + (*output->resampled)[1]); + control.play.samples[1] = (*output->resampled)[1]; + } + else + control.play.samples[1] = 0; + } + else { + control.play.nsamples = pcm->length; + control.play.samples[0] = ch1; + control.play.samples[1] = ch2; + } + + control.play.mode = output->mode; + control.play.stats = &player->stats.audio; + + if (output->command(&control) == -1) { + error("output", audio_error); + return MAD_FLOW_BREAK; + } + + ++player->stats.play_framecount; + mad_timer_add(&player->stats.play_timer, header->duration); + + if (player->verbosity > 0) + show_status(&player->stats, header, 0, 0); + + return MAD_FLOW_CONTINUE; +} + +/* + * NAME: get_id3() + * DESCRIPTION: read and parse an ID3 tag from a stream + */ +static +struct id3_tag *get_id3(struct mad_stream *stream, id3_length_t tagsize, + struct input *input) +{ + struct id3_tag *tag = 0; + id3_length_t count; + id3_byte_t const *data; + id3_byte_t *allocated = 0; + + count = stream->bufend - stream->this_frame; + + if (tagsize <= count) { + data = stream->this_frame; + mad_stream_skip(stream, tagsize); + } + else { + allocated = malloc(tagsize); + if (allocated == 0) { + error("id3", _("not enough memory to allocate tag data buffer")); + goto fail; + } + + memcpy(allocated, stream->this_frame, count); + mad_stream_skip(stream, count); + + while (count < tagsize) { + int len; + + do + len = read(input->fd, allocated + count, tagsize - count); + while (len == -1 && errno == EINTR); + + if (len == -1) { + error("id3", ":read"); + goto fail; + } + + if (len == 0) { + error("id3", _("EOF while reading tag data")); + goto fail; + } + + count += len; + } + + data = allocated; + } + + tag = id3_tag_parse(data, tagsize); + + fail: + if (allocated) + free(allocated); + + return tag; +} + +/* + * NAME: decode->error() + * DESCRIPTION: handle a decoding error + */ +static +enum mad_flow decode_error(void *data, struct mad_stream *stream, + struct mad_frame *frame) +{ + struct player *player = data; + signed long tagsize; + + switch (stream->error) { + case MAD_ERROR_BADDATAPTR: + return MAD_FLOW_CONTINUE; + + case MAD_ERROR_LOSTSYNC: + tagsize = id3_tag_query(stream->this_frame, + stream->bufend - stream->this_frame); + if (tagsize > 0) { + if (player->options & PLAYER_OPTION_STREAMID3) { + struct id3_tag *tag; + + tag = get_id3(stream, tagsize, &player->input); + if (tag) { + process_id3(tag, player); + id3_tag_delete(tag); + } + } + else + mad_stream_skip(stream, tagsize); + + if (player->stats.total_bytes >= tagsize) + player->stats.total_bytes -= tagsize; + + return MAD_FLOW_CONTINUE; + } + + /* fall through */ + + default: + if (player->verbosity >= -1 && + !(player->options & PLAYER_OPTION_SHOWTAGSONLY) && + ((stream->error == MAD_ERROR_LOSTSYNC && !player->input.eof) + || stream->sync) && + player->stats.global_framecount != player->stats.error_frame) { + error("error", _("frame %lu: %s"), + player->stats.absolute_framecount, mad_stream_errorstr(stream)); + player->stats.error_frame = player->stats.global_framecount; + } + } + + if (stream->error == MAD_ERROR_BADCRC) { + if (player->stats.global_framecount == player->stats.mute_frame) + mad_frame_mute(frame); + + player->stats.mute_frame = player->stats.global_framecount + 1; + + return MAD_FLOW_IGNORE; + } + + return MAD_FLOW_CONTINUE; +} + +/* + * NAME: decode() + * DESCRIPTION: decode and output audio for an open file + */ +static +int decode(struct player *player) +{ + struct stat stat; + struct mad_decoder decoder; + int options, result; + + if (fstat(player->input.fd, &stat) == -1) { + error("decode", ":fstat"); + return -1; + } + + if (S_ISREG(stat.st_mode)) + player->stats.total_bytes = stat.st_size; + + tag_init(&player->input.tag); + + /* prepare input buffers */ + +# if defined(HAVE_MMAP) + if (S_ISREG(stat.st_mode) && stat.st_size > 0) { + player->input.length = stat.st_size; + + player->input.fdm = map_file(player->input.fd, player->input.length); + if (player->input.fdm == 0 && player->verbosity >= 0) + error("decode", ":mmap"); + + player->input.data = player->input.fdm; + } +# endif + + if (player->input.data == 0) { + player->input.data = malloc(MPEG_BUFSZ); + if (player->input.data == 0) { + error("decode", _("not enough memory to allocate input buffer")); + return -1; + } + + player->input.length = 0; + } + + player->input.eof = 0; + + /* reset statistics */ + player->stats.absolute_timer = mad_timer_zero; + player->stats.play_timer = mad_timer_zero; + player->stats.absolute_framecount = 0; + player->stats.play_framecount = 0; + player->stats.error_frame = -1; + player->stats.vbr = 0; + player->stats.bitrate = 0; + player->stats.vbr_frames = 0; + player->stats.vbr_rate = 0; + player->stats.audio.clipped_samples = 0; + player->stats.audio.peak_clipping = 0; + player->stats.audio.peak_sample = 0; + + mad_decoder_init(&decoder, player, +# if defined(HAVE_MMAP) + player->input.fdm ? decode_input_mmap : +# endif + decode_input_read, + decode_header, decode_filter, + player->output.command ? decode_output : 0, + decode_error, 0); + + options = 0; + if (player->options & PLAYER_OPTION_DOWNSAMPLE) + options |= MAD_OPTION_HALFSAMPLERATE; + if (player->options & PLAYER_OPTION_IGNORECRC) + options |= MAD_OPTION_IGNORECRC; + + mad_decoder_options(&decoder, options); + + result = mad_decoder_run(&decoder, MAD_DECODER_MODE_SYNC); + + mad_decoder_finish(&decoder); + +# if defined(HAVE_MMAP) + if (player->input.fdm) { + if (unmap_file(player->input.fdm, player->input.length) == -1) { + error("decode", ":munmap"); + result = -1; + } + + player->input.fdm = 0; + + if (!player->input.eof) + player->input.data = 0; + } +# endif + + if (player->input.data) { + free(player->input.data); + player->input.data = 0; + } + + tag_finish(&player->input.tag); + + return result; +} + +/* + * NAME: play_one() + * DESCRIPTION: open and play a single file + */ +static +int play_one(struct player *player) +{ + char const *file = player->playlist.entries[player->playlist.current]; + int result; + + if (strcmp(file, "-") == 0) { + if (isatty(STDIN_FILENO)) { + error(0, "%s: %s", _("stdin"), _("is a tty")); + return -1; + } + + player->input.path = _("stdin"); + player->input.fd = STDIN_FILENO; + } + else { + player->input.path = file; + player->input.fd = open(file, O_RDONLY | O_BINARY); + if (player->input.fd == -1) { + error(0, ":", file); + return -1; + } + } + + if (player->verbosity >= 0 && + player->playlist.length > 1) + message(">> %s\n", player->input.path); + + /* reset file information */ + + player->stats.total_bytes = 0; + player->stats.total_time = mad_timer_zero; + + if (!(player->options & PLAYER_OPTION_IGNOREVOLADJ)) + set_gain(player, GAIN_VOLADJ, 0); + + player->output.replay_gain &= ~PLAYER_RGAIN_SET; + + /* try reading ID3 tag information now (else read later from stream) */ + { + int fd; + struct id3_file *file; + + player->options &= ~PLAYER_OPTION_STREAMID3; + + fd = dup(player->input.fd); + file = id3_file_fdopen(fd, ID3_FILE_MODE_READONLY); + if (file == 0) { + close(fd); + player->options |= PLAYER_OPTION_STREAMID3; + } + else { + process_id3(id3_file_tag(file), player); + id3_file_close(file); + } + } + + result = decode(player); + + if (result == 0 && player->verbosity >= 0 && + !(player->options & PLAYER_OPTION_SHOWTAGSONLY)) { + char time_str[19], db_str[7]; + char const *peak_str; + mad_fixed_t peak; + + mad_timer_string(player->stats.play_timer, time_str, "%lu:%02u:%02u.%1u", + MAD_UNITS_HOURS, MAD_UNITS_DECISECONDS, 0); + +# if defined(HAVE_LOCALECONV) + { + char *point; + + point = strchr(time_str, '.'); + if (point) + *point = *localeconv()->decimal_point; + } +# endif + + peak = MAD_F_ONE + player->stats.audio.peak_clipping; + if (peak == MAD_F_ONE) + peak = player->stats.audio.peak_sample; + + if (peak == 0) + peak_str = "-inf"; + else { + sprintf(db_str, "%+.1f", 20 * log10(mad_f_todouble(peak))); + peak_str = db_str; + } + + message("%lu %s (%s), %s dB %s, %lu %s\n", + player->stats.play_framecount, player->stats.play_framecount == 1 ? + _("frame decoded") : _("frames decoded"), time_str, + peak_str, _("peak amplitude"), player->stats.audio.clipped_samples, + player->stats.audio.clipped_samples == 1 ? + _("clipped sample") : _("clipped samples")); + } + + if (player->input.fd != STDIN_FILENO && + close(player->input.fd) == -1 && result == 0) { + error(0, ":", player->input.path); + result = -1; + } + + return result; +} + +/* + * NAME: play_all() + * DESCRIPTION: run the player's playlist + */ +static +int play_all(struct player *player) +{ + int count, i, j, result = 0; + struct playlist *playlist = &player->playlist; + char const *tmp; + + /* set up playlist */ + + count = playlist->length; + + if (player->options & PLAYER_OPTION_SHUFFLE) { + srand(time(0)); + + /* initial shuffle */ + for (i = 0; i < count; ++i) { + j = rand() % count; + + tmp = playlist->entries[i]; + playlist->entries[i] = playlist->entries[j]; + playlist->entries[j] = tmp; + } + } + + /* run playlist */ + + while (count && (player->repeat == -1 || player->repeat--)) { + while (playlist->current < playlist->length) { + i = playlist->current; + + if (playlist->entries[i] == 0) { + ++playlist->current; + continue; + } + + player->control = PLAYER_CONTROL_DEFAULT; + + if (play_one(player) == -1) { + playlist->entries[i] = 0; + --count; + + result = -1; + } + + if ((player->options & PLAYER_OPTION_TIMED) && + mad_timer_compare(player->stats.global_timer, + player->global_stop) > 0) { + count = 0; + break; + } + + switch (player->control) { + case PLAYER_CONTROL_DEFAULT: + if ((player->options & PLAYER_OPTION_SHUFFLE) && player->repeat && + ++i < playlist->length) { + /* pick something from the next half only */ + j = (i + rand() % ((playlist->length + 1) / 2)) % playlist->length; + + tmp = playlist->entries[i]; + playlist->entries[i] = playlist->entries[j]; + playlist->entries[j] = tmp; + } + /* fall through */ + + case PLAYER_CONTROL_NEXT: + ++playlist->current; + break; + + case PLAYER_CONTROL_PREVIOUS: + do { + if (playlist->current-- == 0) + playlist->current = playlist->length; + } + while (playlist->current < playlist->length && + playlist->entries[playlist->current] == 0); + break; + + case PLAYER_CONTROL_REPLAY: + break; + + case PLAYER_CONTROL_STOP: + playlist->current = playlist->length; + count = 0; + break; + } + } + + playlist->current = 0; + } + + return result; +} + +/* + * NAME: stop_audio() + * DESCRIPTION: stop playing the audio device immediately + */ +static +int stop_audio(struct player *player, int flush) +{ + int result = 0; + + if (player->output.command) { + union audio_control control; + + audio_control_init(&control, AUDIO_COMMAND_STOP); + control.stop.flush = flush; + + result = player->output.command(&control); + } + + return result; +} + +# if defined(USE_TTY) +/* + * NAME: readkey() + * DESCRIPTION: read a keypress from the keyboard + */ +static +int readkey(int blocking) +{ +# if !defined(_WIN32) + unsigned char key; + ssize_t count; + + if (!blocking) { + /* tty_fd should be a tty in noncanonical mode with VMIN = VTIME = 0 */ + + count = read(tty_fd, &key, 1); + if (count == -1 && errno != EINTR) { + error("tty", ":read"); + return -1; + } + + return (count == 1) ? key : 0; + } + else { + struct termios tty, save_tty; + + if (tcgetattr(tty_fd, &tty) == -1) { + error("tty", ":tcgetattr"); + return -1; + } + + save_tty = tty; + + /* change terminal temporarily to get a blocking read() */ + + tty.c_cc[VMIN] = 1; + + if (tcsetattr(tty_fd, TCSANOW, &tty) == -1) { + error("tty", ":tcsetattr"); + return -1; + } + + do + count = read(tty_fd, &key, 1); + while (count == -1 && errno == EINTR); + + if (count == -1) + error("tty", ":read"); + + if (tcsetattr(tty_fd, TCSANOW, &save_tty) == -1) { + error("tty", ":tcsetattr"); + return -1; + } + + if (count == -1) + return -1; + + return (count == 1) ? key : 0; + } +# elif defined(_WIN32) + HANDLE console; + INPUT_RECORD input; + DWORD count; + + console = GetStdHandle(STD_INPUT_HANDLE); + + do { + if (GetNumberOfConsoleInputEvents(console, &count) == 0) { + error("tty", "GetNumberOfConsoleInputEvents() failed"); + return -1; + } + + if (count == 0) { + if (!blocking) + return 0; + else { + /* this is necessary to keep Windows from hanging (!) */ + Sleep(500); + + switch (WaitForSingleObject(console, INFINITE)) { + case WAIT_ABANDONED: + case WAIT_OBJECT_0: + continue; + + case WAIT_TIMEOUT: + default: + /* ? */ + case WAIT_FAILED: + error("tty", "WaitForSingleObject() failed"); + return -1; + } + } + } + + if (ReadConsoleInput(console, &input, 1, &count) == 0 || count != 1) { + error("tty", "ReadConsoleInput() failed"); + return -1; + } + } + while (input.EventType != KEY_EVENT || !input.Event.KeyEvent.bKeyDown || + input.Event.KeyEvent.uChar.AsciiChar == 0); + + return (unsigned char) input.Event.KeyEvent.uChar.AsciiChar; +# endif + + return blocking ? -1 : 0; +} + +/* + * NAME: tty_filter() + * DESCRIPTION: process TTY commands + */ +static +enum mad_flow tty_filter(void *data, struct mad_frame *frame) +{ + struct player *player = data; + enum mad_flow flow = MAD_FLOW_CONTINUE; + int command, stopped = 0; + + command = readkey(0); + if (command == -1) + return MAD_FLOW_BREAK; + + again: + switch (command) { + case KEY_STOP: + stopped = 1; + + player->control = PLAYER_CONTROL_REPLAY; + flow = MAD_FLOW_STOP; + + /* fall through */ + + case KEY_PAUSE: + stop_audio(player, stopped); + message(" --%s--", stopped ? _("Stopped") : _("Paused")); + + command = readkey(1); + + message(""); + + if (command == -1) + return MAD_FLOW_BREAK; + + if (command != KEY_PAUSE) + goto again; + + break; + + case KEY_FORWARD: + case KEY_CTRL('n'): + case '>': + player->control = PLAYER_CONTROL_NEXT; + goto stop; + + case KEY_BACK: + case KEY_CTRL('p'): + case '<': + { + mad_timer_t threshold; + + mad_timer_set(&threshold, 4, 0, 0); + + player->control = + (stopped || + mad_timer_compare(player->stats.play_timer, threshold) < 0) ? + PLAYER_CONTROL_PREVIOUS : PLAYER_CONTROL_REPLAY; + } + goto stop; + + case KEY_QUIT: + case KEY_CTRL('c'): + case 'Q': + player->control = PLAYER_CONTROL_STOP; + goto stop; + + case KEY_INFO: + case '?': + if (player->verbosity <= 0) { + show_status(&player->stats, 0, player->input.path, 1); + message("\n"); + } + break; + + case KEY_TIME: + if (player->verbosity > 0) { + char const *label = 0; + + switch (player->stats.show) { + case STATS_SHOW_OVERALL: + player->stats.show = STATS_SHOW_REMAINING; + label = N_("[Current Time Remaining]"); + break; + + case STATS_SHOW_REMAINING: + player->stats.show = STATS_SHOW_CURRENT; + label = N_("[Current Time]"); + break; + + case STATS_SHOW_CURRENT: + player->stats.show = STATS_SHOW_OVERALL; + label = N_("[Overall Time]"); + break; + } + + show_status(&player->stats, 0, gettext(label), 1); + } + break; + + case KEY_GAINDECR: + case KEY_GAININCR: + case KEY_GAINZERO: + case KEY_GAININFO: + { + double db; + + switch (command) { + case KEY_GAINDECR: + db = set_gain(player, GAIN_ATTAMP | GAIN_RELATIVE, -0.5); + break; + + case KEY_GAININCR: + db = set_gain(player, GAIN_ATTAMP | GAIN_RELATIVE, +0.5); + break; + + case KEY_GAINZERO: + db = set_gain(player, GAIN_ATTAMP, 0); + break; + + default: + db = set_gain(player, 0, 0); + break; + } + + if (player->verbosity > 0) { + static char status[15]; + + sprintf(status, "%+.1f dB gain", db); + show_status(&player->stats, 0, status, 1); + } + } + break; + } + + return flow; + + stop: + stop_audio(player, 1); + return MAD_FLOW_STOP; +} +# endif + +/* + * NAME: addfilter() + * DESCRIPTION: insert a filter at the beginning of the filter chain + */ +static +int addfilter(struct player *player, filter_func_t *func, void *data) +{ + struct filter *filter; + + filter = filter_new(func, data, player->output.filters); + if (filter == 0) + return -1; + + player->output.filters = filter; + + return 0; +} + +/* + * NAME: setup_filters() + * DESCRIPTION: create output filters + */ +static +int setup_filters(struct player *player) +{ + /* filters must be added in reverse order */ + +# if defined(EXPERIMENTAL) + if ((player->options & PLAYER_OPTION_EXTERNALMIX) && + addfilter(player, mixer_filter, stdout) == -1) + return -1; + + if ((player->options & PLAYER_OPTION_EXPERIMENTAL) && + addfilter(player, experimental_filter, 0) == -1) + return -1; +# endif + + if ((player->options & PLAYER_OPTION_FADEIN) && + addfilter(player, fadein_filter, player) == -1) + return -1; + + addfilter(player, gain_filter, &player->output.gain); + + if (player->output.select == PLAYER_CHANNEL_MONO && + addfilter(player, mono_filter, player) == -1) + return -1; + +# if defined(USE_TTY) + if ((player->options & PLAYER_OPTION_TTYCONTROL) && + addfilter(player, tty_filter, player) == -1) + return -1; +# endif + + return 0; +} + +# if defined(USE_TTY) && !defined(_WIN32) +/* + * NAME: restore_tty() + * DESCRIPTION: revert to previous terminal settings + */ +static +int restore_tty(int interrupt) +{ + struct termios tty; + struct sigaction action; + int result = 0; + + if (tcgetattr(tty_fd, &tty) == 0 && + tcsetattr(tty_fd, interrupt ? TCSAFLUSH : TCSADRAIN, + &save_tty) == -1) { + if (!interrupt) + error("tty", ":tcsetattr"); + result = -1; + } + + save_tty = tty; + + if (sigaction(SIGINT, 0, &action) == 0 && + sigaction(SIGINT, &save_sigint, 0) == -1) { + if (!interrupt) + error("tty", ":sigaction(SIGINT)"); + result = -1; + } + + save_sigint = action; + + if (sigaction(SIGTSTP, 0, &action) == 0 && + sigaction(SIGTSTP, &save_sigtstp, 0) == -1) { + if (!interrupt) + error("tty", ":sigaction(SIGTSTP)"); + result = -1; + } + + save_sigtstp = action; + + if (!interrupt) { + if (close(tty_fd) == -1) { + error("tty", ":close"); + result = -1; + } + + tty_fd = -1; + } + + return result; +} + +/* + * NAME: signal_handler() + * DESCRIPTION: restore tty state after software interrupt + */ +static +void signal_handler(int signal) +{ + static struct sigaction save_sigcont; + + /* restore tty state and previous signal actions */ + + restore_tty(1); + + /* handle SIGCONT after SIGTSTP */ + + switch (signal) { + case SIGTSTP: + { + struct sigaction action; + + sigaction(SIGCONT, 0, &save_sigcont); + + action = save_sigcont; + action.sa_handler = signal_handler; + sigemptyset(&action.sa_mask); + sigaddset(&action.sa_mask, SIGTSTP); + sigaddset(&action.sa_mask, SIGINT); + action.sa_flags = 0; + + sigaction(SIGCONT, &action, 0); + } + break; + + case SIGCONT: + sigaction(SIGCONT, &save_sigcont, 0); + on_same_line = 0; /* redraw status line */ + break; + } + + /* re-send signal, which is currently blocked */ + + kill(getpid(), signal); + + /* return to previous thread, which should immediately receive the signal */ + + return; +} + +/* + * NAME: setup_tty() + * DESCRIPTION: change terminal parameters and signal handlers + */ +static +int setup_tty(void) +{ + struct termios tty; + struct sigaction action; + + /* open controlling terminal */ + + tty_fd = open(TTY_DEVICE, O_RDONLY); + if (tty_fd == -1) { + error("tty", ":", TTY_DEVICE); + return -1; + } + + /* save current terminal and signal settings */ + + if (tcgetattr(tty_fd, &save_tty) == -1) { + error("tty", ":tcgetattr"); + return -1; + } + + if (sigaction(SIGTSTP, 0, &save_sigtstp) == -1) { + error("tty", ":sigaction(SIGTSTP)"); + return -1; + } + + if (sigaction(SIGINT, 0, &save_sigint) == -1) { + error("tty", ":sigaction(SIGINT)"); + return -1; + } + + /* catch SIGTSTP and SIGINT so the tty state can be restored */ + + action = save_sigtstp; + action.sa_handler = signal_handler; + sigemptyset(&action.sa_mask); + sigaddset(&action.sa_mask, SIGINT); +# if 0 /* on some systems (Mac OS X) this remains masked upon continue (?!) */ + sigaddset(&action.sa_mask, SIGCONT); +# endif + action.sa_flags = 0; + + if (sigaction(SIGTSTP, &action, 0) == -1) { + error("tty", ":sigaction(SIGTSTP)"); + goto fail; + } + + action = save_sigint; + action.sa_handler = signal_handler; + sigemptyset(&action.sa_mask); + sigaddset(&action.sa_mask, SIGTSTP); + sigaddset(&action.sa_mask, SIGCONT); + action.sa_flags = 0; + + if (sigaction(SIGINT, &action, 0) == -1) { + error("tty", ":sigaction(SIGINT)"); + goto fail; + } + + /* turn off echo and canonical mode */ + + tty = save_tty; + + tty.c_lflag &= ~(ECHO | ICANON); + + /* set VMIN = VTIME = 0 so read() always returns immediately */ + + tty.c_cc[VMIN] = 0; + tty.c_cc[VTIME] = 0; + + if (tcsetattr(tty_fd, TCSAFLUSH, &tty) == -1) { + error("tty", ":tcsetattr"); + goto fail; + } + + return 0; + + fail: + sigaction(SIGINT, &save_sigint, 0); + sigaction(SIGTSTP, &save_sigtstp, 0); + return -1; +} +# endif + +/* + * NAME: silence() + * DESCRIPTION: output silence for a period of time + */ +static +int silence(struct player *player, mad_timer_t duration, char const *label) +{ + union audio_control control; + unsigned int nchannels, speed, nsamples; + mad_fixed_t *samples; + mad_timer_t unit; + int result = 0; + + audio_control_init(&control, AUDIO_COMMAND_CONFIG); + control.config.channels = 2; + control.config.speed = 44100; + + if (player->output.command(&control) == -1) { + error("audio", audio_error); + return -1; + } + + nchannels = control.config.channels; + speed = control.config.speed; + nsamples = speed > MAX_NSAMPLES ? MAX_NSAMPLES : speed; + + player->output.channels_in = nchannels; + player->output.channels_out = nchannels; + player->output.speed_in = speed; + player->output.speed_out = speed; + + samples = calloc(nsamples, sizeof(mad_fixed_t)); + if (samples == 0) { + error("silence", _("not enough memory to allocate sample buffer")); + return -1; + } + + audio_control_init(&control, AUDIO_COMMAND_PLAY); + control.play.nsamples = nsamples; + control.play.samples[0] = samples; + control.play.samples[1] = (nchannels == 2) ? samples : 0; + control.play.mode = player->output.mode; + control.play.stats = &player->stats.audio; + + mad_timer_set(&unit, 0, nsamples, speed); + + for (mad_timer_negate(&duration); + mad_timer_sign(duration) < 0; + mad_timer_add(&duration, unit)) { + if (mad_timer_compare(unit, mad_timer_abs(duration)) > 0) { + unit = mad_timer_abs(duration); + control.play.nsamples = mad_timer_fraction(unit, speed); + } + +# if defined(USE_TTY) + if ((player->options & PLAYER_OPTION_TTYCONTROL) && + tty_filter(player, 0) != MAD_FLOW_CONTINUE) + goto fail; +# endif + + if (player->output.command(&control) == -1) { + error("audio", audio_error); + goto fail; + } + + mad_timer_add(&player->stats.global_timer, unit); + + if (player->verbosity > 0) + show_status(&player->stats, 0, label, 0); + } + + if (0) { + fail: + result = -1; + } + + free(samples); + + return result; +} + +/* + * NAME: player->run() + * DESCRIPTION: begin playback + */ +int player_run(struct player *player, int argc, char const *argv[]) +{ + int result = 0; + union audio_control control; + + player->playlist.entries = argv; + player->playlist.length = argc; + + /* set up terminal settings */ + +# if defined(USE_TTY) && !defined(_WIN32) + if ((player->options & PLAYER_OPTION_TTYCONTROL) && + setup_tty() == -1) + player->options &= ~PLAYER_OPTION_TTYCONTROL; +# endif + + /* initialize ancillary data output file */ + + if (player->ancillary.path) { + if (player->output.path && + strcmp(player->ancillary.path, player->output.path) == 0) { + error("output", _("ancillary and audio output have same path")); + goto fail; + } + + if (strcmp(player->ancillary.path, "-") == 0) + player->ancillary.file = stdout; + else { + player->ancillary.file = fopen(player->ancillary.path, "wb"); + if (player->ancillary.file == 0) { + error("ancillary", ":", player->ancillary.path); + goto fail; + } + } + } + + /* set up filters */ + + if (setup_filters(player) == -1) { + error("filter", _("not enough memory to allocate filters")); + goto fail; + } + + set_gain(player, 0, 0); + + /* initialize audio */ + + if (player->output.command) { + audio_control_init(&control, AUDIO_COMMAND_INIT); + control.init.path = player->output.path; + + if (player->output.command(&control) == -1) { + error("audio", audio_error, control.init.path); + goto fail; + } + + if ((player->options & PLAYER_OPTION_SKIP) && + mad_timer_sign(player->global_start) < 0) { + player->stats.global_timer = player->global_start; + + if (silence(player, mad_timer_abs(player->global_start), + _("lead-in")) == -1) + result = -1; + } + } + + /* run playlist */ + + if (result == 0) + result = play_all(player); + + /* drain and close audio */ + + if (player->output.command) { + audio_control_init(&control, AUDIO_COMMAND_FINISH); + + if (player->output.command(&control) == -1) { + error("audio", audio_error); + goto fail; + } + } + + if (0) { + fail: + result = -1; + } + + /* drain and close ancillary data output file */ + + if (player->ancillary.file) { + if (player->ancillary.length) { + if (fputc(player->ancillary.buffer << (8 - player->ancillary.length), + player->ancillary.file) == EOF && + result == 0) { + error("ancillary", ":fputc"); + result = -1; + } + + player->ancillary.length = 0; + } + + if (player->ancillary.file != stdout && + fclose(player->ancillary.file) == EOF && + result == 0) { + error("ancillary", ":fclose"); + result = -1; + } + + player->ancillary.file = 0; + } + + /* restore terminal settings */ + +# if defined(USE_TTY) && !defined(_WIN32) + if (player->options & PLAYER_OPTION_TTYCONTROL) + restore_tty(0); +# endif + + return result; +} |