| /*************************************************************************** |
| * __________ __ ___. |
| * Open \______ \ ____ ____ | | _\_ |__ _______ ___ |
| * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / |
| * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < |
| * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ |
| * \/ \/ \/ \/ \/ |
| * $Id$ |
| * |
| * Copyright (C) 2005 Miika Pekkarinen |
| * |
| * All files in this archive are subject to the GNU General Public License. |
| * See the file COPYING in the source tree root for full license agreement. |
| * |
| * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY |
| * KIND, either express or implied. |
| * |
| ****************************************************************************/ |
| #include <stdio.h> |
| #include <string.h> |
| #include <stdlib.h> |
| #include <ctype.h> |
| |
| #include "system.h" |
| #include "thread.h" |
| #include "file.h" |
| #include "lcd.h" |
| #include "font.h" |
| #include "backlight.h" |
| #include "button.h" |
| #include "kernel.h" |
| #include "tree.h" |
| #include "debug.h" |
| #include "sprintf.h" |
| #include "settings.h" |
| #include "codecs.h" |
| #include "audio.h" |
| #include "logf.h" |
| #include "mp3_playback.h" |
| #include "usb.h" |
| #include "status.h" |
| #include "main_menu.h" |
| #include "ata.h" |
| #include "screens.h" |
| #include "playlist.h" |
| #include "playback.h" |
| #include "pcmbuf.h" |
| #include "pcm_playback.h" |
| #include "pcm_record.h" |
| #include "buffer.h" |
| #include "dsp.h" |
| #include "abrepeat.h" |
| #ifdef HAVE_LCD_BITMAP |
| #include "icons.h" |
| #include "peakmeter.h" |
| #include "action.h" |
| #endif |
| #include "lang.h" |
| #include "bookmark.h" |
| #include "misc.h" |
| #include "sound.h" |
| #include "metadata.h" |
| #include "talk.h" |
| #ifdef CONFIG_TUNER |
| #include "radio.h" |
| #endif |
| #include "splash.h" |
| |
| static volatile bool audio_codec_loaded; |
| static volatile bool voice_codec_loaded; |
| static volatile bool playing; |
| static volatile bool paused; |
| |
| #define CODEC_VORBIS "/.rockbox/codecs/vorbis.codec" |
| #define CODEC_MPA_L3 "/.rockbox/codecs/mpa.codec" |
| #define CODEC_FLAC "/.rockbox/codecs/flac.codec" |
| #define CODEC_WAV "/.rockbox/codecs/wav.codec" |
| #define CODEC_A52 "/.rockbox/codecs/a52.codec" |
| #define CODEC_MPC "/.rockbox/codecs/mpc.codec" |
| #define CODEC_WAVPACK "/.rockbox/codecs/wavpack.codec" |
| #define CODEC_ALAC "/.rockbox/codecs/alac.codec" |
| #define CODEC_AAC "/.rockbox/codecs/aac.codec" |
| #define CODEC_SHN "/.rockbox/codecs/shorten.codec" |
| #define CODEC_AIFF "/.rockbox/codecs/aiff.codec" |
| |
| #define AUDIO_DEFAULT_FIRST_LIMIT (1024*1024*10) |
| #define AUDIO_FILL_CYCLE (1024*256) |
| #define AUDIO_DEFAULT_WATERMARK (1024*512) |
| #define AUDIO_DEFAULT_FILECHUNK (1024*32) |
| |
| enum { |
| Q_AUDIO_PLAY = 1, |
| Q_AUDIO_STOP, |
| Q_AUDIO_PAUSE, |
| Q_AUDIO_RESUME, |
| Q_AUDIO_NEXT, |
| Q_AUDIO_PREV, |
| Q_AUDIO_FF_REWIND, |
| Q_AUDIO_FLUSH_RELOAD, |
| Q_AUDIO_CODEC_DONE, |
| Q_AUDIO_FLUSH, |
| Q_AUDIO_TRACK_CHANGED, |
| Q_AUDIO_DIR_NEXT, |
| Q_AUDIO_DIR_PREV, |
| Q_AUDIO_SEAMLESS_SEEK, |
| Q_AUDIO_POSTINIT, |
| |
| Q_CODEC_LOAD, |
| Q_CODEC_LOAD_DISK, |
| }; |
| |
| /* As defined in plugins/lib/xxx2wav.h */ |
| #define MALLOC_BUFSIZE (512*1024) |
| #define GUARD_BUFSIZE (32*1024) |
| |
| /* As defined in plugin.lds */ |
| #define CODEC_IRAM_ORIGIN 0x1000c000 |
| #define CODEC_IRAM_SIZE 0xc000 |
| |
| extern bool audio_is_initialized; |
| |
| /* Buffer control thread. */ |
| static struct event_queue audio_queue; |
| static long audio_stack[(DEFAULT_STACK_SIZE + 0x1000)/sizeof(long)]; |
| static const char audio_thread_name[] = "audio"; |
| |
| /* Codec thread. */ |
| static struct event_queue codec_queue; |
| static long codec_stack[(DEFAULT_STACK_SIZE + 0x2000)/sizeof(long)] IBSS_ATTR; |
| static const char codec_thread_name[] = "codec"; |
| |
| /* Voice codec thread. */ |
| static struct event_queue voice_codec_queue; |
| /* Not enough IRAM for this. */ |
| static long voice_codec_stack[(DEFAULT_STACK_SIZE + 0x2000)/sizeof(long)] IBSS_ATTR; |
| static const char voice_codec_thread_name[] = "voice codec"; |
| |
| static struct mutex mutex_bufferfill; |
| static struct mutex mutex_codecthread; |
| |
| static struct mp3entry id3_voice; |
| |
| static char *voicebuf; |
| static int voice_remaining; |
| static bool voice_is_playing; |
| static void (*voice_getmore)(unsigned char** start, int* size); |
| |
| /* Is file buffer currently being refilled? */ |
| static volatile bool filling; |
| |
| volatile int current_codec; |
| extern unsigned char codecbuf[]; |
| |
| /* Ring buffer where tracks and codecs are loaded. */ |
| static char *filebuf; |
| |
| /* Total size of the ring buffer. */ |
| int filebuflen; |
| |
| /* Bytes available in the buffer. */ |
| int filebufused; |
| |
| /* Ring buffer read and write indexes. */ |
| static volatile int buf_ridx; |
| static volatile int buf_widx; |
| |
| /* Step count to the next unbuffered track. */ |
| static int last_peek_offset; |
| |
| /* Track information (count in file buffer, read/write indexes for |
| track ring structure. */ |
| int track_count; |
| static volatile int track_ridx; |
| static volatile int track_widx; |
| static bool track_changed; |
| |
| /* Partially loaded song's file handle to continue buffering later. */ |
| static int current_fd; |
| |
| /* Information about how many bytes left on the buffer re-fill run. */ |
| static long fill_bytesleft; |
| |
| /* Track info structure about songs in the file buffer. */ |
| static struct track_info tracks[MAX_TRACK]; |
| |
| /* Pointer to track info structure about current song playing. */ |
| static struct track_info *cur_ti; |
| static struct track_info *prev_ti; |
| |
| /* Have we reached end of the current playlist. */ |
| static bool playlist_end = false; |
| |
| /* Codec API including function callbacks. */ |
| extern struct codec_api ci; |
| extern struct codec_api ci_voice; |
| |
| /* When we change a song and buffer is not in filling state, this |
| variable keeps information about whether to go a next/previous track. */ |
| static int new_track; |
| |
| /* Callback function to call when current track has really changed. */ |
| void (*track_changed_callback)(struct mp3entry *id3); |
| void (*track_buffer_callback)(struct mp3entry *id3, bool last_track); |
| void (*track_unbuffer_callback)(struct mp3entry *id3, bool last_track); |
| |
| static void playback_init(void); |
| |
| /* Configuration */ |
| static int conf_bufferlimit; |
| static int conf_watermark; |
| static int conf_filechunk; |
| static int buffer_margin; |
| |
| static bool v1first = false; |
| |
| static void mp3_set_elapsed(struct mp3entry* id3); |
| int mp3_get_file_pos(void); |
| |
| static void do_swap(int idx_old, int idx_new) |
| { |
| #ifndef SIMULATOR |
| unsigned char *iram_p = (unsigned char *)(CODEC_IRAM_ORIGIN); |
| unsigned char *iram_buf[2]; |
| #endif |
| unsigned char *dram_buf[2]; |
| |
| |
| #ifndef SIMULATOR |
| iram_buf[0] = &filebuf[filebuflen]; |
| iram_buf[1] = &filebuf[filebuflen+CODEC_IRAM_SIZE]; |
| memcpy(iram_buf[idx_old], iram_p, CODEC_IRAM_SIZE); |
| memcpy(iram_p, iram_buf[idx_new], CODEC_IRAM_SIZE); |
| #endif |
| |
| dram_buf[0] = (unsigned char *)&filebuf[filebuflen+CODEC_IRAM_SIZE*2]; |
| dram_buf[1] = (unsigned char *)&filebuf[filebuflen+CODEC_IRAM_SIZE*2+CODEC_SIZE]; |
| memcpy(dram_buf[idx_old], codecbuf, CODEC_SIZE); |
| memcpy(codecbuf, dram_buf[idx_new], CODEC_SIZE); |
| } |
| |
| static void swap_codec(void) |
| { |
| int last_codec; |
| |
| logf("swapping codec:%d", current_codec); |
| |
| /* We should swap codecs' IRAM contents and code space. */ |
| do_swap(current_codec, !current_codec); |
| |
| last_codec = current_codec; |
| current_codec = !current_codec; |
| |
| /* Release the semaphore and force a task switch. */ |
| mutex_unlock(&mutex_codecthread); |
| sleep(1); |
| |
| /* Waiting until we are ready to run again. */ |
| mutex_lock(&mutex_codecthread); |
| |
| /* Check if codec swap did not happen. */ |
| if (current_codec != last_codec) |
| { |
| logf("no codec switch happened!"); |
| do_swap(current_codec, !current_codec); |
| current_codec = !current_codec; |
| } |
| |
| invalidate_icache(); |
| logf("codec resuming:%d", current_codec); |
| } |
| |
| #ifdef HAVE_ADJUSTABLE_CPU_FREQ |
| static void voice_boost_cpu(bool state) |
| { |
| static bool voice_cpu_boosted = false; |
| |
| if (!voice_codec_loaded) |
| state = false; |
| |
| if (state != voice_cpu_boosted) |
| { |
| cpu_boost(state); |
| voice_cpu_boosted = state; |
| } |
| } |
| #else |
| #define voice_boost_cpu(state) do { } while(0) |
| #endif |
| |
| bool codec_pcmbuf_insert_split_callback(void *ch1, void *ch2, |
| long length) |
| { |
| char* src[2]; |
| char *dest; |
| long input_size; |
| long output_size; |
| |
| src[0] = ch1; |
| src[1] = ch2; |
| |
| while (paused) |
| { |
| sleep(1); |
| if (ci.stop_codec || ci.reload_codec || ci.seek_time) |
| return true; |
| } |
| |
| if (dsp_stereo_mode() == STEREO_NONINTERLEAVED) |
| { |
| length *= 2; /* Length is per channel */ |
| } |
| |
| while (length > 0) { |
| /* This will prevent old audio from playing when skipping tracks. */ |
| if ((ci.reload_codec || ci.stop_codec) && |
| current_codec != CODEC_IDX_VOICE) |
| return true; |
| |
| while ((dest = pcmbuf_request_buffer(dsp_output_size(length), |
| &output_size)) == NULL) { |
| sleep(1); |
| if ((ci.reload_codec || ci.stop_codec) && |
| current_codec != CODEC_IDX_VOICE) |
| return true; |
| } |
| |
| /* Get the real input_size for output_size bytes, guarding |
| * against resampling buffer overflows. */ |
| input_size = dsp_input_size(output_size); |
| if (input_size > length) { |
| DEBUGF("Error: dsp_input_size(%ld=dsp_output_size(%ld))=%ld > %ld\n", |
| output_size, length, input_size, length); |
| input_size = length; |
| } |
| |
| if (input_size <= 0) { |
| pcmbuf_flush_buffer(0); |
| DEBUGF("Warning: dsp_input_size(%ld=dsp_output_size(%ld))=%ld <= 0\n", |
| output_size, length, input_size); |
| /* should we really continue, or should we break? |
| * We should probably continue because calling |
| * pcmbuf_flush_buffer(0) will wrap the buffer if it was fully |
| * filled and so next call to pcmbuf_request_buffer should give |
| * the requested output_size. */ |
| continue; |
| } |
| |
| output_size = dsp_process(dest, src, input_size); |
| |
| /* Hotswap between audio and voice codecs as necessary. */ |
| switch (current_codec) |
| { |
| case CODEC_IDX_AUDIO: |
| pcmbuf_flush_buffer(output_size); |
| if (voice_is_playing && pcmbuf_usage() > 30 |
| && pcmbuf_mix_usage() < 20) |
| { |
| voice_boost_cpu(true); |
| swap_codec(); |
| voice_boost_cpu(false); |
| } |
| break ; |
| |
| case CODEC_IDX_VOICE: |
| if (audio_codec_loaded) { |
| pcmbuf_mix(dest, output_size); |
| if ((pcmbuf_usage() < 10) |
| || pcmbuf_mix_usage() > 70) |
| swap_codec(); |
| } else { |
| pcmbuf_flush_buffer(output_size); |
| } |
| break ; |
| } |
| |
| length -= input_size; |
| } |
| |
| return true; |
| } |
| |
| bool codec_pcmbuf_insert_callback(char *buf, long length) |
| { |
| /* TODO: The audiobuffer API should probably be updated, and be based on |
| * pcmbuf_insert_split(). |
| */ |
| long real_length = length; |
| |
| if (dsp_stereo_mode() == STEREO_NONINTERLEAVED) |
| { |
| length /= 2; /* Length is per channel */ |
| } |
| |
| /* Second channel is only used for non-interleaved stereo. */ |
| return codec_pcmbuf_insert_split_callback(buf, buf + (real_length / 2), |
| length); |
| } |
| |
| void* get_codec_memory_callback(long *size) |
| { |
| *size = MALLOC_BUFSIZE; |
| if (voice_codec_loaded) |
| return &audiobuf[talk_get_bufsize()]; |
| |
| return &audiobuf[0]; |
| } |
| |
| static void pcmbuf_position_callback(int size) ICODE_ATTR; |
| static void pcmbuf_position_callback(int size) { |
| unsigned int time = size * 1000 / 4 / 44100 + prev_ti->id3.elapsed; |
| if (time >= prev_ti->id3.length) { |
| pcmbuf_set_position_callback(NULL); |
| prev_ti->id3.elapsed = prev_ti->id3.length; |
| } else { |
| prev_ti->id3.elapsed = time; |
| } |
| } |
| |
| void codec_set_elapsed_callback(unsigned int value) |
| { |
| unsigned int latency; |
| |
| if (ci.stop_codec || current_codec == CODEC_IDX_VOICE) |
| return ; |
| |
| #ifdef AB_REPEAT_ENABLE |
| ab_position_report(value); |
| #endif |
| latency = pcmbuf_get_latency(); |
| |
| if (value < latency) { |
| cur_ti->id3.elapsed = 0; |
| } else if (value - latency > cur_ti->id3.elapsed |
| || value - latency < cur_ti->id3.elapsed - 2) { |
| cur_ti->id3.elapsed = value - latency; |
| } |
| } |
| |
| void codec_set_offset_callback(unsigned int value) |
| { |
| unsigned int latency; |
| |
| if (ci.stop_codec || current_codec == CODEC_IDX_VOICE) |
| return ; |
| |
| latency = pcmbuf_get_latency() * cur_ti->id3.bitrate / 8; |
| |
| if (value < latency) { |
| cur_ti->id3.offset = 0; |
| } else { |
| cur_ti->id3.offset = value - latency; |
| } |
| } |
| |
| long codec_filebuf_callback(void *ptr, long size) |
| { |
| char *buf = (char *)ptr; |
| int copy_n; |
| int part_n; |
| |
| if (ci.stop_codec || !playing || current_codec == CODEC_IDX_VOICE) |
| return 0; |
| |
| copy_n = MIN((off_t)size, (off_t)cur_ti->available + cur_ti->filerem); |
| |
| while (copy_n > cur_ti->available) { |
| yield(); |
| if (ci.stop_codec || ci.reload_codec) |
| return 0; |
| } |
| |
| if (copy_n == 0) |
| return 0; |
| |
| part_n = MIN(copy_n, filebuflen - buf_ridx); |
| memcpy(buf, &filebuf[buf_ridx], part_n); |
| if (part_n < copy_n) { |
| memcpy(&buf[part_n], &filebuf[0], copy_n - part_n); |
| } |
| |
| buf_ridx += copy_n; |
| if (buf_ridx >= filebuflen) |
| buf_ridx -= filebuflen; |
| ci.curpos += copy_n; |
| cur_ti->available -= copy_n; |
| filebufused -= copy_n; |
| |
| return copy_n; |
| } |
| |
| void* voice_request_data(long *realsize, long reqsize) |
| { |
| while (queue_empty(&voice_codec_queue) && (voice_remaining == 0 |
| || voicebuf == NULL) && !ci_voice.stop_codec) |
| { |
| yield(); |
| if (audio_codec_loaded && (pcmbuf_usage() < 30 |
| || !voice_is_playing || voicebuf == NULL)) |
| { |
| swap_codec(); |
| } |
| else if (!voice_is_playing) |
| { |
| voice_boost_cpu(false); |
| if (!pcm_is_playing()) |
| pcmbuf_boost(false); |
| sleep(HZ/16); |
| } |
| |
| if (voice_remaining) |
| { |
| voice_is_playing = true; |
| break ; |
| } |
| |
| if (voice_getmore != NULL) |
| { |
| voice_getmore((unsigned char **)&voicebuf, (int *)&voice_remaining); |
| |
| if (!voice_remaining) |
| { |
| voice_is_playing = false; |
| /* Force pcm playback. */ |
| pcmbuf_play_start(); |
| } |
| } |
| } |
| |
| if (reqsize < 0) |
| reqsize = 0; |
| |
| voice_is_playing = true; |
| *realsize = voice_remaining; |
| if (*realsize > reqsize) |
| *realsize = reqsize; |
| |
| if (*realsize == 0) |
| return NULL; |
| |
| return voicebuf; |
| } |
| |
| void* codec_request_buffer_callback(long *realsize, long reqsize) |
| { |
| long part_n; |
| |
| /* Voice codec. */ |
| if (current_codec == CODEC_IDX_VOICE) { |
| return voice_request_data(realsize, reqsize); |
| } |
| |
| if (ci.stop_codec || !playing) { |
| *realsize = 0; |
| return NULL; |
| } |
| |
| *realsize = MIN((off_t)reqsize, (off_t)cur_ti->available + cur_ti->filerem); |
| if (*realsize == 0) { |
| return NULL; |
| } |
| |
| while ((int)*realsize > cur_ti->available) { |
| yield(); |
| if (ci.stop_codec || ci.reload_codec) { |
| *realsize = 0; |
| return NULL; |
| } |
| } |
| |
| part_n = MIN((int)*realsize, filebuflen - buf_ridx); |
| if (part_n < *realsize) { |
| part_n += GUARD_BUFSIZE; |
| if (part_n < *realsize) |
| *realsize = part_n; |
| memcpy(&filebuf[filebuflen], &filebuf[0], *realsize - |
| (filebuflen - buf_ridx)); |
| } |
| |
| return (char *)&filebuf[buf_ridx]; |
| } |
| |
| static bool rebuffer_and_seek(int newpos) |
| { |
| int fd; |
| |
| logf("Re-buffering song"); |
| mutex_lock(&mutex_bufferfill); |
| |
| /* (Re-)open current track's file handle. */ |
| fd = open(playlist_peek(0), O_RDONLY); |
| if (fd < 0) { |
| logf("Open failed!"); |
| mutex_unlock(&mutex_bufferfill); |
| return false; |
| } |
| if (current_fd >= 0) |
| close(current_fd); |
| current_fd = fd; |
| |
| /* Clear codec buffer. */ |
| audio_invalidate_tracks(); |
| filebufused = 0; |
| playlist_end = false; |
| buf_ridx = buf_widx = 0; |
| cur_ti->filerem = cur_ti->filesize - newpos; |
| cur_ti->filepos = newpos; |
| cur_ti->start_pos = newpos; |
| ci.curpos = newpos; |
| cur_ti->available = 0; |
| lseek(current_fd, newpos, SEEK_SET); |
| |
| mutex_unlock(&mutex_bufferfill); |
| |
| while (cur_ti->available == 0 && cur_ti->filerem > 0) { |
| sleep(1); |
| if (ci.stop_codec || ci.reload_codec || !queue_empty(&audio_queue)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void codec_advance_buffer_callback(long amount) |
| { |
| if (current_codec == CODEC_IDX_VOICE) { |
| //logf("voice ad.buf:%d", amount); |
| amount = MAX(0, MIN(amount, voice_remaining)); |
| voicebuf += amount; |
| voice_remaining -= amount; |
| |
| return ; |
| } |
| |
| if (amount > cur_ti->available + cur_ti->filerem) |
| amount = cur_ti->available + cur_ti->filerem; |
| |
| while (amount > cur_ti->available && filling) |
| sleep(1); |
| |
| if (amount > cur_ti->available) { |
| if (!rebuffer_and_seek(ci.curpos + amount)) |
| ci.stop_codec = true; |
| return ; |
| } |
| |
| buf_ridx += amount; |
| if (buf_ridx >= filebuflen) |
| buf_ridx -= filebuflen; |
| cur_ti->available -= amount; |
| filebufused -= amount; |
| ci.curpos += amount; |
| codec_set_offset_callback(ci.curpos); |
| } |
| |
| void codec_advance_buffer_loc_callback(void *ptr) |
| { |
| long amount; |
| |
| if (current_codec == CODEC_IDX_VOICE) |
| amount = (int)ptr - (int)voicebuf; |
| else |
| amount = (int)ptr - (int)&filebuf[buf_ridx]; |
| codec_advance_buffer_callback(amount); |
| } |
| |
| off_t codec_mp3_get_filepos_callback(int newtime) |
| { |
| off_t newpos; |
| |
| cur_ti->id3.elapsed = newtime; |
| newpos = mp3_get_file_pos(); |
| |
| return newpos; |
| } |
| |
| void codec_seek_complete_callback(void) |
| { |
| /* assume we're called from non-voice codec, as they shouldn't seek */ |
| ci.seek_time = 0; |
| } |
| |
| bool codec_seek_buffer_callback(off_t newpos) |
| { |
| int difference; |
| |
| if (current_codec == CODEC_IDX_VOICE) |
| return false; |
| |
| if (newpos < 0) |
| newpos = 0; |
| |
| if (newpos >= cur_ti->filesize) |
| newpos = cur_ti->filesize - 1; |
| |
| difference = newpos - ci.curpos; |
| /* Seeking forward */ |
| if (difference >= 0) { |
| logf("seek: +%d", difference); |
| codec_advance_buffer_callback(difference); |
| return true; |
| } |
| |
| /* Seeking backward */ |
| difference = -difference; |
| if (ci.curpos - difference < 0) |
| difference = ci.curpos; |
| |
| /* We need to reload the song. */ |
| if (newpos < cur_ti->start_pos) |
| return rebuffer_and_seek(newpos); |
| |
| /* Seeking inside buffer space. */ |
| logf("seek: -%d", difference); |
| filebufused += difference; |
| cur_ti->available += difference; |
| buf_ridx -= difference; |
| if (buf_ridx < 0) |
| buf_ridx = filebuflen + buf_ridx; |
| ci.curpos -= difference; |
| |
| return true; |
| } |
| |
| static void set_filebuf_watermark(int seconds) |
| { |
| long bytes; |
| |
| if (current_codec == CODEC_IDX_VOICE) |
| return ; |
| |
| if (!filebuf) |
| return; /* Audio buffers not yet set up */ |
| |
| bytes = MAX((int)cur_ti->id3.bitrate * seconds * (1000/8), conf_watermark); |
| bytes = MIN(bytes, filebuflen / 2); |
| conf_watermark = bytes; |
| } |
| |
| static void codec_configure_callback(int setting, void *value) |
| { |
| switch (setting) { |
| case CODEC_SET_FILEBUF_WATERMARK: |
| conf_watermark = (unsigned int)value; |
| set_filebuf_watermark(buffer_margin); |
| break; |
| |
| case CODEC_SET_FILEBUF_CHUNKSIZE: |
| conf_filechunk = (unsigned int)value; |
| break; |
| |
| case CODEC_DSP_ENABLE: |
| if ((bool)value) |
| ci.pcmbuf_insert = codec_pcmbuf_insert_callback; |
| else |
| ci.pcmbuf_insert = pcmbuf_insert_buffer; |
| break ; |
| |
| default: |
| if (!dsp_configure(setting, value)) { |
| logf("Illegal key: %d", setting); |
| } |
| } |
| } |
| |
| void audio_set_track_buffer_event(void (*handler)(struct mp3entry *id3, |
| bool last_track)) |
| { |
| track_buffer_callback = handler; |
| } |
| |
| void audio_set_track_unbuffer_event(void (*handler)(struct mp3entry *id3, |
| bool last_track)) |
| { |
| track_unbuffer_callback = handler; |
| } |
| |
| void audio_set_track_changed_event(void (*handler)(struct mp3entry *id3)) |
| { |
| track_changed_callback = handler; |
| } |
| |
| static void codec_track_changed(void) |
| { |
| track_changed = true; |
| queue_post(&audio_queue, Q_AUDIO_TRACK_CHANGED, 0); |
| } |
| |
| /* Give codecs or file buffering the right amount of processing time |
| to prevent pcm audio buffer from going empty. */ |
| static void yield_codecs(void) |
| { |
| yield(); |
| if (!pcm_is_playing() && !paused) |
| sleep(5); |
| while ((pcmbuf_is_crossfade_active() || pcmbuf_is_lowdata()) |
| && !ci.stop_codec && playing && queue_empty(&audio_queue) |
| && filebufused > (128*1024)) |
| sleep(1); |
| } |
| |
| /* FIXME: This code should be made more generic and move to metadata.c */ |
| void strip_id3v1_tag(void) |
| { |
| int i; |
| static const unsigned char tag[] = "TAG"; |
| int tagptr; |
| bool found = true; |
| |
| if (filebufused >= 128) |
| { |
| tagptr = buf_widx - 128; |
| if (tagptr < 0) |
| tagptr += filebuflen; |
| |
| for(i = 0;i < 3;i++) |
| { |
| if(tagptr >= filebuflen) |
| tagptr -= filebuflen; |
| |
| if(filebuf[tagptr] != tag[i]) |
| { |
| found = false; |
| break; |
| } |
| |
| tagptr++; |
| } |
| |
| if(found) |
| { |
| /* Skip id3v1 tag */ |
| logf("Skipping ID3v1 tag\n"); |
| buf_widx -= 128; |
| tracks[track_widx].available -= 128; |
| filebufused -= 128; |
| } |
| } |
| } |
| |
| static void audio_fill_file_buffer(void) |
| { |
| long i, size; |
| int rc; |
| |
| if (current_fd < 0) |
| return ; |
| |
| /* Throw away buffered codec. */ |
| if (tracks[track_widx].start_pos != 0) |
| tracks[track_widx].codecsize = 0; |
| |
| mutex_lock(&mutex_bufferfill); |
| i = 0; |
| size = MIN(tracks[track_widx].filerem, AUDIO_FILL_CYCLE); |
| while (i < size) { |
| /* Give codecs some processing time. */ |
| yield_codecs(); |
| |
| if (fill_bytesleft == 0) |
| break ; |
| rc = MIN(conf_filechunk, filebuflen - buf_widx); |
| rc = MIN(rc, fill_bytesleft); |
| rc = read(current_fd, &filebuf[buf_widx], rc); |
| if (rc <= 0) { |
| tracks[track_widx].filerem = 0; |
| break ; |
| } |
| |
| buf_widx += rc; |
| if (buf_widx >= filebuflen) |
| buf_widx -= filebuflen; |
| i += rc; |
| tracks[track_widx].available += rc; |
| tracks[track_widx].filerem -= rc; |
| tracks[track_widx].filepos += rc; |
| filebufused += rc; |
| fill_bytesleft -= rc; |
| } |
| |
| if (tracks[track_widx].filerem == 0) { |
| strip_id3v1_tag(); |
| } |
| |
| mutex_unlock(&mutex_bufferfill); |
| |
| /*logf("Filled:%d/%d", tracks[track_widx].available, |
| tracks[track_widx].filerem);*/ |
| } |
| |
| static 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; |
| } |
| |
| static bool loadcodec(bool start_play) |
| { |
| off_t size; |
| int fd; |
| int i, rc; |
| const char *codec_path; |
| int copy_n; |
| int prev_track; |
| |
| switch (tracks[track_widx].id3.codectype) { |
| case AFMT_OGG_VORBIS: |
| logf("Codec: Vorbis"); |
| codec_path = CODEC_VORBIS; |
| break; |
| case AFMT_MPA_L1: |
| case AFMT_MPA_L2: |
| case AFMT_MPA_L3: |
| logf("Codec: MPA L1/L2/L3"); |
| codec_path = CODEC_MPA_L3; |
| break; |
| case AFMT_PCM_WAV: |
| logf("Codec: PCM WAV"); |
| codec_path = CODEC_WAV; |
| break; |
| case AFMT_FLAC: |
| logf("Codec: FLAC"); |
| codec_path = CODEC_FLAC; |
| break; |
| case AFMT_A52: |
| logf("Codec: A52"); |
| codec_path = CODEC_A52; |
| break; |
| case AFMT_MPC: |
| logf("Codec: Musepack"); |
| codec_path = CODEC_MPC; |
| break; |
| case AFMT_WAVPACK: |
| logf("Codec: WAVPACK"); |
| codec_path = CODEC_WAVPACK; |
| break; |
| case AFMT_ALAC: |
| logf("Codec: ALAC"); |
| codec_path = CODEC_ALAC; |
| break; |
| case AFMT_AAC: |
| logf("Codec: AAC"); |
| codec_path = CODEC_AAC; |
| break; |
| case AFMT_SHN: |
| logf("Codec: SHN"); |
| codec_path = CODEC_SHN; |
| break; |
| case AFMT_AIFF: |
| logf("Codec: PCM AIFF"); |
| codec_path = CODEC_AIFF; |
| break; |
| default: |
| logf("Codec: Unsupported"); |
| codec_path = NULL; |
| return false; |
| } |
| |
| tracks[track_widx].codecsize = 0; |
| |
| if (!start_play) { |
| prev_track = track_widx - 1; |
| if (prev_track < 0) |
| prev_track = MAX_TRACK-1; |
| if (track_count > 0 && |
| get_codec_base_type(tracks[track_widx].id3.codectype) == |
| get_codec_base_type(tracks[prev_track].id3.codectype)) |
| { |
| logf("Reusing prev. codec"); |
| return true; |
| } |
| } else { |
| /* Load the codec directly from disk and save some memory. */ |
| cur_ti = &tracks[track_widx]; |
| ci.filesize = cur_ti->filesize; |
| ci.id3 = (struct mp3entry *)&cur_ti->id3; |
| ci.taginfo_ready = (bool *)&cur_ti->taginfo_ready; |
| ci.curpos = 0; |
| playing = true; |
| logf("Starting codec"); |
| queue_post(&codec_queue, Q_CODEC_LOAD_DISK, (void *)codec_path); |
| return true; |
| } |
| |
| fd = open(codec_path, O_RDONLY); |
| if (fd < 0) { |
| logf("Codec doesn't exist!"); |
| return false; |
| } |
| |
| size = filesize(fd); |
| if ((off_t)fill_bytesleft < size + conf_watermark) { |
| logf("Not enough space"); |
| /* Set codectype back to zero to indicate no codec was loaded. */ |
| tracks[track_widx].id3.codectype = 0; |
| fill_bytesleft = 0; |
| close(fd); |
| return false; |
| } |
| |
| i = 0; |
| while (i < size) { |
| yield_codecs(); |
| |
| copy_n = MIN(conf_filechunk, filebuflen - buf_widx); |
| rc = read(fd, &filebuf[buf_widx], copy_n); |
| if (rc < 0) |
| return false; |
| buf_widx += rc; |
| filebufused += rc; |
| fill_bytesleft -= rc; |
| if (buf_widx >= filebuflen) |
| buf_widx -= filebuflen; |
| i += rc; |
| } |
| close(fd); |
| logf("Done: %dB", i); |
| |
| tracks[track_widx].codecsize = size; |
| |
| return true; |
| } |
| |
| static bool read_next_metadata(void) |
| { |
| int fd; |
| char *trackname; |
| int next_track; |
| int status; |
| |
| next_track = track_widx; |
| if (tracks[track_widx].taginfo_ready) |
| next_track++; |
| |
| if (next_track >= MAX_TRACK) |
| next_track -= MAX_TRACK; |
| |
| if (tracks[next_track].taginfo_ready) |
| return true; |
| |
| trackname = playlist_peek(last_peek_offset + 1); |
| if (!trackname) |
| return false; |
| |
| fd = open(trackname, O_RDONLY); |
| if (fd < 0) |
| return false; |
| |
| /** Start buffer refilling also because we need to spin-up the disk. |
| * In fact, it might be better not to start filling here, because if user |
| * is manipulating the playlist a lot, we will just lose battery. */ |
| // filling = true; |
| status = get_metadata(&tracks[next_track],fd,trackname,v1first); |
| /* Preload the glyphs in the tags */ |
| if (status) { |
| if (tracks[next_track].id3.title) |
| lcd_getstringsize(tracks[next_track].id3.title, NULL, NULL); |
| if (tracks[next_track].id3.artist) |
| lcd_getstringsize(tracks[next_track].id3.artist, NULL, NULL); |
| if (tracks[next_track].id3.album) |
| lcd_getstringsize(tracks[next_track].id3.album, NULL, NULL); |
| } |
| track_changed = true; |
| close(fd); |
| |
| return status; |
| } |
| |
| static bool audio_load_track(int offset, bool start_play, int peek_offset) |
| { |
| char *trackname; |
| int fd = -1; |
| off_t size; |
| int rc, i; |
| int copy_n; |
| char msgbuf[80]; |
| |
| /* Stop buffer filling if there is no free track entries. |
| Don't fill up the last track entry (we wan't to store next track |
| metadata there). */ |
| if (track_count >= MAX_TRACK - 1) { |
| fill_bytesleft = 0; |
| return false; |
| } |
| |
| /* Don't start loading track if the current write position already |
| contains a BUFFERED track. The entry may contain the metadata |
| which is ok. */ |
| if (tracks[track_widx].filesize != 0) |
| return false; |
| |
| peek_again: |
| /* Get track name from current playlist read position. */ |
| logf("Buffering track:%d/%d", track_widx, track_ridx); |
| /* Handle broken playlists. */ |
| while ( (trackname = playlist_peek(peek_offset)) != NULL) { |
| fd = open(trackname, O_RDONLY); |
| if (fd < 0) { |
| logf("Open failed"); |
| /* Skip invalid entry from playlist. */ |
| playlist_skip_entry(NULL, peek_offset); |
| continue ; |
| } |
| break ; |
| } |
| |
| if (!trackname) { |
| logf("End-of-playlist"); |
| playlist_end = true; |
| return false; |
| } |
| |
| /* Initialize track entry. */ |
| size = filesize(fd); |
| tracks[track_widx].filerem = size; |
| tracks[track_widx].filesize = size; |
| tracks[track_widx].filepos = 0; |
| tracks[track_widx].available = 0; |
| //tracks[track_widx].taginfo_ready = false; |
| tracks[track_widx].playlist_offset = peek_offset; |
| last_peek_offset = peek_offset; |
| |
| if (buf_widx >= filebuflen) |
| buf_widx -= filebuflen; |
| |
| /* Set default values */ |
| if (start_play) { |
| int last_codec = current_codec; |
| current_codec = CODEC_IDX_AUDIO; |
| conf_bufferlimit = AUDIO_DEFAULT_FIRST_LIMIT; |
| conf_watermark = AUDIO_DEFAULT_WATERMARK; |
| conf_filechunk = AUDIO_DEFAULT_FILECHUNK; |
| dsp_configure(DSP_RESET, 0); |
| ci.configure(CODEC_DSP_ENABLE, false); |
| current_codec = last_codec; |
| } |
| |
| /* Get track metadata if we don't already have it. */ |
| if (!tracks[track_widx].taginfo_ready) { |
| if (!get_metadata(&tracks[track_widx],fd,trackname,v1first)) { |
| logf("Metadata error!"); |
| tracks[track_widx].filesize = 0; |
| tracks[track_widx].filerem = 0; |
| tracks[track_widx].taginfo_ready = false; |
| close(fd); |
| /* Skip invalid entry from playlist. */ |
| playlist_skip_entry(NULL, peek_offset); |
| goto peek_again; |
| } |
| } |
| |
| /* Load the codec. */ |
| tracks[track_widx].codecbuf = &filebuf[buf_widx]; |
| if (!loadcodec(start_play)) { |
| /* We should not use gui_syncplash from audio thread! */ |
| snprintf(msgbuf, sizeof(msgbuf)-1, "No codec for: %s", trackname); |
| gui_syncsplash(HZ*2, true, msgbuf); |
| close(fd); |
| |
| /* Set filesize to zero to indicate no file was loaded. */ |
| tracks[track_widx].filesize = 0; |
| tracks[track_widx].filerem = 0; |
| tracks[track_widx].taginfo_ready = false; |
| |
| /* Try skipping to next track. */ |
| if (fill_bytesleft > 0) { |
| /* Skip invalid entry from playlist. */ |
| playlist_skip_entry(NULL, peek_offset); |
| goto peek_again; |
| } |
| return false; |
| } |
| |
| tracks[track_widx].start_pos = 0; |
| set_filebuf_watermark(buffer_margin); |
| tracks[track_widx].id3.elapsed = 0; |
| |
| /* Starting playback from an offset is only support in MPA at the moment */ |
| if (offset > 0) { |
| switch (tracks[track_widx].id3.codectype) { |
| case AFMT_MPA_L2: |
| case AFMT_MPA_L3: |
| lseek(fd, offset, SEEK_SET); |
| tracks[track_widx].id3.offset = offset; |
| mp3_set_elapsed(&tracks[track_widx].id3); |
| tracks[track_widx].filepos = offset; |
| tracks[track_widx].filerem = tracks[track_widx].filesize - offset; |
| ci.curpos = offset; |
| tracks[track_widx].start_pos = offset; |
| break; |
| |
| case AFMT_WAVPACK: |
| lseek(fd, offset, SEEK_SET); |
| tracks[track_widx].id3.offset = offset; |
| tracks[track_widx].id3.elapsed = tracks[track_widx].id3.length / 2; |
| tracks[track_widx].filepos = offset; |
| tracks[track_widx].filerem = tracks[track_widx].filesize - offset; |
| ci.curpos = offset; |
| tracks[track_widx].start_pos = offset; |
| break; |
| case AFMT_OGG_VORBIS: |
| case AFMT_FLAC: |
| tracks[track_widx].id3.offset = offset; |
| break; |
| } |
| } |
| |
| if (start_play) { |
| track_count++; |
| codec_track_changed(); |
| } |
| |
| /* Do some initial file buffering. */ |
| mutex_lock(&mutex_bufferfill); |
| i = tracks[track_widx].start_pos; |
| size = MIN(size, AUDIO_FILL_CYCLE); |
| while (i < size) { |
| /* Give codecs some processing time to prevent glitches. */ |
| yield_codecs(); |
| |
| if (fill_bytesleft == 0) |
| break ; |
| |
| copy_n = MIN(conf_filechunk, filebuflen - buf_widx); |
| copy_n = MIN(size - i, copy_n); |
| copy_n = MIN((int)fill_bytesleft, copy_n); |
| rc = read(fd, &filebuf[buf_widx], copy_n); |
| if (rc < copy_n) { |
| logf("File error!"); |
| tracks[track_widx].filesize = 0; |
| tracks[track_widx].filerem = 0; |
| close(fd); |
| mutex_unlock(&mutex_bufferfill); |
| return false; |
| } |
| buf_widx += rc; |
| if (buf_widx >= filebuflen) |
| buf_widx -= filebuflen; |
| i += rc; |
| tracks[track_widx].available += rc; |
| tracks[track_widx].filerem -= rc; |
| filebufused += rc; |
| fill_bytesleft -= rc; |
| } |
| mutex_unlock(&mutex_bufferfill); |
| |
| if (!start_play) |
| track_count++; |
| |
| tracks[track_widx].filepos = i; |
| |
| if (current_fd >= 0) { |
| close(current_fd); |
| current_fd = -1; |
| } |
| |
| /* Leave the file handle open for faster buffer refill. */ |
| if (tracks[track_widx].filerem != 0) { |
| current_fd = fd; |
| logf("Partially buf:%d", tracks[track_widx].available); |
| } else { |
| logf("Completely buf."); |
| close(fd); |
| |
| strip_id3v1_tag(); |
| |
| if (++track_widx >= MAX_TRACK) { |
| track_widx = 0; |
| } |
| tracks[track_widx].filerem = 0; |
| } |
| |
| return true; |
| } |
| |
| static void audio_clear_track_entries(bool buffered_only) |
| { |
| int cur_idx, event_count; |
| int i; |
| |
| cur_idx = track_widx; |
| event_count = 0; |
| for (i = 0; i < MAX_TRACK - track_count; i++) { |
| if (++cur_idx >= MAX_TRACK) |
| cur_idx = 0; |
| |
| if (tracks[cur_idx].event_sent) |
| event_count++; |
| |
| if (!track_unbuffer_callback) |
| memset(&tracks[cur_idx], 0, sizeof(struct track_info)); |
| } |
| |
| if (!track_unbuffer_callback) |
| return ; |
| |
| cur_idx = track_widx; |
| for (i = 0; i < MAX_TRACK - track_count; i++) { |
| if (++cur_idx >= MAX_TRACK) |
| cur_idx = 0; |
| |
| /* Send an event to notify that track has finished. */ |
| if (tracks[cur_idx].event_sent) { |
| event_count--; |
| track_unbuffer_callback(&tracks[cur_idx].id3, event_count == 0); |
| } |
| |
| if (tracks[cur_idx].event_sent || !buffered_only) |
| memset(&tracks[cur_idx], 0, sizeof(struct track_info)); |
| } |
| } |
| |
| static void stop_codec_flush(void) |
| { |
| ci.stop_codec = true; |
| pcmbuf_play_stop(); |
| while (audio_codec_loaded) |
| yield(); |
| pcmbuf_play_stop(); |
| } |
| |
| static void audio_stop_playback(bool resume) |
| { |
| logf("stop_playback:%d", resume); |
| paused = false; |
| if (playing) |
| playlist_update_resume_info(resume ? audio_current_track() : NULL); |
| playing = false; |
| filling = false; |
| stop_codec_flush(); |
| if (current_fd >= 0) { |
| close(current_fd); |
| current_fd = -1; |
| } |
| |
| track_count = 0; |
| /* Mark all entries null. */ |
| audio_clear_track_entries(false); |
| } |
| |
| static void audio_play_start(int offset) |
| { |
| if (current_fd >= 0) { |
| close(current_fd); |
| current_fd = -1; |
| } |
| |
| memset(&tracks, 0, sizeof(struct track_info) * MAX_TRACK); |
| sound_set_volume(global_settings.volume); |
| track_count = 0; |
| track_widx = 0; |
| track_ridx = 0; |
| buf_ridx = 0; |
| buf_widx = 0; |
| filebufused = 0; |
| pcmbuf_set_boost_mode(true); |
| |
| fill_bytesleft = filebuflen; |
| filling = true; |
| last_peek_offset = -1; |
| |
| if (audio_load_track(offset, true, 0)) { |
| if (track_buffer_callback) { |
| cur_ti->event_sent = true; |
| track_buffer_callback(&cur_ti->id3, true); |
| } |
| } else { |
| logf("Failure"); |
| audio_stop_playback(false); |
| } |
| |
| pcmbuf_set_boost_mode(false); |
| } |
| |
| /* Send callback events to notify about new tracks. */ |
| static void generate_postbuffer_events(void) |
| { |
| int i; |
| int cur_ridx, event_count; |
| |
| /* At first determine how many unsent events we have. */ |
| cur_ridx = track_ridx; |
| event_count = 0; |
| for (i = 0; i < track_count; i++) { |
| if (!tracks[cur_ridx].event_sent) |
| event_count++; |
| if (++cur_ridx >= MAX_TRACK) |
| cur_ridx -= MAX_TRACK; |
| } |
| |
| /* Now sent these events. */ |
| cur_ridx = track_ridx; |
| for (i = 0; i < track_count; i++) { |
| if (!tracks[cur_ridx].event_sent) { |
| tracks[cur_ridx].event_sent = true; |
| event_count--; |
| /* We still want to set event_sent flags even if not using |
| event callbacks. */ |
| if (track_buffer_callback) |
| track_buffer_callback(&tracks[cur_ridx].id3, event_count == 0); |
| } |
| if (++cur_ridx >= MAX_TRACK) |
| cur_ridx -= MAX_TRACK; |
| } |
| } |
| |
| static void initialize_buffer_fill(void) |
| { |
| int cur_idx, i; |
| |
| /* Initialize only once; do not truncate the tracks. */ |
| if (filling) |
| return ; |
| |
| /* Save the current resume position once. */ |
| playlist_update_resume_info(audio_current_track()); |
| |
| fill_bytesleft = filebuflen - filebufused; |
| cur_ti->start_pos = ci.curpos; |
| |
| pcmbuf_set_boost_mode(true); |
| |
| filling = true; |
| |
| /* Calculate real track count after throwing away old tracks. */ |
| cur_idx = track_ridx; |
| for (i = 0; i < track_count; i++) { |
| if (cur_idx == track_widx) |
| break ; |
| |
| if (++cur_idx >= MAX_TRACK) |
| cur_idx = 0; |
| } |
| |
| track_count = i; |
| if (tracks[track_widx].filesize == 0) { |
| if (--track_widx < 0) |
| track_widx = MAX_TRACK - 1; |
| } else { |
| track_count++; |
| } |
| |
| /* Mark all buffered entries null (not metadata for next track). */ |
| audio_clear_track_entries(true); |
| } |
| |
| static void audio_check_buffer(void) |
| { |
| /* Start buffer filling as necessary. */ |
| if ((!conf_watermark || filebufused > conf_watermark |
| || !queue_empty(&audio_queue) || !playing || ci.stop_codec |
| || ci.reload_codec || playlist_end) && !filling) |
| return ; |
| |
| mutex_lock(&mutex_bufferfill); |
| initialize_buffer_fill(); |
| mutex_unlock(&mutex_bufferfill); |
| |
| /* Limit buffering size at first run. */ |
| if (conf_bufferlimit && fill_bytesleft > conf_bufferlimit |
| - filebufused) { |
| fill_bytesleft = MAX(0, conf_bufferlimit - filebufused); |
| } |
| |
| /* Try to load remainings of the file. */ |
| if (tracks[track_widx].filerem > 0) |
| audio_fill_file_buffer(); |
| |
| /* Increase track write index as necessary. */ |
| if (tracks[track_widx].filerem == 0 && tracks[track_widx].filesize != 0) { |
| if (++track_widx == MAX_TRACK) |
| track_widx = 0; |
| } |
| |
| /* Load new files to fill the entire buffer. */ |
| if (audio_load_track(0, false, last_peek_offset + 1)) { |
| if (conf_bufferlimit) |
| fill_bytesleft = 0; |
| } |
| else if (tracks[track_widx].filerem == 0) |
| fill_bytesleft = 0; |
| |
| if (fill_bytesleft <= 0) |
| { |
| /* Read next unbuffered track's metadata as necessary. */ |
| read_next_metadata(); |
| |
| generate_postbuffer_events(); |
| filling = false; |
| conf_bufferlimit = 0; |
| pcmbuf_set_boost_mode(false); |
| |
| #ifndef SIMULATOR |
| if (playing) |
| ata_sleep(); |
| #endif |
| } |
| } |
| |
| static void audio_update_trackinfo(void) |
| { |
| ci.filesize = cur_ti->filesize; |
| cur_ti->id3.elapsed = 0; |
| cur_ti->id3.offset = 0; |
| ci.id3 = (struct mp3entry *)&cur_ti->id3; |
| ci.curpos = 0; |
| cur_ti->start_pos = 0; |
| ci.taginfo_ready = (bool *)&cur_ti->taginfo_ready; |
| |
| /* Manual track change (always crossfade or flush audio). */ |
| if (new_track) |
| { |
| pcmbuf_crossfade_init(true); |
| codec_track_changed(); |
| } |
| |
| /* Automatic track change with crossfade. */ |
| else if (pcmbuf_is_crossfade_enabled() && !pcmbuf_is_crossfade_active()) |
| { |
| pcmbuf_crossfade_init(false); |
| codec_track_changed(); |
| } |
| |
| /* Gapless playback. */ |
| else |
| { |
| pcmbuf_add_event(codec_track_changed); |
| } |
| } |
| |
| enum { |
| SKIP_FAIL, |
| SKIP_OK_DISK, |
| SKIP_OK_RAM, |
| }; |
| |
| /* Should handle all situations. */ |
| static int skip_next_track(bool inside_codec_thread) |
| { |
| logf("skip next"); |
| /* Manual track skipping. */ |
| if (new_track > 0) |
| last_peek_offset--; |
| |
| /* Automatic track skipping. */ |
| else |
| { |
| if (!playlist_check(1)) { |
| ci.reload_codec = false; |
| return SKIP_FAIL; |
| } |
| last_peek_offset--; |
| playlist_next(1); |
| } |
| |
| if (++track_ridx >= MAX_TRACK) |
| track_ridx = 0; |
| |
| /* Wait for new track data. */ |
| while (tracks[track_ridx].filesize == 0 && filling |
| && !ci.stop_codec) |
| yield(); |
| |
| if (tracks[track_ridx].filesize <= 0) |
| { |
| logf("Loading from disk..."); |
| ci.reload_codec = true; |
| /* Stop playback if manual track change. */ |
| if (new_track != 0 && !pcmbuf_is_crossfade_enabled()) |
| { |
| if (inside_codec_thread) |
| pcmbuf_play_stop(); |
| else |
| stop_codec_flush(); |
| } |
| else if (pcmbuf_is_crossfade_enabled()) |
| pcmbuf_crossfade_init(new_track != 0); |
| |
| queue_post(&audio_queue, Q_AUDIO_PLAY, 0); |
| return SKIP_OK_DISK; |
| } |
| |
| buf_ridx += cur_ti->available; |
| filebufused -= cur_ti->available; |
| |
| cur_ti = &tracks[track_ridx]; |
| buf_ridx += cur_ti->codecsize; |
| filebufused -= cur_ti->codecsize; |
| if (buf_ridx >= filebuflen) |
| buf_ridx -= filebuflen; |
| audio_update_trackinfo(); |
| |
| if (!filling) |
| pcmbuf_set_boost_mode(false); |
| |
| return SKIP_OK_RAM; |
| } |
| |
| static int skip_previous_track(bool inside_codec_thread) |
| { |
| logf("skip previous"); |
| last_peek_offset++; |
| if (--track_ridx < 0) |
| track_ridx += MAX_TRACK; |
| |
| if (tracks[track_ridx].filesize == 0 || |
| filebufused+ci.curpos+tracks[track_ridx].filesize |
| /*+ (off_t)tracks[track_ridx].codecsize*/ > filebuflen) { |
| logf("Loading from disk..."); |
| ci.reload_codec = true; |
| /* Stop playback. */ |
| /* FIXME: Only stop playback if disk is not spinning! */ |
| if (pcmbuf_is_crossfade_enabled()) |
| pcmbuf_crossfade_init(true); |
| else if (inside_codec_thread) |
| pcmbuf_play_stop(); |
| else |
| stop_codec_flush(); |
| |
| queue_post(&audio_queue, Q_AUDIO_PLAY, 0); |
| return SKIP_OK_DISK; |
| } |
| |
| buf_ridx -= ci.curpos + cur_ti->codecsize; |
| filebufused += ci.curpos + cur_ti->codecsize; |
| cur_ti->available = cur_ti->filesize - cur_ti->filerem; |
| |
| cur_ti = &tracks[track_ridx]; |
| buf_ridx -= cur_ti->filesize; |
| filebufused += cur_ti->filesize; |
| cur_ti->available = cur_ti->filesize; |
| if (buf_ridx < 0) |
| buf_ridx += filebuflen; |
| |
| audio_update_trackinfo(); |
| |
| return SKIP_OK_RAM; |
| } |
| |
| /* Request the next track with new codec. */ |
| static void audio_change_track(void) |
| { |
| logf("change track"); |
| |
| if (!ci.reload_codec) |
| { |
| if (skip_next_track(false) == SKIP_FAIL) |
| { |
| logf("No more tracks"); |
| while (pcm_is_playing()) |
| sleep(1); |
| audio_stop_playback(false); |
| return ; |
| } |
| } |
| |
| ci.reload_codec = false; |
| /* Needed for fast skipping. */ |
| if (cur_ti->codecsize > 0) |
| queue_post(&codec_queue, Q_CODEC_LOAD, 0); |
| } |
| |
| bool codec_request_next_track_callback(void) |
| { |
| prev_ti = cur_ti; |
| |
| if (current_codec == CODEC_IDX_VOICE) { |
| voice_remaining = 0; |
| /* Terminate the codec if there are messages waiting on the queue or |
| the core has been requested the codec to be terminated. */ |
| return !ci_voice.stop_codec && queue_empty(&voice_codec_queue); |
| } |
| #ifdef AB_REPEAT_ENABLE |
| ab_end_of_track_report(); |
| #endif |
| |
| pcmbuf_set_position_callback(pcmbuf_position_callback); |
| |
| if (ci.stop_codec || !playing) |
| return false; |
| |
| logf("Request new track"); |
| |
| /* Advance to next track. */ |
| if (new_track >= 0 || !ci.reload_codec) { |
| if (skip_next_track(true) != SKIP_OK_RAM) |
| return false; |
| } |
| |
| /* Advance to previous track. */ |
| else { |
| if (skip_previous_track(true) != SKIP_OK_RAM) |
| return false; |
| } |
| |
| new_track = 0; |
| ci.reload_codec = false; |
| |
| logf("On-the-fly change"); |
| |
| /* Check if the next codec is the same file. */ |
| if (get_codec_base_type(prev_ti->id3.codectype) != |
| get_codec_base_type(cur_ti->id3.codectype)) |
| { |
| logf("New codec:%d/%d", cur_ti->id3.codectype, |
| tracks[track_ridx].id3.codectype); |
| |
| if (cur_ti->codecsize == 0) |
| { |
| logf("Loading from disk [2]..."); |
| queue_post(&audio_queue, Q_AUDIO_PLAY, 0); |
| } |
| else |
| ci.reload_codec = true; |
| |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /* Invalidates all but currently playing track. */ |
| void audio_invalidate_tracks(void) |
| { |
| if (track_count == 0) { |
| /* This call doesn't seem necessary anymore. Uncomment it |
| if things break */ |
| /* queue_post(&audio_queue, Q_AUDIO_PLAY, 0); */ |
| return ; |
| } |
| |
| playlist_end = false; |
| track_count = 1; |
| last_peek_offset = 0; |
| track_widx = track_ridx; |
| /* Mark all other entries null (also buffered wrong metadata). */ |
| audio_clear_track_entries(false); |
| filebufused = cur_ti->available; |
| buf_widx = buf_ridx + cur_ti->available; |
| if (buf_widx >= filebuflen) |
| buf_widx -= filebuflen; |
| read_next_metadata(); |
| } |
| |
| static void initiate_track_change(int peek_index) |
| { |
| /* Detect if disk is spinning or already loading. */ |
| if (filling || ci.reload_codec || !audio_codec_loaded) { |
| if (pcmbuf_is_crossfade_enabled()) |
| pcmbuf_crossfade_init(true); |
| else |
| pcmbuf_play_stop(); |
| ci.stop_codec = true; |
| queue_post(&audio_queue, Q_AUDIO_PLAY, 0); |
| } else { |
| new_track = peek_index; |
| ci.reload_codec = true; |
| } |
| |
| codec_track_changed(); |
| } |
| |
| static void initiate_dir_change(int direction) |
| { |
| if(!playlist_next_dir(direction)) |
| return; |
| |
| queue_post(&audio_queue, Q_AUDIO_PLAY, (bool *)true); |
| |
| codec_track_changed(); |
| } |
| |
| void audio_thread(void) |
| { |
| struct event ev; |
| int last_tick = 0; |
| bool play_pending = false; |
| |
| /* At first initialize audio system in background. */ |
| playback_init(); |
| |
| while (1) { |
| if (!play_pending && queue_empty(&audio_queue)) |
| { |
| yield_codecs(); |
| audio_check_buffer(); |
| } |
| else |
| { |
| // ata_spin(); |
| sleep(1); |
| } |
| |
| queue_wait_w_tmo(&audio_queue, &ev, 0); |
| if (ev.id == SYS_TIMEOUT && play_pending) |
| { |
| ev.id = Q_AUDIO_PLAY; |
| ev.data = (bool *)1; |
| } |
| |
| switch (ev.id) { |
| case Q_AUDIO_PLAY: |
| /* Don't start playing immediately if user is skipping tracks |
| * fast to prevent UI lag. */ |
| track_count = 0; |
| last_peek_offset = 0; |
| track_changed = true; |
| playlist_end = false; |
| if (current_tick - last_tick < HZ/2) |
| { |
| play_pending = true; |
| break ; |
| } |
| play_pending = false; |
| last_tick = current_tick; |
| |
| /* Do not start crossfading if audio is paused. */ |
| if (paused) |
| pcmbuf_play_stop(); |
| |
| #ifdef CONFIG_TUNER |
| /* check if radio is playing */ |
| if (get_radio_status() != FMRADIO_OFF) { |
| radio_stop(); |
| } |
| #endif |
| |
| logf("starting..."); |
| playing = true; |
| ci.stop_codec = true; |
| ci.reload_codec = false; |
| ci.seek_time = 0; |
| |
| while (audio_codec_loaded) |
| yield(); |
| |
| audio_play_start((int)ev.data); |
| playlist_update_resume_info(audio_current_track()); |
| |
| /* If there are no tracks in the playlist, then the playlist |
| was empty or none of the filenames were valid. No point |
| in playing an empty playlist. */ |
| if (playlist_amount() == 0) { |
| audio_stop_playback(false); |
| } |
| break ; |
| |
| case Q_AUDIO_STOP: |
| audio_stop_playback(true); |
| break ; |
| |
| case Q_AUDIO_PAUSE: |
| logf("audio_pause"); |
| pcm_mute(true); |
| pcm_play_pause(false); |
| paused = true; |
| break ; |
| |
| case Q_AUDIO_RESUME: |
| logf("audio_resume"); |
| pcm_play_pause(true); |
| pcm_mute(false); |
| paused = false; |
| break ; |
| |
| case Q_AUDIO_NEXT: |
| logf("audio_next"); |
| last_tick = current_tick; |
| playlist_end = false; |
| initiate_track_change(1); |
| break ; |
| |
| case Q_AUDIO_PREV: |
| logf("audio_prev"); |
| last_tick = current_tick; |
| playlist_end = false; |
| initiate_track_change(-1); |
| break; |
| |
| case Q_AUDIO_FF_REWIND: |
| if (!playing) |
| break ; |
| pcmbuf_play_stop(); |
| ci.seek_time = (int)ev.data+1; |
| break ; |
| |
| case Q_AUDIO_SEAMLESS_SEEK: |
| if (!playing) |
| break ; |
| ci.seek_time = (int)ev.data+1; |
| break ; |
| |
| case Q_AUDIO_DIR_NEXT: |
| logf("audio_dir_next"); |
| playlist_end = false; |
| if (global_settings.beep) |
| pcmbuf_beep(5000, 100, 2500*global_settings.beep); |
| initiate_dir_change(1); |
| break; |
| |
| case Q_AUDIO_DIR_PREV: |
| logf("audio_dir_prev"); |
| playlist_end = false; |
| if (global_settings.beep) |
| pcmbuf_beep(5000, 100, 2500*global_settings.beep); |
| initiate_dir_change(-1); |
| break; |
| |
| case Q_AUDIO_FLUSH: |
| audio_invalidate_tracks(); |
| break ; |
| |
| case Q_AUDIO_TRACK_CHANGED: |
| if (track_changed_callback) |
| track_changed_callback(&cur_ti->id3); |
| playlist_update_resume_info(audio_current_track()); |
| pcmbuf_set_position_callback(NULL); |
| break ; |
| |
| case Q_AUDIO_CODEC_DONE: |
| break ; |
| |
| #ifndef SIMULATOR |
| case SYS_USB_CONNECTED: |
| logf("USB: Audio core"); |
| audio_stop_playback(true); |
| usb_acknowledge(SYS_USB_CONNECTED_ACK); |
| usb_wait_for_disconnect(&audio_queue); |
| break ; |
| #endif |
| case SYS_TIMEOUT: |
| break; |
| } |
| } |
| } |
| |
| void codec_thread(void) |
| { |
| struct event ev; |
| long codecsize; |
| int status; |
| int wrap; |
| |
| while (1) { |
| status = 0; |
| queue_wait(&codec_queue, &ev); |
| new_track = 0; |
| |
| switch (ev.id) { |
| case Q_CODEC_LOAD_DISK: |
| ci.stop_codec = false; |
| audio_codec_loaded = true; |
| mutex_lock(&mutex_codecthread); |
| current_codec = CODEC_IDX_AUDIO; |
| status = codec_load_file((char *)ev.data, &ci); |
| mutex_unlock(&mutex_codecthread); |
| break ; |
| |
| case Q_CODEC_LOAD: |
| logf("Codec start"); |
| codecsize = cur_ti->codecsize; |
| if (codecsize == 0) { |
| logf("Codec slot is empty!"); |
| /* Wait for the pcm buffer to go empty */ |
| while (pcm_is_playing()) |
| yield(); |
| audio_stop_playback(true); |
| break ; |
| } |
| |
| ci.stop_codec = false; |
| wrap = (int)&filebuf[filebuflen] - (int)cur_ti->codecbuf; |
| audio_codec_loaded = true; |
| mutex_lock(&mutex_codecthread); |
| current_codec = CODEC_IDX_AUDIO; |
| status = codec_load_ram(cur_ti->codecbuf, codecsize, |
| &filebuf[0], wrap, &ci); |
| mutex_unlock(&mutex_codecthread); |
| break ; |
| |
| #ifndef SIMULATOR |
| case SYS_USB_CONNECTED: |
| while (voice_codec_loaded) { |
| if (current_codec != CODEC_IDX_VOICE) |
| swap_codec(); |
| sleep(1); |
| } |
| logf("USB: Audio codec"); |
| usb_acknowledge(SYS_USB_CONNECTED_ACK); |
| usb_wait_for_disconnect(&codec_queue); |
| break ; |
| #endif |
| } |
| |
| audio_codec_loaded = false; |
| |
| switch (ev.id) { |
| case Q_CODEC_LOAD_DISK: |
| case Q_CODEC_LOAD: |
| if (status != CODEC_OK) { |
| logf("Codec failure"); |
| // audio_stop_playback(); |
| ci.reload_codec = false; |
| gui_syncsplash(HZ*2, true, "Codec failure"); |
| } else { |
| logf("Codec finished"); |
| } |
| |
| if (playing && !ci.stop_codec) |
| audio_change_track(); |
| |
| // queue_post(&audio_queue, Q_AUDIO_CODEC_DONE, (void *)status); |
| } |
| } |
| } |
| |
| static void reset_buffer(void) |
| { |
| filebuf = (char *)&audiobuf[MALLOC_BUFSIZE]; |
| filebuflen = audiobufend - audiobuf - pcmbuf_get_bufsize() |
| - PCMBUF_GUARD - MALLOC_BUFSIZE - GUARD_BUFSIZE; |
| |
| if (talk_get_bufsize() && voice_codec_loaded) |
| { |
| filebuf = &filebuf[talk_get_bufsize()]; |
| filebuflen -= 2*CODEC_IRAM_SIZE + 2*CODEC_SIZE + talk_get_bufsize(); |
| } |
| } |
| |
| void voice_codec_thread(void) |
| { |
| struct event ev; |
| int status; |
| |
| current_codec = CODEC_IDX_AUDIO; |
| voice_codec_loaded = false; |
| while (1) { |
| status = 0; |
| voice_is_playing = false; |
| queue_wait(&voice_codec_queue, &ev); |
| switch (ev.id) { |
| case Q_CODEC_LOAD_DISK: |
| logf("Loading voice codec"); |
| audio_stop_playback(true); |
| mutex_lock(&mutex_codecthread); |
| current_codec = CODEC_IDX_VOICE; |
| dsp_configure(DSP_RESET, 0); |
| ci.configure(CODEC_DSP_ENABLE, (bool *)true); |
| voice_remaining = 0; |
| voice_getmore = NULL; |
| voice_codec_loaded = true; |
| reset_buffer(); |
| ci_voice.stop_codec = false; |
| |
| status = codec_load_file((char *)ev.data, &ci_voice); |
| |
| logf("Voice codec finished"); |
| audio_stop_playback(true); |
| mutex_unlock(&mutex_codecthread); |
| current_codec = CODEC_IDX_AUDIO; |
| voice_codec_loaded = false; |
| reset_buffer(); |
| break ; |
| |
| #ifndef SIMULATOR |
| case SYS_USB_CONNECTED: |
| logf("USB: Voice codec"); |
| usb_acknowledge(SYS_USB_CONNECTED_ACK); |
| usb_wait_for_disconnect(&voice_codec_queue); |
| break ; |
| #endif |
| } |
| } |
| } |
| |
| void voice_init(void) |
| { |
| if (!filebuf) |
| return; /* Audio buffers not yet set up */ |
| |
| while (voice_codec_loaded) |
| { |
| logf("Terminating voice codec"); |
| ci_voice.stop_codec = true; |
| if (current_codec != CODEC_IDX_VOICE) |
| swap_codec(); |
| sleep(1); |
| } |
| |
| if (!talk_get_bufsize()) |
| return ; |
| |
| logf("Starting voice codec"); |
| queue_post(&voice_codec_queue, Q_CODEC_LOAD_DISK, (void *)CODEC_MPA_L3); |
| while (!voice_codec_loaded) |
| sleep(1); |
| } |
| |
| struct mp3entry* audio_current_track(void) |
| { |
| const char *filename; |
| const char *p; |
| static struct mp3entry temp_id3; |
| |
| if (track_count > 0 && cur_ti->taginfo_ready) |
| return (struct mp3entry *)&cur_ti->id3; |
| |
| filename = playlist_peek(0); |
| if (!filename) |
| filename = "No file!"; |
| |
| // if (tagcache_fill_tags(&temp_id3, filename)) |
| // return &temp_id3; |
| |
| p = strrchr(filename, '/'); |
| if (!p) |
| p = filename; |
| else |
| p++; |
| |
| memset(&temp_id3, 0, sizeof(struct mp3entry)); |
| strncpy(temp_id3.path, p, sizeof(temp_id3.path)-1); |
| temp_id3.title = &temp_id3.path[0]; |
| |
| return &temp_id3; |
| } |
| |
| struct mp3entry* audio_next_track(void) |
| { |
| int next_idx = track_ridx + 1; |
| |
| if (track_count == 0) |
| return NULL; |
| |
| if (next_idx >= MAX_TRACK) |
| next_idx = 0; |
| |
| if (!tracks[next_idx].taginfo_ready) |
| return NULL; |
| |
| return &tracks[next_idx].id3; |
| } |
| |
| bool audio_has_changed_track(void) |
| { |
| if (track_changed) { |
| track_changed = false; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void audio_play(int offset) |
| { |
| logf("audio_play"); |
| if (pcmbuf_is_crossfade_enabled()) |
| { |
| ci.stop_codec = true; |
| sleep(1); |
| pcmbuf_crossfade_init(true); |
| } |
| else |
| { |
| stop_codec_flush(); |
| pcmbuf_play_stop(); |
| } |
| |
| queue_post(&audio_queue, Q_AUDIO_PLAY, (void *)offset); |
| } |
| |
| void audio_stop(void) |
| { |
| logf("audio_stop"); |
| queue_post(&audio_queue, Q_AUDIO_STOP, 0); |
| while (playing || audio_codec_loaded) |
| yield(); |
| } |
| |
| bool mp3_pause_done(void) |
| { |
| return paused; |
| } |
| |
| void audio_pause(void) |
| { |
| queue_post(&audio_queue, Q_AUDIO_PAUSE, 0); |
| } |
| |
| void audio_resume(void) |
| { |
| queue_post(&audio_queue, Q_AUDIO_RESUME, 0); |
| } |
| |
| void audio_next(void) |
| { |
| /* Prevent UI lag and update the WPS immediately. */ |
| if (global_settings.beep) |
| pcmbuf_beep(5000, 100, 2500*global_settings.beep); |
| |
| if (!playlist_check(1)) |
| return ; |
| playlist_next(1); |
| track_changed = true; |
| |
| /* Force WPS to update even if audio thread is blocked spinning. */ |
| if (mutex_bufferfill.locked) |
| cur_ti->taginfo_ready = false; |
| |
| queue_post(&audio_queue, Q_AUDIO_NEXT, 0); |
| } |
| |
| void audio_prev(void) |
| { |
| /* Prevent UI lag and update the WPS immediately. */ |
| if (global_settings.beep) |
| pcmbuf_beep(5000, 100, 2500*global_settings.beep); |
| |
| if (!playlist_check(-1)) |
| return ; |
| playlist_next(-1); |
| track_changed = true; |
| |
| /* Force WPS to update even if audio thread is blocked spinning. */ |
| if (mutex_bufferfill.locked) |
| cur_ti->taginfo_ready = false; |
| |
| queue_post(&audio_queue, Q_AUDIO_PREV, 0); |
| } |
| |
| void audio_next_dir(void) |
| { |
| queue_post(&audio_queue, Q_AUDIO_DIR_NEXT, 0); |
| } |
| |
| void audio_prev_dir(void) |
| { |
| queue_post(&audio_queue, Q_AUDIO_DIR_PREV, 0); |
| } |
| |
| void audio_ff_rewind(int newpos) |
| { |
| logf("rewind: %d", newpos); |
| queue_post(&audio_queue, Q_AUDIO_FF_REWIND, (int *)newpos); |
| } |
| |
| void audio_seamless_seek(int newpos) |
| { |
| logf("seamless_seek: %d", newpos); |
| queue_post(&audio_queue, Q_AUDIO_SEAMLESS_SEEK, (int *)newpos); |
| } |
| |
| void audio_flush_and_reload_tracks(void) |
| { |
| logf("flush & reload"); |
| queue_post(&audio_queue, Q_AUDIO_FLUSH, 0); |
| } |
| |
| void audio_error_clear(void) |
| { |
| } |
| |
| int audio_status(void) |
| { |
| int ret = 0; |
| |
| if (playing) |
| ret |= AUDIO_STATUS_PLAY; |
| |
| if (paused) |
| ret |= AUDIO_STATUS_PAUSE; |
| |
| return ret; |
| } |
| |
| int audio_get_file_pos(void) |
| { |
| return 0; |
| } |
| |
| |
| /* Copied from mpeg.c. Should be moved somewhere else. */ |
| static void mp3_set_elapsed(struct mp3entry* id3) |
| { |
| if ( id3->vbr ) { |
| if ( id3->has_toc ) { |
| /* calculate elapsed time using TOC */ |
| int i; |
| unsigned int remainder, plen, relpos, nextpos; |
| |
| /* find wich percent we're at */ |
| for (i=0; i<100; i++ ) |
| { |
| if ( id3->offset < id3->toc[i] * (id3->filesize / 256) ) |
| { |
| break; |
| } |
| } |
| |
| i--; |
| if (i < 0) |
| i = 0; |
| |
| relpos = id3->toc[i]; |
| |
| if (i < 99) |
| { |
| nextpos = id3->toc[i+1]; |
| } |
| else |
| { |
| nextpos = 256; |
| } |
| |
| remainder = id3->offset - (relpos * (id3->filesize / 256)); |
| |
| /* set time for this percent (divide before multiply to prevent |
| overflow on long files. loss of precision is negligible on |
| short files) */ |
| id3->elapsed = i * (id3->length / 100); |
| |
| /* calculate remainder time */ |
| plen = (nextpos - relpos) * (id3->filesize / 256); |
| id3->elapsed += (((remainder * 100) / plen) * |
| (id3->length / 10000)); |
| } |
| else { |
| /* no TOC exists. set a rough estimate using average bitrate */ |
| int tpk = id3->length / (id3->filesize / 1024); |
| id3->elapsed = id3->offset / 1024 * tpk; |
| } |
| } |
| else |
| /* constant bitrate, use exact calculation */ |
| id3->elapsed = id3->offset / (id3->bitrate / 8); |
| } |
| |
| /* Copied from mpeg.c. Should be moved somewhere else. */ |
| int mp3_get_file_pos(void) |
| { |
| int pos = -1; |
| struct mp3entry *id3 = audio_current_track(); |
| |
| if (id3->vbr) |
| { |
| if (id3->has_toc) |
| { |
| /* Use the TOC to find the new position */ |
| unsigned int percent, remainder; |
| int curtoc, nexttoc, plen; |
| |
| percent = (id3->elapsed*100)/id3->length; |
| if (percent > 99) |
| percent = 99; |
| |
| curtoc = id3->toc[percent]; |
| |
| if (percent < 99) |
| nexttoc = id3->toc[percent+1]; |
| else |
| nexttoc = 256; |
| |
| pos = (id3->filesize/256)*curtoc; |
| |
| /* Use the remainder to get a more accurate position */ |
| remainder = (id3->elapsed*100)%id3->length; |
| remainder = (remainder*100)/id3->length; |
| plen = (nexttoc - curtoc)*(id3->filesize/256); |
| pos += (plen/100)*remainder; |
| } |
| else |
| { |
| /* No TOC exists, estimate the new position */ |
| pos = (id3->filesize / (id3->length / 1000)) * |
| (id3->elapsed / 1000); |
| } |
| } |
| else if (id3->bitrate) |
| pos = id3->elapsed * (id3->bitrate / 8); |
| else |
| { |
| return -1; |
| } |
| |
| if (pos >= (int)(id3->filesize - id3->id3v1len)) |
| { |
| /* Don't seek right to the end of the file so that we can |
| transition properly to the next song */ |
| pos = id3->filesize - id3->id3v1len - 1; |
| } |
| else if (pos < (int)id3->first_frame_offset) |
| { |
| /* skip past id3v2 tag and other leading garbage */ |
| pos = id3->first_frame_offset; |
| } |
| return pos; |
| } |
| |
| void mp3_play_data(const unsigned char* start, int size, |
| void (*get_more)(unsigned char** start, int* size)) |
| { |
| voice_getmore = get_more; |
| voicebuf = (char *)start; |
| voice_remaining = size; |
| voice_is_playing = true; |
| pcmbuf_reset_mixpos(); |
| } |
| |
| void audio_set_buffer_margin(int setting) |
| { |
| static const int lookup[] = {5, 15, 30, 60, 120, 180, 300, 600}; |
| buffer_margin = lookup[setting]; |
| logf("buffer margin: %ds", buffer_margin); |
| set_filebuf_watermark(buffer_margin); |
| } |
| |
| /* Set crossfade & PCM buffer length. */ |
| void audio_set_crossfade(int enable) |
| { |
| long size; |
| bool was_playing = playing; |
| int offset = 0; |
| int seconds = 1; |
| |
| if (!filebuf) |
| return; /* Audio buffers not yet set up */ |
| |
| /* Store the track resume position */ |
| if (playing) |
| offset = cur_ti->id3.offset; |
| |
| if (enable) |
| { |
| seconds = global_settings.crossfade_fade_out_delay |
| + global_settings.crossfade_fade_out_duration; |
| } |
| |
| /* Buffer has to be at least 2s long. */ |
| seconds += 2; |
| logf("buf len: %d", seconds); |
| size = seconds * (NATIVE_FREQUENCY*4); |
| if (pcmbuf_get_bufsize() == size) |
| return ; |
| |
| /* Playback has to be stopped before changing the buffer size. */ |
| audio_stop_playback(true); |
| |
| /* Re-initialize audio system. */ |
| if (was_playing) |
| gui_syncsplash(0, true, (char *)str(LANG_RESTARTING_PLAYBACK)); |
| pcmbuf_init(size); |
| pcmbuf_crossfade_enable(enable); |
| reset_buffer(); |
| logf("abuf:%dB", pcmbuf_get_bufsize()); |
| logf("fbuf:%dB", filebuflen); |
| |
| voice_init(); |
| |
| /* Restart playback. */ |
| if (was_playing) { |
| audio_play(offset); |
| |
| /* Wait for the playback to start again (and display the splash |
| screen during that period. */ |
| playing = true; |
| while (playing && !audio_codec_loaded) |
| yield(); |
| } |
| } |
| |
| void mpeg_id3_options(bool _v1first) |
| { |
| v1first = _v1first; |
| } |
| |
| void test_buffer_event(struct mp3entry *id3, bool last_track) |
| { |
| (void)id3; |
| (void)last_track; |
| |
| logf("be:%d%s", last_track, id3->path); |
| } |
| |
| void test_unbuffer_event(struct mp3entry *id3, bool last_track) |
| { |
| (void)id3; |
| (void)last_track; |
| |
| logf("ube:%d%s", last_track, id3->path); |
| } |
| |
| static void playback_init(void) |
| { |
| static bool voicetagtrue = true; |
| struct event ev; |
| |
| logf("playback api init"); |
| pcm_init(); |
| |
| #if defined(HAVE_RECORDING) && !defined(SIMULATOR) |
| /* Set the input multiplexer to Line In */ |
| pcm_rec_mux(0); |
| #endif |
| |
| audio_set_track_buffer_event(test_buffer_event); |
| audio_set_track_unbuffer_event(test_unbuffer_event); |
| |
| /* Initialize codec api. */ |
| ci.read_filebuf = codec_filebuf_callback; |
| ci.pcmbuf_insert = pcmbuf_insert_buffer; |
| ci.pcmbuf_insert_split = codec_pcmbuf_insert_split_callback; |
| ci.get_codec_memory = get_codec_memory_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.request_next_track = codec_request_next_track_callback; |
| ci.mp3_get_filepos = codec_mp3_get_filepos_callback; |
| ci.seek_buffer = codec_seek_buffer_callback; |
| ci.seek_complete = codec_seek_complete_callback; |
| ci.set_elapsed = codec_set_elapsed_callback; |
| ci.set_offset = codec_set_offset_callback; |
| ci.configure = codec_configure_callback; |
| |
| memcpy(&ci_voice, &ci, sizeof(struct codec_api)); |
| memset(&id3_voice, 0, sizeof(struct mp3entry)); |
| ci_voice.taginfo_ready = &voicetagtrue; |
| ci_voice.id3 = &id3_voice; |
| ci_voice.pcmbuf_insert = codec_pcmbuf_insert_callback; |
| id3_voice.frequency = 11200; |
| id3_voice.length = 1000000L; |
| |
| create_thread(codec_thread, codec_stack, sizeof(codec_stack), |
| codec_thread_name); |
| create_thread(voice_codec_thread, voice_codec_stack, |
| sizeof(voice_codec_stack), voice_codec_thread_name); |
| |
| while (1) |
| { |
| queue_wait(&audio_queue, &ev); |
| if (ev.id == Q_AUDIO_POSTINIT) |
| break ; |
| } |
| |
| filebuf = (char *)&audiobuf[MALLOC_BUFSIZE]; |
| |
| /* Apply relevant settings */ |
| audio_set_buffer_margin(global_settings.buffer_margin); |
| audio_set_crossfade(global_settings.crossfade); |
| |
| sound_settings_apply(); |
| } |
| |
| void audio_preinit(void) |
| { |
| logf("playback system pre-init"); |
| |
| filebufused = 0; |
| filling = false; |
| current_codec = CODEC_IDX_AUDIO; |
| playing = false; |
| audio_codec_loaded = false; |
| voice_is_playing = false; |
| paused = false; |
| track_changed = false; |
| current_fd = -1; |
| track_buffer_callback = NULL; |
| track_unbuffer_callback = NULL; |
| track_changed_callback = NULL; |
| /* Just to prevent cur_ti never be anything random. */ |
| cur_ti = &tracks[0]; |
| |
| mutex_init(&mutex_bufferfill); |
| mutex_init(&mutex_codecthread); |
| |
| queue_init(&audio_queue); |
| queue_init(&codec_queue); |
| queue_init(&voice_codec_queue); |
| |
| create_thread(audio_thread, audio_stack, sizeof(audio_stack), |
| audio_thread_name); |
| } |
| |
| void audio_init(void) |
| { |
| logf("playback system post-init"); |
| |
| queue_post(&audio_queue, Q_AUDIO_POSTINIT, 0); |
| } |
| |