| /*************************************************************************** |
| * __________ __ ___. |
| * Open \______ \ ____ ____ | | _\_ |__ _______ ___ |
| * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / |
| * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < |
| * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ |
| * \/ \/ \/ \/ \/ |
| * $Id$ |
| * |
| * Copyright (C) 2005-2007 Miika Pekkarinen |
| * Copyright (C) 2007-2008 Nicolas Pennequin |
| * |
| * 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 "playback.h" |
| #include "codec_thread.h" |
| #include "system.h" |
| #include "kernel.h" |
| #include "codecs.h" |
| #include "buffering.h" |
| #include "pcmbuf.h" |
| #include "dsp.h" |
| #include "abrepeat.h" |
| #include "metadata.h" |
| #include "splash.h" |
| |
| /* Define LOGF_ENABLE to enable logf output in this file */ |
| /*#define LOGF_ENABLE*/ |
| #include "logf.h" |
| |
| /* macros to enable logf for queues |
| logging on SYS_TIMEOUT can be disabled */ |
| #ifdef SIMULATOR |
| /* Define this for logf output of all queuing except SYS_TIMEOUT */ |
| #define PLAYBACK_LOGQUEUES |
| /* Define this to logf SYS_TIMEOUT messages */ |
| /*#define PLAYBACK_LOGQUEUES_SYS_TIMEOUT*/ |
| #endif |
| |
| #ifdef PLAYBACK_LOGQUEUES |
| #define LOGFQUEUE logf |
| #else |
| #define LOGFQUEUE(...) |
| #endif |
| |
| #ifdef PLAYBACK_LOGQUEUES_SYS_TIMEOUT |
| #define LOGFQUEUE_SYS_TIMEOUT logf |
| #else |
| #define LOGFQUEUE_SYS_TIMEOUT(...) |
| #endif |
| |
| |
| /* Variables are commented with the threads that use them: |
| * A=audio, C=codec, V=voice. A suffix of - indicates that |
| * the variable is read but not updated on that thread. |
| |
| * Unless otherwise noted, the extern variables are located |
| * in playback.c. |
| */ |
| |
| /* Main state control */ |
| volatile bool audio_codec_loaded SHAREDBSS_ATTR = false; /* Codec loaded? (C/A-) */ |
| |
| extern struct mp3entry *thistrack_id3, /* the currently playing track */ |
| *othertrack_id3; /* prev track during track-change-transition, or end of playlist, |
| * next track otherwise */ |
| |
| /* Track change controls */ |
| extern bool automatic_skip; /* Who initiated in-progress skip? (C/A-) */ |
| |
| /* Set to true if the codec thread should send an audio stop request |
| * (typically because the end of the playlist has been reached). |
| */ |
| static bool codec_requested_stop = false; |
| |
| extern struct event_queue audio_queue; |
| extern struct event_queue codec_queue; |
| |
| extern struct codec_api ci; /* from codecs.c */ |
| |
| /* Codec thread */ |
| unsigned int codec_thread_id; /* For modifying thread priority later. |
| Used by playback.c and pcmbuf.c */ |
| static struct queue_sender_list codec_queue_sender_list; |
| static long codec_stack[(DEFAULT_STACK_SIZE + 0x2000)/sizeof(long)] |
| IBSS_ATTR; |
| static const char codec_thread_name[] = "codec"; |
| |
| /* function prototypes */ |
| static bool codec_load_next_track(void); |
| |
| |
| /**************************************/ |
| |
| /** misc external functions */ |
| |
| int get_codec_base_type(int type) |
| { |
| switch (type) { |
| case AFMT_MPA_L1: |
| case AFMT_MPA_L2: |
| case AFMT_MPA_L3: |
| return AFMT_MPA_L3; |
| } |
| |
| return type; |
| } |
| |
| const char *get_codec_filename(int cod_spec) |
| { |
| const char *fname; |
| |
| #ifdef HAVE_RECORDING |
| /* Can choose decoder or encoder if one available */ |
| int type = cod_spec & CODEC_TYPE_MASK; |
| int afmt = cod_spec & CODEC_AFMT_MASK; |
| |
| if ((unsigned)afmt >= AFMT_NUM_CODECS) |
| type = AFMT_UNKNOWN | (type & CODEC_TYPE_MASK); |
| |
| fname = (type == CODEC_TYPE_ENCODER) ? |
| audio_formats[afmt].codec_enc_root_fn : |
| audio_formats[afmt].codec_root_fn; |
| |
| logf("%s: %d - %s", |
| (type == CODEC_TYPE_ENCODER) ? "Encoder" : "Decoder", |
| afmt, fname ? fname : "<unknown>"); |
| #else /* !HAVE_RECORDING */ |
| /* Always decoder */ |
| if ((unsigned)cod_spec >= AFMT_NUM_CODECS) |
| cod_spec = AFMT_UNKNOWN; |
| fname = audio_formats[cod_spec].codec_root_fn; |
| logf("Codec: %d - %s", cod_spec, fname ? fname : "<unknown>"); |
| #endif /* HAVE_RECORDING */ |
| |
| return fname; |
| } /* get_codec_filename */ |
| |
| /* Borrow the codec thread and return the ID */ |
| void codec_thread_do_callback(void (*fn)(void), unsigned int *id) |
| { |
| /* Set id before telling thread to call something; it may be |
| * needed before this function returns. */ |
| if (id != NULL) |
| *id = codec_thread_id; |
| |
| /* Codec thread will signal just before entering callback */ |
| LOGFQUEUE("codec >| Q_CODEC_DO_CALLBACK"); |
| queue_send(&codec_queue, Q_CODEC_DO_CALLBACK, (intptr_t)fn); |
| } |
| |
| |
| /** codec API callbacks */ |
| |
| static void* codec_get_buffer(size_t *size) |
| { |
| if (codec_size >= CODEC_SIZE) |
| return NULL; |
| *size = CODEC_SIZE - codec_size; |
| return &codecbuf[codec_size]; |
| } |
| |
| static bool codec_pcmbuf_insert_callback( |
| const void *ch1, const void *ch2, int count) |
| { |
| const char *src[2] = { ch1, ch2 }; |
| |
| while (count > 0) |
| { |
| int out_count = dsp_output_count(ci.dsp, count); |
| int inp_count; |
| char *dest; |
| |
| /* Prevent audio from a previous track from playing */ |
| if (ci.new_track || ci.stop_codec) |
| return true; |
| |
| while ((dest = pcmbuf_request_buffer(&out_count)) == NULL) |
| { |
| cancel_cpu_boost(); |
| sleep(1); |
| if (ci.seek_time || ci.new_track || ci.stop_codec) |
| return true; |
| } |
| |
| /* Get the real input_size for output_size bytes, guarding |
| * against resampling buffer overflows. */ |
| inp_count = dsp_input_count(ci.dsp, out_count); |
| |
| if (inp_count <= 0) |
| return true; |
| |
| /* Input size has grown, no error, just don't write more than length */ |
| if (inp_count > count) |
| inp_count = count; |
| |
| out_count = dsp_process(ci.dsp, dest, src, inp_count); |
| |
| if (out_count <= 0) |
| return true; |
| |
| pcmbuf_write_complete(out_count); |
| |
| count -= inp_count; |
| } |
| |
| return true; |
| } /* codec_pcmbuf_insert_callback */ |
| |
| static void codec_set_elapsed_callback(unsigned int value) |
| { |
| if (ci.seek_time) |
| return; |
| |
| #ifdef AB_REPEAT_ENABLE |
| ab_position_report(value); |
| #endif |
| |
| unsigned int latency = pcmbuf_get_latency(); |
| if (value < latency) |
| thistrack_id3->elapsed = 0; |
| else |
| { |
| unsigned int elapsed = value - latency; |
| if (elapsed > thistrack_id3->elapsed || |
| elapsed < thistrack_id3->elapsed - 2) |
| { |
| thistrack_id3->elapsed = elapsed; |
| } |
| } |
| } |
| |
| static void codec_set_offset_callback(size_t value) |
| { |
| if (ci.seek_time) |
| return; |
| |
| unsigned int latency = pcmbuf_get_latency() * thistrack_id3->bitrate / 8; |
| if (value < latency) |
| thistrack_id3->offset = 0; |
| else |
| thistrack_id3->offset = value - latency; |
| } |
| |
| /* helper function, not a callback */ |
| static void codec_advance_buffer_counters(size_t amount) |
| { |
| bufadvance(get_audio_hid(), amount); |
| ci.curpos += amount; |
| } |
| |
| /* copy up-to size bytes into ptr and return the actual size copied */ |
| static size_t codec_filebuf_callback(void *ptr, size_t size) |
| { |
| ssize_t copy_n; |
| |
| if (ci.stop_codec || !audio_is_playing()) |
| return 0; |
| |
| copy_n = bufread(get_audio_hid(), size, ptr); |
| |
| /* Nothing requested OR nothing left */ |
| if (copy_n == 0) |
| return 0; |
| |
| /* Update read and other position pointers */ |
| codec_advance_buffer_counters(copy_n); |
| |
| /* Return the actual amount of data copied to the buffer */ |
| return copy_n; |
| } /* codec_filebuf_callback */ |
| |
| static void* codec_request_buffer_callback(size_t *realsize, size_t reqsize) |
| { |
| size_t copy_n = reqsize; |
| ssize_t ret; |
| void *ptr; |
| |
| if (!audio_is_playing()) |
| { |
| *realsize = 0; |
| return NULL; |
| } |
| |
| ret = bufgetdata(get_audio_hid(), reqsize, &ptr); |
| if (ret >= 0) |
| copy_n = MIN((size_t)ret, reqsize); |
| |
| if (copy_n == 0) |
| { |
| *realsize = 0; |
| return NULL; |
| } |
| |
| *realsize = copy_n; |
| |
| return ptr; |
| } /* codec_request_buffer_callback */ |
| |
| static void codec_advance_buffer_callback(size_t amount) |
| { |
| codec_advance_buffer_counters(amount); |
| codec_set_offset_callback(ci.curpos); |
| } |
| |
| static void codec_advance_buffer_loc_callback(void *ptr) |
| { |
| size_t amount = buf_get_offset(get_audio_hid(), ptr); |
| codec_advance_buffer_callback(amount); |
| } |
| |
| static bool codec_seek_buffer_callback(size_t newpos) |
| { |
| logf("codec_seek_buffer_callback"); |
| |
| int ret = bufseek(get_audio_hid(), newpos); |
| if (ret == 0) { |
| ci.curpos = newpos; |
| return true; |
| } |
| else { |
| return false; |
| } |
| } |
| |
| static void codec_seek_complete_callback(void) |
| { |
| logf("seek_complete"); |
| /* If seeking-while-playing, pcm_is_paused() is true. |
| * If seeking-while-paused, audio_is_paused() is true. |
| * A seamless seek skips this section. */ |
| if (pcm_is_paused() || audio_is_paused()) |
| { |
| /* Clear the buffer */ |
| pcmbuf_play_stop(); |
| dsp_configure(ci.dsp, DSP_FLUSH, 0); |
| |
| /* If seeking-while-playing, resume pcm playback */ |
| if (!audio_is_paused()) |
| pcmbuf_pause(false); |
| } |
| ci.seek_time = 0; |
| } |
| |
| static void codec_discard_codec_callback(void) |
| { |
| int *codec_hid = get_codec_hid(); |
| if (*codec_hid >= 0) |
| { |
| bufclose(*codec_hid); |
| *codec_hid = -1; |
| } |
| } |
| |
| static bool codec_request_next_track_callback(void) |
| { |
| int prev_codectype; |
| |
| if (ci.stop_codec || !audio_is_playing()) |
| return false; |
| |
| prev_codectype = get_codec_base_type(thistrack_id3->codectype); |
| if (!codec_load_next_track()) |
| return false; |
| |
| /* Seek to the beginning of the new track because if the struct |
| mp3entry was buffered, "elapsed" might not be zero (if the track has |
| been played already but not unbuffered) */ |
| codec_seek_buffer_callback(thistrack_id3->first_frame_offset); |
| /* Check if the next codec is the same file. */ |
| if (prev_codectype == get_codec_base_type(thistrack_id3->codectype)) |
| { |
| logf("New track loaded"); |
| codec_discard_codec_callback(); |
| return true; |
| } |
| else |
| { |
| logf("New codec:%d/%d", thistrack_id3->codectype, prev_codectype); |
| return false; |
| } |
| } |
| |
| static void codec_configure_callback(int setting, intptr_t value) |
| { |
| if (!dsp_configure(ci.dsp, setting, value)) |
| { logf("Illegal key:%d", setting); } |
| } |
| |
| /* Initialize codec API */ |
| void codec_init_codec_api(void) |
| { |
| ci.dsp = (struct dsp_config *)dsp_configure(NULL, DSP_MYDSP, |
| CODEC_IDX_AUDIO); |
| ci.codec_get_buffer = codec_get_buffer; |
| ci.pcmbuf_insert = codec_pcmbuf_insert_callback; |
| ci.set_elapsed = codec_set_elapsed_callback; |
| ci.read_filebuf = codec_filebuf_callback; |
| ci.request_buffer = codec_request_buffer_callback; |
| ci.advance_buffer = codec_advance_buffer_callback; |
| ci.advance_buffer_loc = codec_advance_buffer_loc_callback; |
| ci.seek_buffer = codec_seek_buffer_callback; |
| ci.seek_complete = codec_seek_complete_callback; |
| ci.request_next_track = codec_request_next_track_callback; |
| ci.discard_codec = codec_discard_codec_callback; |
| ci.set_offset = codec_set_offset_callback; |
| ci.configure = codec_configure_callback; |
| } |
| |
| |
| /** track change functions */ |
| |
| static inline void codec_crossfade_track_change(void) |
| { |
| /* Initiate automatic crossfade mode */ |
| pcmbuf_crossfade_init(false); |
| /* Notify the wps that the track change starts now */ |
| LOGFQUEUE("codec > audio Q_AUDIO_TRACK_CHANGED"); |
| queue_post(&audio_queue, Q_AUDIO_TRACK_CHANGED, 0); |
| } |
| |
| static void codec_track_skip_done(bool was_manual) |
| { |
| /* Manual track change (always crossfade or flush audio). */ |
| if (was_manual) |
| { |
| pcmbuf_crossfade_init(true); |
| LOGFQUEUE("codec > audio Q_AUDIO_TRACK_CHANGED"); |
| queue_post(&audio_queue, Q_AUDIO_TRACK_CHANGED, 0); |
| } |
| /* Automatic track change w/crossfade, if not in "Track Skip Only" mode. */ |
| else if (pcmbuf_is_crossfade_enabled() && !pcmbuf_is_crossfade_active() |
| && global_settings.crossfade != CROSSFADE_ENABLE_TRACKSKIP) |
| { |
| if (global_settings.crossfade == CROSSFADE_ENABLE_SHUFFLE_AND_TRACKSKIP) |
| { |
| if (global_settings.playlist_shuffle) |
| /* shuffle mode is on, so crossfade: */ |
| codec_crossfade_track_change(); |
| else |
| /* shuffle mode is off, so normal gapless playback */ |
| pcmbuf_start_track_change(); |
| } |
| else |
| /* normal crossfade: */ |
| codec_crossfade_track_change(); |
| } |
| else |
| /* normal gapless playback. */ |
| pcmbuf_start_track_change(); |
| } |
| |
| static bool codec_load_next_track(void) |
| { |
| intptr_t result = Q_CODEC_REQUEST_FAILED; |
| |
| audio_set_prev_elapsed(thistrack_id3->elapsed); |
| |
| #ifdef AB_REPEAT_ENABLE |
| ab_end_of_track_report(); |
| #endif |
| |
| logf("Request new track"); |
| |
| if (ci.new_track == 0) |
| { |
| ci.new_track++; |
| automatic_skip = true; |
| } |
| |
| if (!ci.stop_codec) |
| { |
| trigger_cpu_boost(); |
| LOGFQUEUE("codec >| audio Q_AUDIO_CHECK_NEW_TRACK"); |
| result = queue_send(&audio_queue, Q_AUDIO_CHECK_NEW_TRACK, 0); |
| } |
| |
| switch (result) |
| { |
| case Q_CODEC_REQUEST_COMPLETE: |
| LOGFQUEUE("codec |< Q_CODEC_REQUEST_COMPLETE"); |
| codec_track_skip_done(!automatic_skip); |
| return true; |
| |
| case Q_CODEC_REQUEST_FAILED: |
| LOGFQUEUE("codec |< Q_CODEC_REQUEST_FAILED"); |
| ci.new_track = 0; |
| ci.stop_codec = true; |
| codec_requested_stop = true; |
| return false; |
| |
| default: |
| LOGFQUEUE("codec |< default"); |
| ci.stop_codec = true; |
| codec_requested_stop = true; |
| return false; |
| } |
| } |
| |
| /** CODEC THREAD */ |
| static void codec_thread(void) |
| { |
| struct queue_event ev; |
| int status; |
| |
| while (1) { |
| status = 0; |
| |
| if (!pcmbuf_is_crossfade_active()) { |
| cancel_cpu_boost(); |
| } |
| |
| queue_wait(&codec_queue, &ev); |
| codec_requested_stop = false; |
| |
| switch (ev.id) { |
| case Q_CODEC_LOAD_DISK: |
| LOGFQUEUE("codec < Q_CODEC_LOAD_DISK"); |
| queue_reply(&codec_queue, 1); |
| audio_codec_loaded = true; |
| ci.stop_codec = false; |
| status = codec_load_file((const char *)ev.data, &ci); |
| LOGFQUEUE("codec_load_file %s %d\n", (const char *)ev.data, status); |
| break; |
| |
| case Q_CODEC_LOAD: |
| LOGFQUEUE("codec < Q_CODEC_LOAD"); |
| if (*get_codec_hid() < 0) { |
| logf("Codec slot is empty!"); |
| /* Wait for the pcm buffer to go empty */ |
| while (pcm_is_playing()) |
| yield(); |
| /* This must be set to prevent an infinite loop */ |
| ci.stop_codec = true; |
| LOGFQUEUE("codec > codec Q_AUDIO_PLAY"); |
| queue_post(&codec_queue, Q_AUDIO_PLAY, 0); |
| break; |
| } |
| |
| audio_codec_loaded = true; |
| ci.stop_codec = false; |
| status = codec_load_buf(*get_codec_hid(), &ci); |
| LOGFQUEUE("codec_load_buf %d\n", status); |
| break; |
| |
| case Q_CODEC_DO_CALLBACK: |
| LOGFQUEUE("codec < Q_CODEC_DO_CALLBACK"); |
| queue_reply(&codec_queue, 1); |
| if ((void*)ev.data != NULL) |
| { |
| cpucache_invalidate(); |
| ((void (*)(void))ev.data)(); |
| cpucache_flush(); |
| } |
| break; |
| |
| #ifdef AUDIO_HAVE_RECORDING |
| case Q_ENCODER_LOAD_DISK: |
| LOGFQUEUE("codec < Q_ENCODER_LOAD_DISK"); |
| audio_codec_loaded = false; /* Not audio codec! */ |
| logf("loading encoder"); |
| ci.stop_encoder = false; |
| status = codec_load_file((const char *)ev.data, &ci); |
| logf("encoder stopped"); |
| break; |
| #endif /* AUDIO_HAVE_RECORDING */ |
| |
| default: |
| LOGFQUEUE("codec < default"); |
| } |
| |
| if (audio_codec_loaded) |
| { |
| if (ci.stop_codec) |
| { |
| status = CODEC_OK; |
| if (!audio_is_playing()) |
| pcmbuf_play_stop(); |
| |
| } |
| audio_codec_loaded = false; |
| } |
| |
| switch (ev.id) { |
| case Q_CODEC_LOAD_DISK: |
| case Q_CODEC_LOAD: |
| LOGFQUEUE("codec < Q_CODEC_LOAD"); |
| if (audio_is_playing()) |
| { |
| if (ci.new_track || status != CODEC_OK) |
| { |
| if (!ci.new_track) |
| { |
| logf("Codec failure, %d %d", ci.new_track, status); |
| splash(HZ*2, "Codec failure"); |
| } |
| |
| if (!codec_load_next_track()) |
| { |
| LOGFQUEUE("codec > audio Q_AUDIO_STOP"); |
| /* End of playlist */ |
| queue_post(&audio_queue, Q_AUDIO_STOP, 0); |
| break; |
| } |
| } |
| else |
| { |
| logf("Codec finished"); |
| if (ci.stop_codec) |
| { |
| /* Wait for the audio to stop playing before |
| * triggering the WPS exit */ |
| while(pcm_is_playing()) |
| { |
| /* There has been one too many struct pointer swaps by now |
| * so even though it says othertrack_id3, its the correct one! */ |
| othertrack_id3->elapsed = |
| othertrack_id3->length - pcmbuf_get_latency(); |
| sleep(1); |
| } |
| |
| if (codec_requested_stop) |
| { |
| LOGFQUEUE("codec > audio Q_AUDIO_STOP"); |
| queue_post(&audio_queue, Q_AUDIO_STOP, 0); |
| } |
| break; |
| } |
| } |
| |
| if (*get_codec_hid() >= 0) |
| { |
| LOGFQUEUE("codec > codec Q_CODEC_LOAD"); |
| queue_post(&codec_queue, Q_CODEC_LOAD, 0); |
| } |
| else |
| { |
| const char *codec_fn = |
| get_codec_filename(thistrack_id3->codectype); |
| if (codec_fn) |
| { |
| LOGFQUEUE("codec > codec Q_CODEC_LOAD_DISK"); |
| queue_post(&codec_queue, Q_CODEC_LOAD_DISK, |
| (intptr_t)codec_fn); |
| } |
| } |
| } |
| break; |
| |
| #ifdef AUDIO_HAVE_RECORDING |
| case Q_ENCODER_LOAD_DISK: |
| LOGFQUEUE("codec < Q_ENCODER_LOAD_DISK"); |
| |
| if (status == CODEC_OK) |
| break; |
| |
| logf("Encoder failure"); |
| splash(HZ*2, "Encoder failure"); |
| |
| if (ci.enc_codec_loaded < 0) |
| break; |
| |
| logf("Encoder failed to load"); |
| ci.enc_codec_loaded = -1; |
| break; |
| #endif /* AUDIO_HAVE_RECORDING */ |
| |
| default: |
| LOGFQUEUE("codec < default"); |
| |
| } /* end switch */ |
| } |
| } |
| |
| void make_codec_thread(void) |
| { |
| codec_thread_id = create_thread( |
| codec_thread, codec_stack, sizeof(codec_stack), |
| CREATE_THREAD_FROZEN, |
| codec_thread_name IF_PRIO(, PRIORITY_PLAYBACK) |
| IF_COP(, CPU)); |
| queue_enable_queue_send(&codec_queue, &codec_queue_sender_list, |
| codec_thread_id); |
| } |