6510973223
git-svn-id: svn://svn.rockbox.org/rockbox/trunk@29387 a1c6a512-1295-4272-9138-f99709370657
2394 lines
66 KiB
C
2394 lines
66 KiB
C
/***************************************************************************
|
|
* __________ __ ___.
|
|
* 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.
|
|
*
|
|
****************************************************************************/
|
|
|
|
/* TODO: Pause should be handled in here, rather than PCMBUF so that voice can
|
|
* play whilst audio is paused */
|
|
#include "config.h"
|
|
#include "system.h"
|
|
#include <string.h>
|
|
#include "playback.h"
|
|
#include "codec_thread.h"
|
|
#include "kernel.h"
|
|
#include "codecs.h"
|
|
#include "buffering.h"
|
|
#include "voice_thread.h"
|
|
#include "usb.h"
|
|
#include "ata.h"
|
|
#include "playlist.h"
|
|
#include "pcmbuf.h"
|
|
#include "buffer.h"
|
|
#include "cuesheet.h"
|
|
#ifdef HAVE_TAGCACHE
|
|
#include "tagcache.h"
|
|
#endif
|
|
#ifdef HAVE_LCD_BITMAP
|
|
#ifdef HAVE_ALBUMART
|
|
#include "albumart.h"
|
|
#endif
|
|
#endif
|
|
#include "sound.h"
|
|
#include "metadata.h"
|
|
#include "splash.h"
|
|
#include "talk.h"
|
|
#include "panic.h"
|
|
|
|
#ifdef HAVE_RECORDING
|
|
#include "pcm_record.h"
|
|
#endif
|
|
|
|
#define PLAYBACK_VOICE
|
|
|
|
/* amount of guess-space to allow for codecs that must hunt and peck
|
|
* for their correct seeek target, 32k seems a good size */
|
|
#define AUDIO_REBUFFER_GUESS_SIZE (1024*32)
|
|
|
|
/* 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
|
|
|
|
|
|
static enum filling_state {
|
|
STATE_IDLE, /* audio is stopped: nothing to do */
|
|
STATE_FILLING, /* adding tracks to the buffer */
|
|
STATE_FULL, /* can't add any more tracks */
|
|
STATE_END_OF_PLAYLIST, /* all remaining tracks have been added */
|
|
STATE_FINISHED, /* all remaining tracks are fully buffered */
|
|
STATE_ENDING, /* audio playback is ending */
|
|
#if (CONFIG_PLATFORM & PLATFORM_NATIVE)
|
|
STATE_USB, /* USB mode, ignore most messages */
|
|
#endif
|
|
} filling;
|
|
|
|
/* As defined in plugins/lib/xxx2wav.h */
|
|
#define GUARD_BUFSIZE (32*1024)
|
|
|
|
bool audio_is_initialized = false;
|
|
static bool audio_thread_ready SHAREDBSS_ATTR = false;
|
|
|
|
/* 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. */
|
|
/* TBD: Split out "audio" and "playback" (ie. calling) threads */
|
|
|
|
/* Main state control */
|
|
static volatile bool playing SHAREDBSS_ATTR = false;/* Is audio playing? (A) */
|
|
static volatile bool paused SHAREDBSS_ATTR = false; /* Is audio paused? (A/C-) */
|
|
|
|
/* Ring buffer where compressed audio and codecs are loaded */
|
|
static unsigned char *filebuf = NULL; /* Start of buffer (A/C-) */
|
|
static unsigned char *malloc_buf = NULL; /* Start of malloc buffer (A/C-) */
|
|
static size_t filebuflen = 0; /* Size of buffer (A/C-) */
|
|
/* FIXME: make buf_ridx (C/A-) */
|
|
|
|
/* Possible arrangements of the buffer */
|
|
enum audio_buffer_state
|
|
{
|
|
AUDIOBUF_STATE_TRASHED = -1, /* trashed; must be reset */
|
|
AUDIOBUF_STATE_INITIALIZED = 0, /* voice+audio OR audio-only */
|
|
AUDIOBUF_STATE_VOICED_ONLY = 1, /* voice-only */
|
|
};
|
|
static int buffer_state = AUDIOBUF_STATE_TRASHED; /* Buffer state */
|
|
|
|
/* These are used to store the current and next (or prev if the current is the last)
|
|
* mp3entry's in a round-robin system. This guarentees that the pointer returned
|
|
* by audio_current/next_track will be valid for the full duration of the
|
|
* currently playing track */
|
|
static struct mp3entry mp3entry_buf[2];
|
|
struct mp3entry *thistrack_id3, /* the currently playing track */
|
|
*othertrack_id3; /* prev track during track-change-transition, or end of playlist,
|
|
* next track otherwise */
|
|
static struct mp3entry unbuffered_id3; /* the id3 for the first unbuffered track */
|
|
|
|
/* for cuesheet support */
|
|
static struct cuesheet *curr_cue = NULL;
|
|
|
|
|
|
#define MAX_MULTIPLE_AA SKINNABLE_SCREENS_COUNT
|
|
|
|
#ifdef HAVE_ALBUMART
|
|
|
|
static struct albumart_slot {
|
|
struct dim dim; /* holds width, height of the albumart */
|
|
int used; /* counter, increments if something uses it */
|
|
} albumart_slots[MAX_MULTIPLE_AA];
|
|
|
|
#define FOREACH_ALBUMART(i) for(i = 0;i < MAX_MULTIPLE_AA; i++)
|
|
#endif
|
|
|
|
|
|
#define MAX_TRACK 128
|
|
#define MAX_TRACK_MASK (MAX_TRACK-1)
|
|
|
|
/* Track info structure about songs in the file buffer (A/C-) */
|
|
static struct track_info {
|
|
int audio_hid; /* The ID for the track's buffer handle */
|
|
int id3_hid; /* The ID for the track's metadata handle */
|
|
int codec_hid; /* The ID for the track's codec handle */
|
|
#ifdef HAVE_ALBUMART
|
|
int aa_hid[MAX_MULTIPLE_AA];/* The ID for the track's album art handle */
|
|
#endif
|
|
int cuesheet_hid; /* The ID for the track's parsed cueesheet handle */
|
|
|
|
size_t filesize; /* File total length */
|
|
|
|
bool taginfo_ready; /* Is metadata read */
|
|
|
|
} tracks[MAX_TRACK];
|
|
|
|
static volatile int track_ridx = 0; /* Track being decoded (A/C-) */
|
|
static int track_widx = 0; /* Track being buffered (A) */
|
|
#define CUR_TI (&tracks[track_ridx]) /* Playing track info pointer (A/C-) */
|
|
|
|
static struct track_info *prev_ti = NULL; /* Pointer to the previously played
|
|
track */
|
|
|
|
/* Information used only for filling the buffer */
|
|
/* Playlist steps from playing track to next track to be buffered (A) */
|
|
static int last_peek_offset = 0;
|
|
|
|
/* Scrobbler support */
|
|
static unsigned long prev_track_elapsed = 0; /* Previous track elapsed time (C/A-)*/
|
|
|
|
/* Track change controls */
|
|
static bool automatic_skip = false; /* Who initiated in-progress skip? (A) */
|
|
extern bool track_transition; /* Are we in a track transition? */
|
|
static bool dir_skip = false; /* Is a directory skip pending? (A) */
|
|
static bool new_playlist = false; /* Are we starting a new playlist? (A) */
|
|
static int wps_offset = 0; /* Pending track change offset, to keep WPS responsive (A) */
|
|
static bool skipped_during_pause = false; /* Do we need to clear the PCM buffer when playback resumes (A) */
|
|
|
|
static bool start_play_g = false; /* Used by audio_load_track to notify
|
|
audio_finish_load_track about start_play */
|
|
|
|
/* True when a track load is in progress, i.e. audio_load_track() has returned
|
|
* but audio_finish_load_track() hasn't been called yet. Used to avoid allowing
|
|
* audio_load_track() to get called twice in a row, which would cause problems.
|
|
*/
|
|
static bool track_load_started = false;
|
|
|
|
#ifdef HAVE_DISK_STORAGE
|
|
static size_t buffer_margin = 5; /* Buffer margin aka anti-skip buffer (A/C-) */
|
|
#endif
|
|
|
|
/* Event queues */
|
|
struct event_queue audio_queue SHAREDBSS_ATTR;
|
|
static struct event_queue pcmbuf_queue SHAREDBSS_ATTR;
|
|
|
|
extern struct codec_api ci;
|
|
extern unsigned int codec_thread_id;
|
|
|
|
/* Multiple threads */
|
|
/* Set the watermark to trigger buffer fill (A/C) */
|
|
static void set_filebuf_watermark(void);
|
|
|
|
/* Audio thread */
|
|
static struct queue_sender_list audio_queue_sender_list SHAREDBSS_ATTR;
|
|
static long audio_stack[(DEFAULT_STACK_SIZE + 0x1000)/sizeof(long)];
|
|
static const char audio_thread_name[] = "audio";
|
|
|
|
static void audio_thread(void);
|
|
static void audio_initiate_track_change(long direction);
|
|
static bool audio_have_tracks(void);
|
|
static void audio_reset_buffer(void);
|
|
static void audio_stop_playback(void);
|
|
|
|
/**************************************/
|
|
|
|
|
|
/** Pcmbuf callbacks */
|
|
|
|
/* Between the codec and PCM track change, we need to keep updating the
|
|
* "elapsed" value of the previous (to the codec, but current to the
|
|
* user/PCM/WPS) track, so that the progressbar reaches the end.
|
|
* During that transition, the WPS will display othertrack_id3. */
|
|
void audio_pcmbuf_position_callback(unsigned int time)
|
|
{
|
|
time += othertrack_id3->elapsed;
|
|
othertrack_id3->elapsed = (time >= othertrack_id3->length)
|
|
? othertrack_id3->length : time;
|
|
}
|
|
|
|
/* Post message from pcmbuf that the end of the previous track
|
|
* has just been played. */
|
|
void audio_post_track_change(bool pcmbuf)
|
|
{
|
|
if (pcmbuf)
|
|
{
|
|
LOGFQUEUE("pcmbuf > pcmbuf Q_AUDIO_TRACK_CHANGED");
|
|
queue_post(&pcmbuf_queue, Q_AUDIO_TRACK_CHANGED, 0);
|
|
}
|
|
else
|
|
{
|
|
LOGFQUEUE("pcmbuf > audio Q_AUDIO_TRACK_CHANGED");
|
|
queue_post(&audio_queue, Q_AUDIO_TRACK_CHANGED, 0);
|
|
}
|
|
}
|
|
|
|
/* Scan the pcmbuf queue and return true if a message pulled */
|
|
static bool pcmbuf_queue_scan(struct queue_event *ev)
|
|
{
|
|
if (!queue_empty(&pcmbuf_queue))
|
|
{
|
|
/* Transfer message to audio queue */
|
|
pcm_play_lock();
|
|
/* Pull message - never, ever any blocking call! */
|
|
queue_wait_w_tmo(&pcmbuf_queue, ev, 0);
|
|
pcm_play_unlock();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/** Helper functions */
|
|
|
|
static struct mp3entry *bufgetid3(int handle_id)
|
|
{
|
|
if (handle_id < 0)
|
|
return NULL;
|
|
|
|
struct mp3entry *id3;
|
|
ssize_t ret = bufgetdata(handle_id, 0, (void *)&id3);
|
|
|
|
if (ret != sizeof(struct mp3entry))
|
|
return NULL;
|
|
|
|
return id3;
|
|
}
|
|
|
|
static bool bufreadid3(int handle_id, struct mp3entry *id3out)
|
|
{
|
|
struct mp3entry *id3 = bufgetid3(handle_id);
|
|
|
|
if (id3)
|
|
{
|
|
copy_mp3entry(id3out, id3);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool clear_track_info(struct track_info *track)
|
|
{
|
|
/* bufclose returns true if the handle is not found, or if it is closed
|
|
* successfully, so these checks are safe on non-existant handles */
|
|
if (!track)
|
|
return false;
|
|
|
|
if (track->codec_hid >= 0) {
|
|
if (bufclose(track->codec_hid))
|
|
track->codec_hid = -1;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
if (track->id3_hid >= 0) {
|
|
if (bufclose(track->id3_hid))
|
|
track->id3_hid = -1;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
if (track->audio_hid >= 0) {
|
|
if (bufclose(track->audio_hid))
|
|
track->audio_hid = -1;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
#ifdef HAVE_ALBUMART
|
|
{
|
|
int i;
|
|
FOREACH_ALBUMART(i)
|
|
{
|
|
if (track->aa_hid[i] >= 0) {
|
|
if (bufclose(track->aa_hid[i]))
|
|
track->aa_hid[i] = -1;
|
|
else
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (track->cuesheet_hid >= 0) {
|
|
if (bufclose(track->cuesheet_hid))
|
|
track->cuesheet_hid = -1;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
track->filesize = 0;
|
|
track->taginfo_ready = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/* --- External interfaces --- */
|
|
|
|
/* This sends a stop message and the audio thread will dump all it's
|
|
subsequenct messages */
|
|
void audio_hard_stop(void)
|
|
{
|
|
/* Stop playback */
|
|
LOGFQUEUE("audio >| audio Q_AUDIO_STOP: 1");
|
|
queue_send(&audio_queue, Q_AUDIO_STOP, 1);
|
|
#ifdef PLAYBACK_VOICE
|
|
voice_stop();
|
|
#endif
|
|
}
|
|
|
|
bool audio_restore_playback(int type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case AUDIO_WANT_PLAYBACK:
|
|
if (buffer_state != AUDIOBUF_STATE_INITIALIZED)
|
|
audio_reset_buffer();
|
|
return true;
|
|
case AUDIO_WANT_VOICE:
|
|
if (buffer_state == AUDIOBUF_STATE_TRASHED)
|
|
audio_reset_buffer();
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
unsigned char *audio_get_buffer(bool talk_buf, size_t *buffer_size)
|
|
{
|
|
unsigned char *buf, *end;
|
|
|
|
if (audio_is_initialized)
|
|
{
|
|
audio_hard_stop();
|
|
}
|
|
/* else buffer_state will be AUDIOBUF_STATE_TRASHED at this point */
|
|
|
|
/* Reset the buffering thread so that it doesn't try to use the data */
|
|
buffering_reset(filebuf, filebuflen);
|
|
|
|
if (buffer_size == NULL)
|
|
{
|
|
/* Special case for talk_init to use since it already knows it's
|
|
trashed */
|
|
buffer_state = AUDIOBUF_STATE_TRASHED;
|
|
return NULL;
|
|
}
|
|
|
|
if (talk_buf || buffer_state == AUDIOBUF_STATE_TRASHED
|
|
|| !talk_voice_required())
|
|
{
|
|
logf("get buffer: talk, audio");
|
|
/* Ok to use everything from audiobuf to audiobufend - voice is loaded,
|
|
the talk buffer is not needed because voice isn't being used, or
|
|
could be AUDIOBUF_STATE_TRASHED already. If state is
|
|
AUDIOBUF_STATE_VOICED_ONLY, no problem as long as memory isn't written
|
|
without the caller knowing what's going on. Changing certain settings
|
|
may move it to a worse condition but the memory in use by something
|
|
else will remain undisturbed.
|
|
*/
|
|
if (buffer_state != AUDIOBUF_STATE_TRASHED)
|
|
{
|
|
talk_buffer_steal();
|
|
buffer_state = AUDIOBUF_STATE_TRASHED;
|
|
}
|
|
|
|
buf = audiobuf;
|
|
end = audiobufend;
|
|
}
|
|
else
|
|
{
|
|
/* Safe to just return this if already AUDIOBUF_STATE_VOICED_ONLY or
|
|
still AUDIOBUF_STATE_INITIALIZED */
|
|
/* Skip talk buffer and move pcm buffer to end to maximize available
|
|
contiguous memory - no audio running means voice will not need the
|
|
swap space */
|
|
logf("get buffer: audio");
|
|
buf = audiobuf + talk_get_bufsize();
|
|
end = audiobufend - pcmbuf_init(audiobufend);
|
|
buffer_state = AUDIOBUF_STATE_VOICED_ONLY;
|
|
}
|
|
|
|
*buffer_size = end - buf;
|
|
|
|
return buf;
|
|
}
|
|
|
|
bool audio_buffer_state_trashed(void)
|
|
{
|
|
return buffer_state == AUDIOBUF_STATE_TRASHED;
|
|
}
|
|
|
|
#ifdef HAVE_RECORDING
|
|
unsigned char *audio_get_recording_buffer(size_t *buffer_size)
|
|
{
|
|
/* Stop audio, voice and obtain all available buffer space */
|
|
audio_hard_stop();
|
|
talk_buffer_steal();
|
|
|
|
unsigned char *end = audiobufend;
|
|
buffer_state = AUDIOBUF_STATE_TRASHED;
|
|
*buffer_size = end - audiobuf;
|
|
|
|
return (unsigned char *)audiobuf;
|
|
}
|
|
|
|
bool audio_load_encoder(int afmt)
|
|
{
|
|
#if (CONFIG_PLATFORM & PLATFORM_NATIVE)
|
|
LOGFQUEUE("audio >| Q_AUDIO_LOAD_ENCODER: %d", afmt);
|
|
return queue_send(&audio_queue, Q_AUDIO_LOAD_ENCODER, afmt) > 0;
|
|
#else
|
|
(void)afmt;
|
|
return true;
|
|
#endif
|
|
} /* audio_load_encoder */
|
|
|
|
void audio_remove_encoder(void)
|
|
{
|
|
#if (CONFIG_PLATFORM & PLATFORM_NATIVE)
|
|
LOGFQUEUE("audio >| Q_AUDIO_LOAD_ENCODER: NULL");
|
|
queue_send(&audio_queue, Q_AUDIO_LOAD_ENCODER, AFMT_UNKNOWN);
|
|
#endif
|
|
} /* audio_remove_encoder */
|
|
|
|
#endif /* HAVE_RECORDING */
|
|
|
|
|
|
struct mp3entry* audio_current_track(void)
|
|
{
|
|
const char *filename;
|
|
struct playlist_track_info trackinfo;
|
|
int cur_idx;
|
|
int offset = ci.new_track + wps_offset;
|
|
struct mp3entry *write_id3;
|
|
|
|
cur_idx = (track_ridx + offset) & MAX_TRACK_MASK;
|
|
|
|
if (cur_idx == track_ridx && *thistrack_id3->path)
|
|
{
|
|
/* The usual case */
|
|
if (tracks[cur_idx].cuesheet_hid >= 0 && !thistrack_id3->cuesheet)
|
|
{
|
|
bufread(tracks[cur_idx].cuesheet_hid, sizeof(struct cuesheet), curr_cue);
|
|
thistrack_id3->cuesheet = curr_cue;
|
|
}
|
|
return thistrack_id3;
|
|
}
|
|
else if (automatic_skip && offset == -1 && *othertrack_id3->path)
|
|
{
|
|
/* We're in a track transition. The codec has moved on to the next track,
|
|
but the audio being played is still the same (now previous) track.
|
|
othertrack_id3.elapsed is being updated in an ISR by
|
|
codec_pcmbuf_position_callback */
|
|
if (tracks[cur_idx].cuesheet_hid >= 0 && !thistrack_id3->cuesheet)
|
|
{
|
|
bufread(tracks[cur_idx].cuesheet_hid, sizeof(struct cuesheet), curr_cue);
|
|
othertrack_id3->cuesheet = curr_cue;
|
|
}
|
|
return othertrack_id3;
|
|
}
|
|
|
|
if (offset != 0)
|
|
{
|
|
/* Codec may be using thistrack_id3, so it must not be overwritten.
|
|
If this is a manual skip, othertrack_id3 will become
|
|
thistrack_id3 in audio_check_new_track().
|
|
FIXME: If this is an automatic skip, it probably means multiple
|
|
short tracks fit in the PCM buffer. Overwriting othertrack_id3
|
|
can lead to an incorrect value later.
|
|
Note that othertrack_id3 may also be used for next track.
|
|
*/
|
|
write_id3 = othertrack_id3;
|
|
}
|
|
else
|
|
{
|
|
write_id3 = thistrack_id3;
|
|
}
|
|
|
|
if (tracks[cur_idx].id3_hid >= 0)
|
|
{
|
|
/* The current track's info has been buffered but not read yet, so get it */
|
|
if (bufreadid3(tracks[cur_idx].id3_hid, write_id3))
|
|
return write_id3;
|
|
}
|
|
|
|
/* We didn't find the ID3 metadata, so we fill temp_id3 with the little info
|
|
we have and return that. */
|
|
|
|
memset(write_id3, 0, sizeof(struct mp3entry));
|
|
|
|
playlist_get_track_info(NULL, playlist_next(0)+wps_offset, &trackinfo);
|
|
filename = trackinfo.filename;
|
|
if (!filename)
|
|
filename = "No file!";
|
|
|
|
#if defined(HAVE_TC_RAMCACHE) && defined(HAVE_DIRCACHE)
|
|
if (tagcache_fill_tags(write_id3, filename))
|
|
return write_id3;
|
|
#endif
|
|
|
|
strlcpy(write_id3->path, filename, sizeof(write_id3->path));
|
|
write_id3->title = strrchr(write_id3->path, '/');
|
|
if (!write_id3->title)
|
|
write_id3->title = &write_id3->path[0];
|
|
else
|
|
write_id3->title++;
|
|
|
|
return write_id3;
|
|
}
|
|
|
|
struct mp3entry* audio_next_track(void)
|
|
{
|
|
int next_idx;
|
|
int offset = ci.new_track + wps_offset;
|
|
|
|
if (!audio_have_tracks())
|
|
return NULL;
|
|
|
|
if (wps_offset == -1 && *thistrack_id3->path)
|
|
{
|
|
/* We're in a track transition. The next track for the WPS is the one
|
|
currently being decoded. */
|
|
return thistrack_id3;
|
|
}
|
|
|
|
next_idx = (track_ridx + offset + 1) & MAX_TRACK_MASK;
|
|
|
|
if (tracks[next_idx].id3_hid >= 0)
|
|
{
|
|
if (bufreadid3(tracks[next_idx].id3_hid, othertrack_id3))
|
|
return othertrack_id3;
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
if (next_idx == track_widx)
|
|
{
|
|
/* The next track hasn't been buffered yet, so we return the static
|
|
version of its metadata. */
|
|
return &unbuffered_id3;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* gets a copy of the id3 data */
|
|
bool audio_peek_track(struct mp3entry* id3, int offset)
|
|
{
|
|
int next_idx;
|
|
int new_offset = ci.new_track + wps_offset + offset;
|
|
|
|
if (!audio_have_tracks())
|
|
return false;
|
|
next_idx = (track_ridx + new_offset) & MAX_TRACK_MASK;
|
|
|
|
if (tracks[next_idx].id3_hid >= 0)
|
|
return bufreadid3(tracks[next_idx].id3_hid, id3);
|
|
|
|
return false;
|
|
}
|
|
|
|
#ifdef HAVE_ALBUMART
|
|
|
|
int playback_current_aa_hid(int slot)
|
|
{
|
|
if (slot < 0)
|
|
return -1;
|
|
int cur_idx;
|
|
int offset = ci.new_track + wps_offset;
|
|
|
|
cur_idx = track_ridx + offset;
|
|
cur_idx &= MAX_TRACK_MASK;
|
|
return tracks[cur_idx].aa_hid[slot];
|
|
}
|
|
|
|
int playback_claim_aa_slot(struct dim *dim)
|
|
{
|
|
int i;
|
|
|
|
/* first try to find a slot already having the size to reuse it
|
|
* since we don't want albumart of the same size buffered multiple times */
|
|
FOREACH_ALBUMART(i)
|
|
{
|
|
struct albumart_slot *slot = &albumart_slots[i];
|
|
if (slot->dim.width == dim->width
|
|
&& slot->dim.height == dim->height)
|
|
{
|
|
slot->used++;
|
|
return i;
|
|
}
|
|
}
|
|
/* size is new, find a free slot */
|
|
FOREACH_ALBUMART(i)
|
|
{
|
|
if (!albumart_slots[i].used)
|
|
{
|
|
albumart_slots[i].used++;
|
|
albumart_slots[i].dim = *dim;
|
|
return i;
|
|
}
|
|
}
|
|
/* sorry, no free slot */
|
|
return -1;
|
|
}
|
|
|
|
void playback_release_aa_slot(int slot)
|
|
{
|
|
/* invalidate the albumart_slot */
|
|
struct albumart_slot *aa_slot = &albumart_slots[slot];
|
|
|
|
if (aa_slot->used > 0)
|
|
aa_slot->used--;
|
|
}
|
|
|
|
#endif
|
|
void audio_play(long offset)
|
|
{
|
|
logf("audio_play");
|
|
|
|
#ifdef PLAYBACK_VOICE
|
|
/* Truncate any existing voice output so we don't have spelling
|
|
* etc. over the first part of the played track */
|
|
talk_force_shutup();
|
|
#endif
|
|
|
|
/* Start playback */
|
|
LOGFQUEUE("audio >| audio Q_AUDIO_PLAY: %ld", offset);
|
|
/* Don't return until playback has actually started */
|
|
queue_send(&audio_queue, Q_AUDIO_PLAY, offset);
|
|
}
|
|
|
|
void audio_stop(void)
|
|
{
|
|
/* Stop playback */
|
|
LOGFQUEUE("audio >| audio Q_AUDIO_STOP");
|
|
/* Don't return until playback has actually stopped */
|
|
queue_send(&audio_queue, Q_AUDIO_STOP, 0);
|
|
}
|
|
|
|
void audio_pause(void)
|
|
{
|
|
LOGFQUEUE("audio >| audio Q_AUDIO_PAUSE");
|
|
/* Don't return until playback has actually paused */
|
|
queue_send(&audio_queue, Q_AUDIO_PAUSE, true);
|
|
}
|
|
|
|
void audio_resume(void)
|
|
{
|
|
LOGFQUEUE("audio >| audio Q_AUDIO_PAUSE resume");
|
|
/* Don't return until playback has actually resumed */
|
|
queue_send(&audio_queue, Q_AUDIO_PAUSE, false);
|
|
}
|
|
|
|
void audio_skip(int direction)
|
|
{
|
|
if (playlist_check(ci.new_track + wps_offset + direction))
|
|
{
|
|
if (global_settings.beep)
|
|
pcmbuf_beep(2000, 100, 2500*global_settings.beep);
|
|
|
|
LOGFQUEUE("audio > audio Q_AUDIO_SKIP %d", direction);
|
|
queue_post(&audio_queue, Q_AUDIO_SKIP, direction);
|
|
/* Update wps while our message travels inside deep playback queues. */
|
|
wps_offset += direction;
|
|
}
|
|
else
|
|
{
|
|
/* No more tracks. */
|
|
if (global_settings.beep)
|
|
pcmbuf_beep(1000, 100, 1500*global_settings.beep);
|
|
}
|
|
}
|
|
|
|
void audio_next(void)
|
|
{
|
|
audio_skip(1);
|
|
}
|
|
|
|
void audio_prev(void)
|
|
{
|
|
audio_skip(-1);
|
|
}
|
|
|
|
void audio_next_dir(void)
|
|
{
|
|
LOGFQUEUE("audio > audio Q_AUDIO_DIR_SKIP 1");
|
|
queue_post(&audio_queue, Q_AUDIO_DIR_SKIP, 1);
|
|
}
|
|
|
|
void audio_prev_dir(void)
|
|
{
|
|
LOGFQUEUE("audio > audio Q_AUDIO_DIR_SKIP -1");
|
|
queue_post(&audio_queue, Q_AUDIO_DIR_SKIP, -1);
|
|
}
|
|
|
|
void audio_pre_ff_rewind(void)
|
|
{
|
|
LOGFQUEUE("audio > audio Q_AUDIO_PRE_FF_REWIND");
|
|
queue_post(&audio_queue, Q_AUDIO_PRE_FF_REWIND, 0);
|
|
}
|
|
|
|
void audio_ff_rewind(long newpos)
|
|
{
|
|
LOGFQUEUE("audio > audio Q_AUDIO_FF_REWIND");
|
|
queue_post(&audio_queue, Q_AUDIO_FF_REWIND, newpos);
|
|
}
|
|
|
|
void audio_flush_and_reload_tracks(void)
|
|
{
|
|
LOGFQUEUE("audio > audio Q_AUDIO_FLUSH");
|
|
queue_post(&audio_queue, Q_AUDIO_FLUSH, 0);
|
|
}
|
|
|
|
void audio_error_clear(void)
|
|
{
|
|
#ifdef AUDIO_HAVE_RECORDING
|
|
pcm_rec_error_clear();
|
|
#endif
|
|
}
|
|
|
|
int audio_status(void)
|
|
{
|
|
int ret = 0;
|
|
|
|
if (playing)
|
|
ret |= AUDIO_STATUS_PLAY;
|
|
|
|
if (paused)
|
|
ret |= AUDIO_STATUS_PAUSE;
|
|
|
|
#ifdef HAVE_RECORDING
|
|
/* Do this here for constitency with mpeg.c version */
|
|
/* FIXME: pcm_rec_status() is deprecated */
|
|
ret |= pcm_rec_status();
|
|
#endif
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool audio_automatic_skip(void)
|
|
{
|
|
return automatic_skip;
|
|
}
|
|
|
|
int audio_get_file_pos(void)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
#ifdef HAVE_DISK_STORAGE
|
|
void audio_set_buffer_margin(int setting)
|
|
{
|
|
static const unsigned short lookup[] = {5, 15, 30, 60, 120, 180, 300, 600};
|
|
buffer_margin = lookup[setting];
|
|
logf("buffer margin: %ld", (long)buffer_margin);
|
|
set_filebuf_watermark();
|
|
}
|
|
#endif
|
|
|
|
#ifdef HAVE_CROSSFADE
|
|
/* Take necessary steps to enable or disable the crossfade setting */
|
|
void audio_set_crossfade(int enable)
|
|
{
|
|
size_t offset;
|
|
bool was_playing;
|
|
size_t size;
|
|
|
|
/* Tell it the next setting to use */
|
|
pcmbuf_request_crossfade_enable(enable);
|
|
|
|
/* Return if size hasn't changed or this is too early to determine
|
|
which in the second case there's no way we could be playing
|
|
anything at all */
|
|
if (pcmbuf_is_same_size()) return;
|
|
|
|
offset = 0;
|
|
was_playing = playing;
|
|
|
|
/* Playback has to be stopped before changing the buffer size */
|
|
if (was_playing)
|
|
{
|
|
/* Store the track resume position */
|
|
offset = thistrack_id3->offset;
|
|
}
|
|
|
|
/* Blast it - audio buffer will have to be setup again next time
|
|
something plays */
|
|
audio_get_buffer(true, &size);
|
|
|
|
/* Restart playback if audio was running previously */
|
|
if (was_playing)
|
|
audio_play(offset);
|
|
}
|
|
#endif
|
|
|
|
/* --- Routines called from multiple threads --- */
|
|
|
|
static void set_filebuf_watermark(void)
|
|
{
|
|
if (!filebuf)
|
|
return; /* Audio buffers not yet set up */
|
|
|
|
#ifdef HAVE_DISK_STORAGE
|
|
int seconds;
|
|
int spinup = ata_spinup_time();
|
|
if (spinup)
|
|
seconds = (spinup / HZ) + 1;
|
|
else
|
|
seconds = 5;
|
|
|
|
seconds += buffer_margin;
|
|
#else
|
|
/* flash storage */
|
|
int seconds = 1;
|
|
#endif
|
|
|
|
/* bitrate of last track in buffer dictates watermark */
|
|
struct mp3entry* id3 = NULL;
|
|
if (tracks[track_widx].taginfo_ready)
|
|
id3 = bufgetid3(tracks[track_widx].id3_hid);
|
|
else
|
|
id3 = bufgetid3(tracks[track_widx-1].id3_hid);
|
|
if (!id3) {
|
|
logf("fwmark: No id3 for last track (r%d/w%d), aborting!", track_ridx, track_widx);
|
|
return;
|
|
}
|
|
size_t bytes = id3->bitrate * (1000/8) * seconds;
|
|
buf_set_watermark(bytes);
|
|
logf("fwmark: %d", bytes);
|
|
}
|
|
|
|
/* --- Buffering callbacks --- */
|
|
|
|
static void buffering_low_buffer_callback(void *data)
|
|
{
|
|
(void)data;
|
|
logf("low buffer callback");
|
|
|
|
if (filling == STATE_FULL || filling == STATE_END_OF_PLAYLIST) {
|
|
/* force a refill */
|
|
LOGFQUEUE("buffering > audio Q_AUDIO_FILL_BUFFER");
|
|
queue_post(&audio_queue, Q_AUDIO_FILL_BUFFER, 0);
|
|
}
|
|
}
|
|
|
|
static void buffering_handle_rebuffer_callback(void *data)
|
|
{
|
|
(void)data;
|
|
LOGFQUEUE("audio >| audio Q_AUDIO_FLUSH");
|
|
queue_post(&audio_queue, Q_AUDIO_FLUSH, 0);
|
|
}
|
|
|
|
static void buffering_handle_finished_callback(void *data)
|
|
{
|
|
logf("handle %d finished buffering", *(int*)data);
|
|
int hid = (*(int*)data);
|
|
|
|
if (hid == tracks[track_widx].id3_hid)
|
|
{
|
|
int offset = ci.new_track + wps_offset;
|
|
int next_idx = (track_ridx + offset + 1) & MAX_TRACK_MASK;
|
|
/* The metadata handle for the last loaded track has been buffered.
|
|
We can ask the audio thread to load the rest of the track's data. */
|
|
LOGFQUEUE("audio > audio Q_AUDIO_FINISH_LOAD");
|
|
queue_post(&audio_queue, Q_AUDIO_FINISH_LOAD, 0);
|
|
if (tracks[next_idx].id3_hid == hid)
|
|
send_event(PLAYBACK_EVENT_NEXTTRACKID3_AVAILABLE, NULL);
|
|
}
|
|
else
|
|
{
|
|
/* This is most likely an audio handle, so we strip the useless
|
|
trailing tags that are left. */
|
|
strip_tags(hid);
|
|
|
|
if (hid == tracks[track_widx-1].audio_hid
|
|
&& filling == STATE_END_OF_PLAYLIST)
|
|
{
|
|
/* This was the last track in the playlist.
|
|
We now have all the data we need. */
|
|
logf("last track finished buffering");
|
|
filling = STATE_FINISHED;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* --- Audio thread --- */
|
|
|
|
static bool audio_have_tracks(void)
|
|
{
|
|
return (audio_track_count() != 0);
|
|
}
|
|
|
|
static int audio_free_track_count(void)
|
|
{
|
|
/* Used tracks + free tracks adds up to MAX_TRACK - 1 */
|
|
return MAX_TRACK - 1 - audio_track_count();
|
|
}
|
|
|
|
int audio_track_count(void)
|
|
{
|
|
/* Calculate difference from track_ridx to track_widx
|
|
* taking into account a possible wrap-around. */
|
|
return (MAX_TRACK + track_widx - track_ridx) & MAX_TRACK_MASK;
|
|
}
|
|
|
|
long audio_filebufused(void)
|
|
{
|
|
return (long) buf_used();
|
|
}
|
|
|
|
/* Update track info after successful a codec track change */
|
|
static void audio_update_trackinfo(void)
|
|
{
|
|
bool resume = false;
|
|
|
|
/* Load the curent track's metadata into curtrack_id3 */
|
|
if (CUR_TI->id3_hid >= 0)
|
|
bufreadid3(CUR_TI->id3_hid, thistrack_id3);
|
|
|
|
/* Reset current position */
|
|
thistrack_id3->elapsed = 0;
|
|
|
|
#ifdef HAVE_TAGCACHE
|
|
/* Ignoring resume position for automatic track change if so configured */
|
|
resume = global_settings.autoresume_enable &&
|
|
(!automatic_skip /* Resume all manually selected tracks */
|
|
|| global_settings.autoresume_automatic == AUTORESUME_NEXTTRACK_ALWAYS
|
|
|| (global_settings.autoresume_automatic != AUTORESUME_NEXTTRACK_NEVER
|
|
/* Not never resume? */
|
|
&& autoresumable(thistrack_id3))); /* Pass Resume filter? */
|
|
#endif
|
|
|
|
if (!resume)
|
|
{
|
|
thistrack_id3->offset = 0;
|
|
}
|
|
|
|
logf("audio_update_trackinfo: Set offset for %s to %lX\n",
|
|
thistrack_id3->title, thistrack_id3->offset);
|
|
|
|
/* Update the codec API */
|
|
ci.filesize = CUR_TI->filesize;
|
|
ci.id3 = thistrack_id3;
|
|
ci.curpos = 0;
|
|
ci.taginfo_ready = &CUR_TI->taginfo_ready;
|
|
}
|
|
|
|
/* Clear tracks between write and read, non inclusive */
|
|
static void audio_clear_track_entries(void)
|
|
{
|
|
int cur_idx = track_widx;
|
|
|
|
logf("Clearing tracks: r%d/w%d", track_ridx, track_widx);
|
|
|
|
/* Loop over all tracks from write-to-read */
|
|
while (1)
|
|
{
|
|
cur_idx = (cur_idx + 1) & MAX_TRACK_MASK;
|
|
|
|
if (cur_idx == track_ridx)
|
|
break;
|
|
|
|
clear_track_info(&tracks[cur_idx]);
|
|
}
|
|
}
|
|
|
|
/* Clear all tracks */
|
|
static bool audio_release_tracks(void)
|
|
{
|
|
int i, cur_idx;
|
|
|
|
logf("releasing all tracks");
|
|
|
|
for(i = 0; i < MAX_TRACK; i++)
|
|
{
|
|
cur_idx = (track_ridx + i) & MAX_TRACK_MASK;
|
|
if (!clear_track_info(&tracks[cur_idx]))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool audio_loadcodec(bool start_play)
|
|
{
|
|
int prev_track, hid;
|
|
char codec_path[MAX_PATH]; /* Full path to codec */
|
|
const struct mp3entry *id3, *prev_id3;
|
|
|
|
if (tracks[track_widx].id3_hid < 0) {
|
|
return false;
|
|
}
|
|
|
|
id3 = bufgetid3(tracks[track_widx].id3_hid);
|
|
if (!id3)
|
|
return false;
|
|
|
|
const char *codec_fn = get_codec_filename(id3->codectype);
|
|
if (codec_fn == NULL)
|
|
return false;
|
|
|
|
tracks[track_widx].codec_hid = -1;
|
|
|
|
if (start_play)
|
|
{
|
|
/* Load the codec directly from disk and save some memory. */
|
|
track_ridx = track_widx;
|
|
ci.filesize = CUR_TI->filesize;
|
|
ci.id3 = thistrack_id3;
|
|
ci.taginfo_ready = &CUR_TI->taginfo_ready;
|
|
ci.curpos = 0;
|
|
return codec_load(-1, id3->codectype);
|
|
}
|
|
else
|
|
{
|
|
/* If we already have another track than this one buffered */
|
|
if (track_widx != track_ridx)
|
|
{
|
|
prev_track = (track_widx - 1) & MAX_TRACK_MASK;
|
|
|
|
id3 = bufgetid3(tracks[track_widx].id3_hid);
|
|
prev_id3 = bufgetid3(tracks[prev_track].id3_hid);
|
|
|
|
/* If the previous codec is the same as this one and the current
|
|
* one is the correct one, there is no need to put another copy of
|
|
* it on the file buffer */
|
|
if (id3 && prev_id3)
|
|
{
|
|
int codt = get_codec_base_type(id3->codectype);
|
|
int prev_codt = get_codec_base_type(prev_id3->codectype);
|
|
int cod_loaded = get_codec_base_type(codec_loaded());
|
|
|
|
if (codt == prev_codt && codt == cod_loaded)
|
|
{
|
|
logf("Reusing prev. codec");
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
codec_get_full_path(codec_path, codec_fn);
|
|
|
|
hid = tracks[track_widx].codec_hid = bufopen(codec_path, 0, TYPE_CODEC, NULL);
|
|
|
|
/* not an error if codec load it supported, will load it from disk
|
|
* application builds don't support it
|
|
*/
|
|
if (hid < 0 && hid != ERR_UNSUPPORTED_TYPE)
|
|
return false;
|
|
|
|
if (hid >= 0)
|
|
logf("Loaded codec");
|
|
else
|
|
logf("Buffering codec unsupported, load later from disk");
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Load metadata for the next track (with bufopen). The rest of the track
|
|
loading will be handled by audio_finish_load_track once the metadata has been
|
|
actually loaded by the buffering thread. */
|
|
static bool audio_load_track(size_t offset, bool start_play)
|
|
{
|
|
char name_buf[MAX_PATH + 1];
|
|
const char *trackname;
|
|
int fd = -1;
|
|
|
|
if (track_load_started) {
|
|
/* There is already a track load in progress, so track_widx hasn't been
|
|
incremented yet. Loading another track would overwrite the one that
|
|
hasn't finished loading. */
|
|
logf("audio_load_track(): a track load is already in progress");
|
|
return false;
|
|
}
|
|
|
|
start_play_g = start_play; /* will be read by audio_finish_load_track */
|
|
|
|
/* 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 (!audio_free_track_count())
|
|
{
|
|
logf("No free tracks");
|
|
return false;
|
|
}
|
|
|
|
last_peek_offset++;
|
|
tracks[track_widx].taginfo_ready = false;
|
|
|
|
logf("Buffering track: r%d/w%d", track_ridx, track_widx);
|
|
/* Get track name from current playlist read position. */
|
|
while ((trackname = playlist_peek(last_peek_offset, name_buf,
|
|
sizeof(name_buf))) != NULL)
|
|
{
|
|
/* Handle broken playlists. */
|
|
fd = open(trackname, O_RDONLY);
|
|
if (fd < 0)
|
|
{
|
|
logf("Open failed");
|
|
/* Skip invalid entry from playlist. */
|
|
playlist_skip_entry(NULL, last_peek_offset);
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
|
|
if (!trackname)
|
|
{
|
|
logf("End-of-playlist");
|
|
memset(&unbuffered_id3, 0, sizeof(struct mp3entry));
|
|
filling = STATE_END_OF_PLAYLIST;
|
|
|
|
if (thistrack_id3->length == 0 && thistrack_id3->filesize == 0)
|
|
{
|
|
/* Stop playback if no valid track was found. */
|
|
audio_stop_playback();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
tracks[track_widx].filesize = filesize(fd);
|
|
|
|
if (offset > tracks[track_widx].filesize)
|
|
offset = 0;
|
|
|
|
/* Set default values */
|
|
if (start_play)
|
|
{
|
|
buf_set_watermark(filebuflen/2);
|
|
dsp_configure(ci.dsp, DSP_RESET, 0);
|
|
playlist_update_resume_info(audio_current_track());
|
|
}
|
|
|
|
/* Get track metadata if we don't already have it. */
|
|
if (tracks[track_widx].id3_hid < 0)
|
|
{
|
|
tracks[track_widx].id3_hid = bufopen(trackname, 0, TYPE_ID3, NULL);
|
|
|
|
if (tracks[track_widx].id3_hid < 0)
|
|
{
|
|
/* Buffer is full. */
|
|
get_metadata(&unbuffered_id3, fd, trackname);
|
|
last_peek_offset--;
|
|
close(fd);
|
|
logf("buffer is full for now (get metadata)");
|
|
filling = STATE_FULL;
|
|
return false;
|
|
}
|
|
|
|
if (track_widx == track_ridx)
|
|
{
|
|
/* TODO: Superfluos buffering call? */
|
|
buf_request_buffer_handle(tracks[track_widx].id3_hid);
|
|
if (bufreadid3(tracks[track_widx].id3_hid, thistrack_id3))
|
|
{
|
|
thistrack_id3->offset = offset;
|
|
logf("audio_load_track: set offset for %s to %lX\n",
|
|
thistrack_id3->title,
|
|
offset);
|
|
}
|
|
else
|
|
memset(thistrack_id3, 0, sizeof(struct mp3entry));
|
|
}
|
|
|
|
if (start_play)
|
|
{
|
|
playlist_update_resume_info(audio_current_track());
|
|
}
|
|
}
|
|
|
|
close(fd);
|
|
track_load_started = true; /* Remember that we've started loading a track */
|
|
return true;
|
|
}
|
|
|
|
#ifdef HAVE_ALBUMART
|
|
/* Load any album art for the file */
|
|
static void audio_load_albumart(struct mp3entry *track_id3)
|
|
{
|
|
int i;
|
|
|
|
FOREACH_ALBUMART(i)
|
|
{
|
|
struct bufopen_bitmap_data user_data;
|
|
int hid = ERR_HANDLE_NOT_FOUND;
|
|
|
|
/* albumart_slots may change during a yield of bufopen,
|
|
* but that's no problem */
|
|
if (tracks[track_widx].aa_hid[i] >= 0 || !albumart_slots[i].used)
|
|
continue;
|
|
|
|
memset(&user_data, 0, sizeof(user_data));
|
|
user_data.dim = &(albumart_slots[i].dim);
|
|
|
|
/* we can only decode jpeg for embedded AA */
|
|
if (track_id3->embed_albumart && track_id3->albumart.type == AA_TYPE_JPG)
|
|
{
|
|
user_data.embedded_albumart = &(track_id3->albumart);
|
|
hid = bufopen(track_id3->path, 0, TYPE_BITMAP, &user_data);
|
|
}
|
|
|
|
if (hid < 0 && hid != ERR_BUFFER_FULL)
|
|
{
|
|
/* no embedded AA or it couldn't be loaded, try other sources */
|
|
char path[MAX_PATH];
|
|
|
|
if (find_albumart(track_id3, path, sizeof(path),
|
|
&(albumart_slots[i].dim)))
|
|
{
|
|
user_data.embedded_albumart = NULL;
|
|
hid = bufopen(path, 0, TYPE_BITMAP, &user_data);
|
|
}
|
|
}
|
|
|
|
if (hid == ERR_BUFFER_FULL)
|
|
{
|
|
filling = STATE_FULL;
|
|
logf("buffer is full for now (get album art)");
|
|
}
|
|
else if (hid < 0)
|
|
{
|
|
logf("Album art loading failed");
|
|
}
|
|
|
|
tracks[track_widx].aa_hid[i] = hid;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Second part of the track loading: We now have the metadata available, so we
|
|
can load the codec, the album art and finally the audio data.
|
|
This is called on the audio thread after the buffering thread calls the
|
|
buffering_handle_finished_callback callback. */
|
|
static void audio_finish_load_track(void)
|
|
{
|
|
size_t file_offset = 0;
|
|
size_t offset = 0;
|
|
bool start_play = start_play_g;
|
|
|
|
track_load_started = false;
|
|
|
|
if (tracks[track_widx].id3_hid < 0) {
|
|
logf("No metadata");
|
|
return;
|
|
}
|
|
|
|
struct mp3entry *track_id3;
|
|
|
|
if (track_widx == track_ridx)
|
|
track_id3 = thistrack_id3;
|
|
else
|
|
track_id3 = bufgetid3(tracks[track_widx].id3_hid);
|
|
|
|
if (track_id3->length == 0 && track_id3->filesize == 0)
|
|
{
|
|
logf("audio_finish_load_track: invalid metadata");
|
|
|
|
/* Invalid metadata */
|
|
bufclose(tracks[track_widx].id3_hid);
|
|
tracks[track_widx].id3_hid = -1;
|
|
|
|
/* Skip invalid entry from playlist. */
|
|
playlist_skip_entry(NULL, last_peek_offset--);
|
|
|
|
/* load next track */
|
|
LOGFQUEUE("audio > audio Q_AUDIO_FILL_BUFFER %d", (int)start_play);
|
|
queue_post(&audio_queue, Q_AUDIO_FILL_BUFFER, start_play);
|
|
|
|
return;
|
|
}
|
|
/* Try to load a cuesheet for the track */
|
|
if (curr_cue)
|
|
{
|
|
char cuepath[MAX_PATH];
|
|
if (look_for_cuesheet_file(track_id3->path, cuepath))
|
|
{
|
|
void *temp;
|
|
tracks[track_widx].cuesheet_hid =
|
|
bufalloc(NULL, sizeof(struct cuesheet), TYPE_CUESHEET);
|
|
if (tracks[track_widx].cuesheet_hid >= 0)
|
|
{
|
|
bufgetdata(tracks[track_widx].cuesheet_hid,
|
|
sizeof(struct cuesheet), &temp);
|
|
struct cuesheet *cuesheet = (struct cuesheet*)temp;
|
|
if (!parse_cuesheet(cuepath, cuesheet))
|
|
{
|
|
bufclose(tracks[track_widx].cuesheet_hid);
|
|
track_id3->cuesheet = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef HAVE_ALBUMART
|
|
audio_load_albumart(track_id3);
|
|
#endif
|
|
|
|
/* Load the codec. */
|
|
if (!audio_loadcodec(start_play))
|
|
{
|
|
if (tracks[track_widx].codec_hid == ERR_BUFFER_FULL)
|
|
{
|
|
/* No space for codec on buffer, not an error */
|
|
filling = STATE_FULL;
|
|
return;
|
|
}
|
|
|
|
/* This is an error condition, either no codec was found, or reading
|
|
* the codec file failed part way through, either way, skip the track */
|
|
/* FIXME: We should not use splashf from audio thread! */
|
|
splashf(HZ*2, "No codec for: %s", track_id3->path);
|
|
/* Skip invalid entry from playlist. */
|
|
playlist_skip_entry(NULL, last_peek_offset);
|
|
return;
|
|
}
|
|
|
|
track_id3->elapsed = 0;
|
|
offset = track_id3->offset;
|
|
size_t resume_rewind = (global_settings.resume_rewind *
|
|
track_id3->bitrate * 1000) / 8;
|
|
|
|
if (offset < resume_rewind)
|
|
{
|
|
offset = 0;
|
|
}
|
|
else
|
|
{
|
|
offset -= resume_rewind;
|
|
}
|
|
|
|
enum data_type type = TYPE_PACKET_AUDIO;
|
|
|
|
switch (track_id3->codectype) {
|
|
case AFMT_MPA_L1:
|
|
case AFMT_MPA_L2:
|
|
case AFMT_MPA_L3:
|
|
if (offset > 0) {
|
|
file_offset = offset;
|
|
}
|
|
break;
|
|
|
|
case AFMT_WAVPACK:
|
|
if (offset > 0) {
|
|
file_offset = offset;
|
|
track_id3->elapsed = track_id3->length / 2;
|
|
}
|
|
break;
|
|
|
|
case AFMT_NSF:
|
|
case AFMT_SPC:
|
|
case AFMT_SID:
|
|
logf("Loading atomic %d",track_id3->codectype);
|
|
type = TYPE_ATOMIC_AUDIO;
|
|
break;
|
|
|
|
default:
|
|
/* no special treatment needed */
|
|
break;
|
|
}
|
|
|
|
track_id3->offset = offset;
|
|
|
|
logf("load track: %s", track_id3->path);
|
|
|
|
if (file_offset > AUDIO_REBUFFER_GUESS_SIZE)
|
|
file_offset -= AUDIO_REBUFFER_GUESS_SIZE;
|
|
else if (track_id3->first_frame_offset)
|
|
file_offset = track_id3->first_frame_offset;
|
|
else
|
|
file_offset = 0;
|
|
|
|
tracks[track_widx].audio_hid = bufopen(track_id3->path, file_offset, type,
|
|
NULL);
|
|
|
|
/* No space left, not an error */
|
|
if (tracks[track_widx].audio_hid == ERR_BUFFER_FULL)
|
|
{
|
|
filling = STATE_FULL;
|
|
logf("buffer is full for now (load audio)");
|
|
return;
|
|
}
|
|
else if (tracks[track_widx].audio_hid < 0)
|
|
{
|
|
/* another error, do not continue either */
|
|
logf("Could not add audio data handle");
|
|
return;
|
|
}
|
|
|
|
/* All required data is now available for the codec -- unless the
|
|
autoresume feature is in effect. In the latter case, the codec
|
|
must wait until after PLAYBACK_EVENT_TRACK_BUFFER, which may
|
|
generate a resume position. */
|
|
#ifdef HAVE_TAGCACHE
|
|
if (! global_settings.autoresume_enable)
|
|
#endif
|
|
tracks[track_widx].taginfo_ready = true;
|
|
|
|
if (start_play)
|
|
{
|
|
ci.curpos=file_offset;
|
|
buf_request_buffer_handle(tracks[track_widx].audio_hid);
|
|
}
|
|
|
|
send_event(PLAYBACK_EVENT_TRACK_BUFFER, track_id3);
|
|
|
|
#ifdef HAVE_TAGCACHE
|
|
/* In case the autoresume feature has been enabled, finally all
|
|
required data is available for the codec. */
|
|
if (global_settings.autoresume_enable)
|
|
tracks[track_widx].taginfo_ready = true;
|
|
#endif
|
|
|
|
track_widx = (track_widx + 1) & MAX_TRACK_MASK;
|
|
|
|
/* load next track */
|
|
LOGFQUEUE("audio > audio Q_AUDIO_FILL_BUFFER");
|
|
queue_post(&audio_queue, Q_AUDIO_FILL_BUFFER, 0);
|
|
|
|
return;
|
|
}
|
|
|
|
static void audio_fill_file_buffer(bool start_play, size_t offset)
|
|
{
|
|
trigger_cpu_boost();
|
|
|
|
/* No need to rebuffer if there are track skips pending,
|
|
* however don't cancel buffering on skipping while filling. */
|
|
if (ci.new_track != 0 && filling != STATE_FILLING)
|
|
return;
|
|
filling = STATE_FILLING;
|
|
|
|
/* Must reset the buffer before use if trashed or voice only - voice
|
|
file size shouldn't have changed so we can go straight from
|
|
AUDIOBUF_STATE_VOICED_ONLY to AUDIOBUF_STATE_INITIALIZED */
|
|
if (buffer_state != AUDIOBUF_STATE_INITIALIZED)
|
|
audio_reset_buffer();
|
|
|
|
logf("Starting buffer fill");
|
|
|
|
if (!start_play)
|
|
audio_clear_track_entries();
|
|
|
|
/* Save the current resume position once. */
|
|
playlist_update_resume_info(audio_current_track());
|
|
|
|
audio_load_track(offset, start_play);
|
|
}
|
|
|
|
static void audio_rebuffer(void)
|
|
{
|
|
logf("Forcing rebuffer");
|
|
|
|
clear_track_info(CUR_TI);
|
|
|
|
/* Reset track pointers */
|
|
track_widx = track_ridx;
|
|
audio_clear_track_entries();
|
|
|
|
/* Reset a possibly interrupted track load */
|
|
track_load_started = false;
|
|
|
|
/* Fill the buffer */
|
|
last_peek_offset = -1;
|
|
ci.curpos = 0;
|
|
|
|
if (!CUR_TI->taginfo_ready)
|
|
memset(thistrack_id3, 0, sizeof(struct mp3entry));
|
|
|
|
audio_fill_file_buffer(false, 0);
|
|
}
|
|
|
|
/* Called on request from the codec to get a new track. This is the codec part
|
|
of the track transition. */
|
|
static void audio_last_track(bool automatic)
|
|
{
|
|
if (automatic)
|
|
{
|
|
ci.new_track = 0;
|
|
automatic_skip = false;
|
|
|
|
if (filling != STATE_ENDING)
|
|
{
|
|
/* Monitor remaining PCM before stopping */
|
|
filling = STATE_ENDING;
|
|
pcmbuf_monitor_track_change(true);
|
|
}
|
|
|
|
codec_stop();
|
|
}
|
|
else
|
|
{
|
|
audio_stop_playback();
|
|
}
|
|
}
|
|
|
|
static void audio_check_new_track(void)
|
|
{
|
|
int track_count;
|
|
int old_track_ridx;
|
|
int i, idx;
|
|
bool forward;
|
|
struct mp3entry *temp;
|
|
|
|
if (ci.new_track == 0)
|
|
{
|
|
ci.new_track++;
|
|
automatic_skip = true;
|
|
}
|
|
|
|
track_count = audio_track_count();
|
|
old_track_ridx = track_ridx;
|
|
|
|
/* Now it's good time to send track finish events. */
|
|
send_event(PLAYBACK_EVENT_TRACK_FINISH, thistrack_id3);
|
|
/* swap the mp3entry pointers */
|
|
temp = thistrack_id3;
|
|
thistrack_id3 = othertrack_id3;
|
|
othertrack_id3 = temp;
|
|
ci.id3 = thistrack_id3;
|
|
memset(thistrack_id3, 0, sizeof(struct mp3entry));
|
|
|
|
if (dir_skip)
|
|
{
|
|
dir_skip = false;
|
|
/* regardless of the return value we need to rebuffer.
|
|
if it fails the old playlist will resume, else the
|
|
next dir will start playing */
|
|
playlist_next_dir(ci.new_track);
|
|
ci.new_track = 0;
|
|
audio_rebuffer();
|
|
goto skip_done;
|
|
}
|
|
|
|
if (new_playlist)
|
|
ci.new_track = 0;
|
|
|
|
/* If the playlist isn't that big */
|
|
if (automatic_skip)
|
|
{
|
|
while (!playlist_check(ci.new_track))
|
|
{
|
|
if (ci.new_track >= 0)
|
|
{
|
|
audio_last_track(true);
|
|
return;
|
|
}
|
|
ci.new_track++;
|
|
}
|
|
}
|
|
|
|
/* Update the playlist */
|
|
last_peek_offset -= ci.new_track;
|
|
|
|
if (playlist_next(ci.new_track) < 0)
|
|
{
|
|
/* End of list */
|
|
audio_last_track(automatic_skip);
|
|
return;
|
|
}
|
|
|
|
if (new_playlist)
|
|
{
|
|
ci.new_track = 1;
|
|
new_playlist = false;
|
|
}
|
|
|
|
/* Save a pointer to the old track to allow later clearing */
|
|
prev_ti = CUR_TI;
|
|
|
|
for (i = 0; i < ci.new_track; i++)
|
|
{
|
|
idx = (track_ridx + i) & MAX_TRACK_MASK;
|
|
struct mp3entry *id3 = bufgetid3(tracks[idx].id3_hid);
|
|
ssize_t offset = buf_handle_offset(tracks[idx].audio_hid);
|
|
if (!id3 || offset < 0 || (unsigned)offset > id3->first_frame_offset)
|
|
{
|
|
/* We don't have all the audio data for that track, so clear it,
|
|
but keep the metadata. */
|
|
if (tracks[idx].audio_hid >= 0 && bufclose(tracks[idx].audio_hid))
|
|
{
|
|
tracks[idx].audio_hid = -1;
|
|
tracks[idx].filesize = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Move to the new track */
|
|
track_ridx = (track_ridx + ci.new_track) & MAX_TRACK_MASK;
|
|
buf_set_base_handle(CUR_TI->audio_hid);
|
|
|
|
if (automatic_skip)
|
|
{
|
|
wps_offset = -ci.new_track;
|
|
}
|
|
|
|
/* If it is not safe to even skip this many track entries */
|
|
if (ci.new_track >= track_count || ci.new_track <= track_count - MAX_TRACK)
|
|
{
|
|
ci.new_track = 0;
|
|
audio_rebuffer();
|
|
goto skip_done;
|
|
}
|
|
|
|
forward = ci.new_track > 0;
|
|
ci.new_track = 0;
|
|
|
|
/* If the target track is clearly not in memory */
|
|
if (CUR_TI->filesize == 0 || !CUR_TI->taginfo_ready)
|
|
{
|
|
audio_rebuffer();
|
|
goto skip_done;
|
|
}
|
|
|
|
/* When skipping backwards, it is possible that we've found a track that's
|
|
* buffered, but which is around the track-wrap and therefore not the track
|
|
* we are looking for */
|
|
if (!forward)
|
|
{
|
|
int cur_idx = track_ridx;
|
|
bool taginfo_ready = true;
|
|
/* We've wrapped the buffer backwards if new > old */
|
|
bool wrap = track_ridx > old_track_ridx;
|
|
|
|
while (1)
|
|
{
|
|
cur_idx = (cur_idx + 1) & MAX_TRACK_MASK;
|
|
|
|
/* if we've advanced past the wrap when cur_idx is zeroed */
|
|
if (!cur_idx)
|
|
wrap = false;
|
|
|
|
/* if we aren't still on the wrap and we've caught the old track */
|
|
if (!(wrap || cur_idx < old_track_ridx))
|
|
break;
|
|
|
|
/* If we hit a track in between without valid tag info, bail */
|
|
if (!tracks[cur_idx].taginfo_ready)
|
|
{
|
|
taginfo_ready = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!taginfo_ready)
|
|
{
|
|
audio_rebuffer();
|
|
}
|
|
}
|
|
|
|
skip_done:
|
|
audio_update_trackinfo();
|
|
pcmbuf_start_track_change(automatic_skip);
|
|
|
|
if (get_codec_base_type(codec_loaded()) ==
|
|
get_codec_base_type(thistrack_id3->codectype))
|
|
{
|
|
/* codec is the same base type */
|
|
logf("New track loaded");
|
|
codec_ack_msg(Q_CODEC_REQUEST_COMPLETE, false);
|
|
}
|
|
else
|
|
{
|
|
/* a codec change is required */
|
|
logf("New codec: %d/%d", thistrack_id3->codectype, codec_loaded());
|
|
codec_ack_msg(Q_CODEC_REQUEST_COMPLETE, true);
|
|
codec_load(tracks[track_ridx].codec_hid, thistrack_id3->codectype);
|
|
tracks[track_ridx].codec_hid = -1; /* Codec thread will close it */
|
|
}
|
|
}
|
|
|
|
unsigned long audio_prev_elapsed(void)
|
|
{
|
|
return prev_track_elapsed;
|
|
}
|
|
|
|
void audio_set_prev_elapsed(unsigned long setting)
|
|
{
|
|
prev_track_elapsed = setting;
|
|
}
|
|
|
|
/* Stop the codec and reset the PCM buffer */
|
|
static void audio_stop_codec_flush(void)
|
|
{
|
|
bool pcm_playing;
|
|
|
|
pcmbuf_pause(true);
|
|
|
|
codec_stop();
|
|
|
|
pcm_play_lock();
|
|
|
|
pcm_playing = pcm_is_playing();
|
|
|
|
pcmbuf_play_stop();
|
|
queue_clear(&pcmbuf_queue);
|
|
|
|
if (pcm_playing)
|
|
pcmbuf_pause(paused);
|
|
|
|
pcm_play_unlock();
|
|
}
|
|
|
|
static void audio_stop_playback(void)
|
|
{
|
|
if (playing)
|
|
{
|
|
/* If we were playing, save resume information */
|
|
struct mp3entry *id3 = NULL;
|
|
|
|
if (!ci.stop_codec)
|
|
id3 = audio_current_track();
|
|
|
|
/* Save the current playing spot, or NULL if the playlist has ended */
|
|
playlist_update_resume_info(id3);
|
|
|
|
/* Now it's good time to send track finish events. Do this
|
|
only if this hasn't been done already as part of a track
|
|
switch. */
|
|
if (id3 == thistrack_id3)
|
|
send_event(PLAYBACK_EVENT_TRACK_FINISH, thistrack_id3);
|
|
|
|
/* TODO: Create auto bookmark too? */
|
|
|
|
prev_track_elapsed = othertrack_id3->elapsed;
|
|
|
|
remove_event(BUFFER_EVENT_BUFFER_LOW, buffering_low_buffer_callback);
|
|
}
|
|
|
|
audio_stop_codec_flush();
|
|
paused = false;
|
|
playing = false;
|
|
track_load_started = false;
|
|
|
|
filling = STATE_IDLE;
|
|
|
|
/* Mark all entries null. */
|
|
audio_clear_track_entries();
|
|
|
|
/* Close all tracks */
|
|
audio_release_tracks();
|
|
}
|
|
|
|
static void audio_play_start(size_t offset)
|
|
{
|
|
int i;
|
|
|
|
send_event(PLAYBACK_EVENT_START_PLAYBACK, NULL);
|
|
#if INPUT_SRC_CAPS != 0
|
|
audio_set_input_source(AUDIO_SRC_PLAYBACK, SRCF_PLAYBACK);
|
|
audio_set_output_source(AUDIO_SRC_PLAYBACK);
|
|
#endif
|
|
|
|
paused = false;
|
|
audio_stop_codec_flush();
|
|
|
|
playing = true;
|
|
track_load_started = false;
|
|
|
|
ci.new_track = 0;
|
|
ci.seek_time = 0;
|
|
wps_offset = 0;
|
|
|
|
sound_set_volume(global_settings.volume);
|
|
track_widx = track_ridx = 0;
|
|
buf_set_base_handle(-1);
|
|
|
|
/* Clear all track entries. */
|
|
for (i = 0; i < MAX_TRACK; i++) {
|
|
clear_track_info(&tracks[i]);
|
|
}
|
|
|
|
last_peek_offset = -1;
|
|
|
|
/* Officially playing */
|
|
queue_reply(&audio_queue, 1);
|
|
|
|
audio_fill_file_buffer(true, offset);
|
|
|
|
add_event(BUFFER_EVENT_BUFFER_LOW, false, buffering_low_buffer_callback);
|
|
|
|
LOGFQUEUE("audio > audio Q_AUDIO_TRACK_CHANGED");
|
|
queue_post(&audio_queue, Q_AUDIO_TRACK_CHANGED, 0);
|
|
}
|
|
|
|
|
|
/* Invalidates all but currently playing track. */
|
|
static void audio_invalidate_tracks(void)
|
|
{
|
|
if (audio_have_tracks())
|
|
{
|
|
last_peek_offset = 0;
|
|
track_widx = track_ridx;
|
|
|
|
/* Mark all other entries null (also buffered wrong metadata). */
|
|
audio_clear_track_entries();
|
|
|
|
track_widx = (track_widx + 1) & MAX_TRACK_MASK;
|
|
|
|
audio_fill_file_buffer(false, 0);
|
|
send_event(PLAYBACK_EVENT_TRACK_CHANGE, thistrack_id3);
|
|
}
|
|
}
|
|
|
|
static void audio_new_playlist(void)
|
|
{
|
|
/* Prepare to start a new fill from the beginning of the playlist */
|
|
last_peek_offset = -1;
|
|
|
|
/* Signal the codec to initiate a track change forward */
|
|
new_playlist = true;
|
|
ci.new_track = 1;
|
|
|
|
if (audio_have_tracks())
|
|
{
|
|
if (paused)
|
|
skipped_during_pause = true;
|
|
track_widx = track_ridx;
|
|
audio_clear_track_entries();
|
|
|
|
track_widx = (track_widx + 1) & MAX_TRACK_MASK;
|
|
|
|
/* Mark the current track as invalid to prevent skipping back to it */
|
|
CUR_TI->taginfo_ready = false;
|
|
}
|
|
|
|
/* Officially playing */
|
|
queue_reply(&audio_queue, 1);
|
|
|
|
audio_fill_file_buffer(false, 0);
|
|
}
|
|
|
|
/* Called on manual track skip */
|
|
static void audio_initiate_track_change(long direction)
|
|
{
|
|
logf("audio_initiate_track_change(%ld)", direction);
|
|
|
|
ci.new_track += direction;
|
|
wps_offset -= direction;
|
|
if (paused)
|
|
skipped_during_pause = true;
|
|
}
|
|
|
|
/* Called on manual dir skip */
|
|
static void audio_initiate_dir_change(long direction)
|
|
{
|
|
dir_skip = true;
|
|
ci.new_track = direction;
|
|
if (paused)
|
|
skipped_during_pause = true;
|
|
}
|
|
|
|
/* Called when PCM track change is complete */
|
|
static void audio_finalise_track_change(void)
|
|
{
|
|
logf("audio_finalise_track_change");
|
|
|
|
if (automatic_skip)
|
|
{
|
|
wps_offset = 0;
|
|
automatic_skip = false;
|
|
|
|
/* Invalidate prevtrack_id3 */
|
|
memset(othertrack_id3, 0, sizeof(struct mp3entry));
|
|
|
|
if (prev_ti && prev_ti->audio_hid < 0)
|
|
{
|
|
/* No audio left so we clear all the track info. */
|
|
clear_track_info(prev_ti);
|
|
}
|
|
}
|
|
send_event(PLAYBACK_EVENT_TRACK_CHANGE, thistrack_id3);
|
|
playlist_update_resume_info(audio_current_track());
|
|
}
|
|
|
|
static void audio_seek_complete(void)
|
|
{
|
|
logf("audio_seek_complete");
|
|
|
|
if (!playing)
|
|
return;
|
|
|
|
/* If seeking-while-playing, pcm_is_paused() is true.
|
|
* If seeking-while-paused, audio_status PAUSE is true.
|
|
* A seamless seek skips this section. */
|
|
ci.seek_time = 0;
|
|
|
|
pcm_play_lock();
|
|
|
|
if (pcm_is_paused() || paused)
|
|
{
|
|
/* Clear the buffer */
|
|
pcmbuf_play_stop();
|
|
|
|
/* If seeking-while-playing, resume PCM playback */
|
|
if (!paused)
|
|
pcmbuf_pause(false);
|
|
}
|
|
|
|
pcm_play_unlock();
|
|
}
|
|
|
|
static void audio_codec_status_message(long reason, int status)
|
|
{
|
|
/* TODO: Push the errors up to the normal UI somewhere */
|
|
switch (reason)
|
|
{
|
|
case Q_CODEC_LOAD_DISK:
|
|
case Q_CODEC_LOAD:
|
|
if (!playing)
|
|
return;
|
|
|
|
if (status < 0)
|
|
{
|
|
splash(HZ*2, "Codec failure");
|
|
audio_check_new_track();
|
|
}
|
|
break;
|
|
|
|
#ifdef AUDIO_HAVE_RECORDING
|
|
case Q_ENCODER_LOAD_DISK:
|
|
if (status < 0)
|
|
splash(HZ*2, "Encoder failure");
|
|
break;
|
|
#endif /* AUDIO_HAVE_RECORDING */
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Layout audio buffer as follows - iram buffer depends on target:
|
|
* [|SWAP:iram][|TALK]|FILE|GUARD|PCM|[SWAP:dram[|iram]|]
|
|
*/
|
|
static void audio_reset_buffer(void)
|
|
{
|
|
/* see audio_get_recording_buffer if this is modified */
|
|
logf("audio_reset_buffer");
|
|
|
|
/* If the setup of anything allocated before the file buffer is
|
|
changed, do check the adjustments after the buffer_alloc call
|
|
as it will likely be affected and need sliding over */
|
|
|
|
/* Initially set up file buffer as all space available */
|
|
malloc_buf = audiobuf + talk_get_bufsize();
|
|
|
|
/* Align the malloc buf to line size.
|
|
* Especially important to cf targets that do line reads/writes.
|
|
* Also for targets which need aligned DMA storage buffers */
|
|
malloc_buf = (unsigned char *)(((uintptr_t)malloc_buf + (CACHEALIGN_SIZE - 1)) & ~(CACHEALIGN_SIZE - 1));
|
|
filebuf = malloc_buf; /* filebuf line align implied */
|
|
filebuflen = (audiobufend - filebuf) & ~(CACHEALIGN_SIZE - 1);
|
|
|
|
/* Subtract whatever the pcm buffer says it used plus the guard buffer */
|
|
const size_t pcmbuf_size = pcmbuf_init(filebuf + filebuflen) +GUARD_BUFSIZE;
|
|
if(pcmbuf_size > filebuflen)
|
|
panicf("%s(): EOM (%zu > %zu)", __func__, pcmbuf_size, filebuflen);
|
|
|
|
filebuflen -= pcmbuf_size;
|
|
|
|
/* Make sure filebuflen is a longword multiple after adjustment - filebuf
|
|
will already be line aligned */
|
|
filebuflen &= ~3;
|
|
|
|
buffering_reset(filebuf, filebuflen);
|
|
|
|
/* Clear any references to the file buffer */
|
|
buffer_state = AUDIOBUF_STATE_INITIALIZED;
|
|
|
|
#if defined(ROCKBOX_HAS_LOGF) && defined(LOGF_ENABLE)
|
|
/* Make sure everything adds up - yes, some info is a bit redundant but
|
|
aids viewing and the sumation of certain variables should add up to
|
|
the location of others. */
|
|
{
|
|
size_t pcmbufsize;
|
|
const unsigned char *pcmbuf = pcmbuf_get_meminfo(&pcmbufsize);
|
|
logf("mabuf: %08X", (unsigned)malloc_buf);
|
|
logf("fbuf: %08X", (unsigned)filebuf);
|
|
logf("fbufe: %08X", (unsigned)(filebuf + filebuflen));
|
|
logf("gbuf: %08X", (unsigned)(filebuf + filebuflen));
|
|
logf("gbufe: %08X", (unsigned)(filebuf + filebuflen + GUARD_BUFSIZE));
|
|
logf("pcmb: %08X", (unsigned)pcmbuf);
|
|
logf("pcmbe: %08X", (unsigned)(pcmbuf + pcmbufsize));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void audio_thread(void)
|
|
{
|
|
struct queue_event ev;
|
|
|
|
pcm_postinit();
|
|
|
|
audio_thread_ready = true;
|
|
|
|
while (1)
|
|
{
|
|
switch (filling) {
|
|
case STATE_IDLE:
|
|
queue_wait(&audio_queue, &ev);
|
|
break;
|
|
|
|
#if (CONFIG_PLATFORM & PLATFORM_NATIVE)
|
|
case STATE_USB:
|
|
queue_wait(&audio_queue, &ev);
|
|
switch (ev.id) {
|
|
#ifdef AUDIO_HAVE_RECORDING
|
|
/* Must monitor the encoder message for recording so it can
|
|
remove it if we process the insertion before it does. It
|
|
cannot simply be removed from under recording however. */
|
|
case Q_AUDIO_LOAD_ENCODER:
|
|
break;
|
|
#endif
|
|
case SYS_USB_DISCONNECTED:
|
|
filling = STATE_IDLE;
|
|
default:
|
|
continue;
|
|
}
|
|
break;
|
|
#endif /* CONFIG_PLATFORM */
|
|
|
|
default:
|
|
/* End of buffering, let's calculate the watermark and
|
|
unboost */
|
|
set_filebuf_watermark();
|
|
cancel_cpu_boost();
|
|
/* Fall-through */
|
|
case STATE_FILLING:
|
|
case STATE_ENDING:
|
|
if (!pcmbuf_queue_scan(&ev))
|
|
queue_wait_w_tmo(&audio_queue, &ev, HZ/2);
|
|
break;
|
|
}
|
|
|
|
switch (ev.id) {
|
|
|
|
case Q_AUDIO_FILL_BUFFER:
|
|
LOGFQUEUE("audio < Q_AUDIO_FILL_BUFFER %d", (int)ev.data);
|
|
audio_fill_file_buffer((bool)ev.data, 0);
|
|
break;
|
|
|
|
case Q_AUDIO_FINISH_LOAD:
|
|
LOGFQUEUE("audio < Q_AUDIO_FINISH_LOAD");
|
|
audio_finish_load_track();
|
|
buf_set_base_handle(CUR_TI->audio_hid);
|
|
break;
|
|
|
|
case Q_AUDIO_PLAY:
|
|
LOGFQUEUE("audio < Q_AUDIO_PLAY");
|
|
if (playing && ev.data <= 0)
|
|
audio_new_playlist();
|
|
else
|
|
{
|
|
audio_stop_playback();
|
|
audio_play_start((size_t)ev.data);
|
|
}
|
|
break;
|
|
|
|
case Q_AUDIO_STOP:
|
|
LOGFQUEUE("audio < Q_AUDIO_STOP");
|
|
if (playing)
|
|
audio_stop_playback();
|
|
if (ev.data != 0)
|
|
queue_clear(&audio_queue);
|
|
break;
|
|
|
|
case Q_AUDIO_PAUSE:
|
|
LOGFQUEUE("audio < Q_AUDIO_PAUSE");
|
|
if (!(bool) ev.data && skipped_during_pause
|
|
#ifdef HAVE_CROSSFADE
|
|
&& !pcmbuf_is_crossfade_active()
|
|
#endif
|
|
)
|
|
pcmbuf_play_stop(); /* Flush old track on resume after skip */
|
|
skipped_during_pause = false;
|
|
if (!playing)
|
|
break;
|
|
pcmbuf_pause((bool)ev.data);
|
|
paused = (bool)ev.data;
|
|
break;
|
|
|
|
case Q_AUDIO_SKIP:
|
|
LOGFQUEUE("audio < Q_AUDIO_SKIP");
|
|
audio_initiate_track_change((long)ev.data);
|
|
break;
|
|
|
|
case Q_AUDIO_PRE_FF_REWIND:
|
|
LOGFQUEUE("audio < Q_AUDIO_PRE_FF_REWIND");
|
|
if (!playing)
|
|
break;
|
|
pcmbuf_pause(true);
|
|
break;
|
|
|
|
case Q_AUDIO_FF_REWIND:
|
|
LOGFQUEUE("audio < Q_AUDIO_FF_REWIND");
|
|
if (!playing)
|
|
break;
|
|
|
|
if (filling == STATE_ENDING)
|
|
{
|
|
/* Temp workaround: There is no codec available */
|
|
if (!paused)
|
|
pcmbuf_pause(false);
|
|
break;
|
|
}
|
|
|
|
if ((long)ev.data == 0)
|
|
{
|
|
/* About to restart the track - send track finish
|
|
events if not already done. */
|
|
if (thistrack_id3 == audio_current_track())
|
|
send_event(PLAYBACK_EVENT_TRACK_FINISH, thistrack_id3);
|
|
}
|
|
|
|
if (automatic_skip)
|
|
{
|
|
/* An automatic track skip is in progress. Finalize it,
|
|
then go back to the previous track */
|
|
audio_finalise_track_change();
|
|
ci.new_track = -1;
|
|
}
|
|
ci.seek_time = (long)ev.data+1;
|
|
break;
|
|
|
|
case Q_AUDIO_CHECK_NEW_TRACK:
|
|
LOGFQUEUE("audio < Q_AUDIO_CHECK_NEW_TRACK");
|
|
audio_check_new_track();
|
|
break;
|
|
|
|
case Q_AUDIO_DIR_SKIP:
|
|
LOGFQUEUE("audio < Q_AUDIO_DIR_SKIP");
|
|
audio_initiate_dir_change(ev.data);
|
|
break;
|
|
|
|
case Q_AUDIO_FLUSH:
|
|
LOGFQUEUE("audio < Q_AUDIO_FLUSH");
|
|
audio_invalidate_tracks();
|
|
break;
|
|
|
|
case Q_AUDIO_TRACK_CHANGED:
|
|
/* PCM track change done */
|
|
LOGFQUEUE("audio < Q_AUDIO_TRACK_CHANGED");
|
|
if (filling != STATE_ENDING)
|
|
audio_finalise_track_change();
|
|
else if (playing)
|
|
audio_stop_playback();
|
|
break;
|
|
|
|
case Q_AUDIO_SEEK_COMPLETE:
|
|
/* Codec seek done */
|
|
LOGFQUEUE("audio < Q_AUDIO_SEEK_COMPLETE");
|
|
audio_seek_complete();
|
|
codec_ack_msg(Q_AUDIO_SEEK_COMPLETE, false);
|
|
break;
|
|
|
|
case Q_CODEC_LOAD:
|
|
case Q_CODEC_LOAD_DISK:
|
|
#ifdef AUDIO_HAVE_RECORDING
|
|
case Q_ENCODER_LOAD_DISK:
|
|
#endif
|
|
/* These are received when a codec has finished normally or
|
|
upon a codec error */
|
|
audio_codec_status_message(ev.id, ev.data);
|
|
break;
|
|
|
|
#if (CONFIG_PLATFORM & PLATFORM_NATIVE)
|
|
case SYS_USB_CONNECTED:
|
|
LOGFQUEUE("audio < SYS_USB_CONNECTED");
|
|
if (playing)
|
|
audio_stop_playback();
|
|
#ifdef PLAYBACK_VOICE
|
|
voice_stop();
|
|
#endif
|
|
filling = STATE_USB;
|
|
usb_acknowledge(SYS_USB_CONNECTED_ACK);
|
|
break;
|
|
#endif
|
|
|
|
#ifdef AUDIO_HAVE_RECORDING
|
|
case Q_AUDIO_LOAD_ENCODER:
|
|
if (playing)
|
|
audio_stop_playback();
|
|
else
|
|
codec_stop(); /* If encoder still loaded, stop it */
|
|
|
|
if (ev.data == AFMT_UNKNOWN)
|
|
break;
|
|
|
|
queue_reply(&audio_queue,
|
|
codec_load(-1, ev.data | CODEC_TYPE_ENCODER));
|
|
break;
|
|
#endif /* AUDIO_HAVE_RECORDING */
|
|
|
|
case SYS_TIMEOUT:
|
|
LOGFQUEUE_SYS_TIMEOUT("audio < SYS_TIMEOUT");
|
|
break;
|
|
|
|
default:
|
|
/* LOGFQUEUE("audio < default : %08lX", ev.id); */
|
|
break;
|
|
} /* end switch */
|
|
} /* end while */
|
|
}
|
|
|
|
/* Initialize the audio system - called from init() in main.c.
|
|
* Last function because of all the references to internal symbols
|
|
*/
|
|
void audio_init(void)
|
|
{
|
|
unsigned int audio_thread_id;
|
|
|
|
/* Can never do this twice */
|
|
if (audio_is_initialized)
|
|
{
|
|
logf("audio: already initialized");
|
|
return;
|
|
}
|
|
|
|
logf("audio: initializing");
|
|
|
|
/* Initialize queues before giving control elsewhere in case it likes
|
|
to send messages. Thread creation will be delayed however so nothing
|
|
starts running until ready if something yields such as talk_init. */
|
|
queue_init(&audio_queue, true);
|
|
queue_init(&pcmbuf_queue, false);
|
|
|
|
pcm_init();
|
|
|
|
codec_init_codec_api();
|
|
|
|
thistrack_id3 = &mp3entry_buf[0];
|
|
othertrack_id3 = &mp3entry_buf[1];
|
|
|
|
/* cuesheet support */
|
|
if (global_settings.cuesheet)
|
|
curr_cue = (struct cuesheet*)buffer_alloc(sizeof(struct cuesheet));
|
|
|
|
/* initialize the buffer */
|
|
filebuf = audiobuf;
|
|
|
|
/* audio_reset_buffer must to know the size of voice buffer so init
|
|
talk first */
|
|
talk_init();
|
|
|
|
make_codec_thread();
|
|
|
|
audio_thread_id = create_thread(audio_thread, audio_stack,
|
|
sizeof(audio_stack), CREATE_THREAD_FROZEN,
|
|
audio_thread_name IF_PRIO(, PRIORITY_USER_INTERFACE)
|
|
IF_COP(, CPU));
|
|
|
|
queue_enable_queue_send(&audio_queue, &audio_queue_sender_list,
|
|
audio_thread_id);
|
|
|
|
#ifdef PLAYBACK_VOICE
|
|
voice_thread_init();
|
|
#endif
|
|
|
|
#ifdef HAVE_CROSSFADE
|
|
/* Set crossfade setting for next buffer init which should be about... */
|
|
pcmbuf_request_crossfade_enable(global_settings.crossfade);
|
|
#endif
|
|
|
|
/* initialize the buffering system */
|
|
|
|
buffering_init();
|
|
/* ...now! Set up the buffers */
|
|
audio_reset_buffer();
|
|
|
|
int i;
|
|
for(i = 0; i < MAX_TRACK; i++)
|
|
{
|
|
tracks[i].audio_hid = -1;
|
|
tracks[i].id3_hid = -1;
|
|
tracks[i].codec_hid = -1;
|
|
tracks[i].cuesheet_hid = -1;
|
|
}
|
|
#ifdef HAVE_ALBUMART
|
|
FOREACH_ALBUMART(i)
|
|
{
|
|
int j;
|
|
for (j = 0; j < MAX_TRACK; j++)
|
|
{
|
|
tracks[j].aa_hid[i] = -1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
add_event(BUFFER_EVENT_REBUFFER, false, buffering_handle_rebuffer_callback);
|
|
add_event(BUFFER_EVENT_FINISHED, false, buffering_handle_finished_callback);
|
|
|
|
/* Probably safe to say */
|
|
audio_is_initialized = true;
|
|
|
|
sound_settings_apply();
|
|
#ifdef HAVE_DISK_STORAGE
|
|
audio_set_buffer_margin(global_settings.buffer_margin);
|
|
#endif
|
|
|
|
/* it's safe to let the threads run now */
|
|
#ifdef PLAYBACK_VOICE
|
|
voice_thread_resume();
|
|
#endif
|
|
codec_thread_resume();
|
|
thread_thaw(audio_thread_id);
|
|
|
|
} /* audio_init */
|
|
|
|
bool audio_is_thread_ready(void)
|
|
{
|
|
return audio_thread_ready;
|
|
}
|
|
|
|
size_t audio_get_filebuflen(void)
|
|
{
|
|
return filebuflen;
|
|
}
|
|
|
|
int get_audio_hid()
|
|
{
|
|
return CUR_TI->audio_hid;
|
|
}
|