summaryrefslogtreecommitdiff
path: root/src/libs/sound/trackplayer.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/libs/sound/trackplayer.c')
-rw-r--r--src/libs/sound/trackplayer.c874
1 files changed, 874 insertions, 0 deletions
diff --git a/src/libs/sound/trackplayer.c b/src/libs/sound/trackplayer.c
new file mode 100644
index 0000000..8068fc3
--- /dev/null
+++ b/src/libs/sound/trackplayer.c
@@ -0,0 +1,874 @@
+/*
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#include "sound.h"
+#include "sndintrn.h"
+#include "libs/sound/trackplayer.h"
+#include "trackint.h"
+#include "libs/log.h"
+#include "libs/memlib.h"
+#include "options.h"
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+#include <memory.h>
+
+
+static int track_count; // total number of tracks
+static int no_page_break; // set when combining several tracks into one
+
+// The one and only sample we play. Track switching is done by modifying
+// this sample while it is playing. StreamDecoderTaskFunc() picks up the
+// changes *mostly* seamlessly (keyword: mostly).
+// This is technically a hack, but a decent one ;)
+static TFB_SoundSample *sound_sample;
+
+static volatile uint32 tracks_length; // total length of tracks in game units
+
+static TFB_SoundChunk *chunks_head; // first decoder in linked list
+static TFB_SoundChunk *chunks_tail; // last decoder in linked list
+static TFB_SoundChunk *last_sub; // last chunk in the list with a subtitle
+
+static TFB_SoundChunk *cur_chunk; // currently playing chunk
+static TFB_SoundChunk *cur_sub_chunk; // currently displayed subtitle chunk
+
+// Accesses to cur_chunk and cur_sub_chunk are guarded by stream_mutex,
+// because these should only be accesses by the DoInput and the
+// stream player threads. Any other accesses would go unguarded.
+// Other data structures are unguarded and should only be accessed from
+// the DoInput thread at certain times, i.e. nothing can be modified
+// between StartTrack() and JumpTrack()/StopTrack() calls.
+// Use caution when changing code, as you may need to guard other data
+// structures the same way.
+
+static void seek_track (sint32 offset);
+
+// stream callbacks
+static bool OnStreamStart (TFB_SoundSample* sample);
+static bool OnChunkEnd (TFB_SoundSample* sample, audio_Object buffer);
+static void OnStreamEnd (TFB_SoundSample* sample);
+static void OnBufferTag (TFB_SoundSample* sample, TFB_SoundTag* tag);
+
+static TFB_SoundCallbacks trackCBs =
+{
+ OnStreamStart,
+ OnChunkEnd,
+ OnStreamEnd,
+ OnBufferTag,
+ NULL
+};
+
+static inline sint32
+chunk_end_time (TFB_SoundChunk *chunk)
+{
+ return (sint32) ((chunk->start_time + chunk->decoder->length)
+ * ONE_SECOND);
+}
+
+static inline sint32
+tracks_end_time (void)
+{
+ return chunk_end_time (chunks_tail);
+}
+
+//JumpTrack currently aborts the current track. However, it doesn't clear the
+//data-structures as StopTrack does. this allows for rewind even after the
+//track has finished playing
+//Question: Should 'abort' call StopTrack?
+void
+JumpTrack (void)
+{
+ if (!sound_sample)
+ return; // nothing to skip
+
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+ seek_track (tracks_length + 1);
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+}
+
+// This should just start playing a stream
+void
+PlayTrack (void)
+{
+ if (!sound_sample)
+ return; // nothing to play
+
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+ tracks_length = tracks_end_time ();
+ // decoder will be set in OnStreamStart()
+ cur_chunk = chunks_head;
+ // Always scope the speech data, we may need it
+ PlayStream (sound_sample, SPEECH_SOURCE, false, true, true);
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+}
+
+void
+PauseTrack (void)
+{
+ if (!sound_sample)
+ return; // nothing to pause
+
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+ PauseStream (SPEECH_SOURCE);
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+}
+
+// ResumeTrack should resume a paused track, and do nothing for a playing track
+void
+ResumeTrack (void)
+{
+ audio_IntVal state;
+
+ if (!sound_sample)
+ return; // nothing to resume
+
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+
+ if (!cur_chunk)
+ { // not playing anything, so no resuming
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+ return;
+ }
+
+ audio_GetSourcei (soundSource[SPEECH_SOURCE].handle, audio_SOURCE_STATE, &state);
+ if (state == audio_PAUSED)
+ ResumeStream (SPEECH_SOURCE);
+
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+}
+
+COUNT
+PlayingTrack (void)
+{
+ // This ignores the paused state and simply returns what track
+ // *should* be playing
+ COUNT result = 0; // default is none
+
+ if (!sound_sample)
+ return 0; // not playing anything
+
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+ if (cur_chunk)
+ result = cur_chunk->track_num + 1;
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+
+ return result;
+}
+
+void
+StopTrack (void)
+{
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+ StopStream (SPEECH_SOURCE);
+ track_count = 0;
+ tracks_length = 0;
+ cur_chunk = NULL;
+ cur_sub_chunk = NULL;
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+
+ if (chunks_head)
+ {
+ chunks_tail = NULL;
+ destroy_SoundChunk_list (chunks_head);
+ chunks_head = NULL;
+ last_sub = NULL;
+ }
+ if (sound_sample)
+ {
+ // We delete the decoders ourselves
+ sound_sample->decoder = NULL;
+ TFB_DestroySoundSample (sound_sample);
+ sound_sample = NULL;
+ }
+}
+
+static void
+DoTrackTag (TFB_SoundChunk *chunk)
+{
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+ if (chunk->callback)
+ chunk->callback(0);
+
+ cur_sub_chunk = chunk;
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+}
+
+// This func is called by PlayStream() when stream is about
+// to start. We have a chance to tweak the stream here.
+// This is called on the DoInput thread.
+static bool
+OnStreamStart (TFB_SoundSample* sample)
+{
+ if (sample != sound_sample)
+ return false; // Huh? Why did we get called on this?
+
+ if (!cur_chunk)
+ return false; // Stream shouldn't be playing at all
+
+ // Adjust the sample to play what we want
+ sample->decoder = cur_chunk->decoder;
+ sample->offset = (sint32) (cur_chunk->start_time * ONE_SECOND);
+
+ if (cur_chunk->tag_me)
+ DoTrackTag (cur_chunk);
+
+ return true;
+}
+
+// This func is called by StreamDecoderTaskFunc() when the last buffer
+// of the current chunk has been decoded (not when it has been *played*).
+// This is called on the stream task thread.
+static bool
+OnChunkEnd (TFB_SoundSample* sample, audio_Object buffer)
+{
+ if (sample != sound_sample)
+ return false; // Huh? Why did we get called on this?
+
+ if (!cur_chunk || !cur_chunk->next)
+ { // all chunks and tracks are done
+ return false;
+ }
+
+ // Move on to the next chunk
+ cur_chunk = cur_chunk->next;
+ // Adjust the sample to play what we want
+ sample->decoder = cur_chunk->decoder;
+ SoundDecoder_Rewind (sample->decoder);
+
+ log_add (log_Info, "Switching to stream %s at pos %d",
+ sample->decoder->filename, sample->decoder->start_sample);
+
+ if (cur_chunk->tag_me)
+ { // Tag the last buffer of the chunk with the next chunk
+ TFB_TagBuffer (sample, buffer, (intptr_t)cur_chunk);
+ }
+
+ return true;
+}
+
+// This func is called by StreamDecoderTaskFunc() when stream has ended
+// This is called on the stream task thread.
+static void
+OnStreamEnd (TFB_SoundSample* sample)
+{
+ if (sample != sound_sample)
+ return; // Huh? Why did we get called on this?
+
+ cur_chunk = NULL;
+ cur_sub_chunk = NULL;
+}
+
+// This func is called by StreamDecoderTaskFunc() when a tagged buffer
+// has finished playing.
+// This is called on the stream task thread.
+static void
+OnBufferTag (TFB_SoundSample* sample, TFB_SoundTag* tag)
+{
+ TFB_SoundChunk* chunk = (TFB_SoundChunk*) tag->data;
+
+ assert (sizeof (tag->data) >= sizeof (chunk));
+
+ if (sample != sound_sample)
+ return; // Huh? Why did we get called on this?
+
+ TFB_ClearBufferTag (tag);
+ DoTrackTag (chunk);
+}
+
+// Parse the timestamps string into an int array.
+// Rerturns number of timestamps parsed.
+static int
+GetTimeStamps (UNICODE *TimeStamps, sint32 *time_stamps)
+{
+ int pos;
+ int num = 0;
+
+ while (*TimeStamps && (pos = strcspn (TimeStamps, ",\r\n")))
+ {
+ UNICODE valStr[32];
+ uint32 val;
+
+ memcpy (valStr, TimeStamps, pos);
+ valStr[pos] = '\0';
+ val = strtoul (valStr, NULL, 10);
+ if (val)
+ {
+ *time_stamps = val;
+ num++;
+ time_stamps++;
+ }
+ TimeStamps += pos;
+ TimeStamps += strspn (TimeStamps, ",\r\n");
+ }
+ return (num);
+}
+
+#define TEXT_SPEED 80
+// Returns number of parsed pages
+static int
+SplitSubPages (UNICODE *text, UNICODE *pages[], sint32 timestamp[], int size)
+{
+ int lead_ellips = 0;
+ COUNT page;
+
+ for (page = 0; page < size && *text != '\0'; ++page)
+ {
+ int aft_ellips;
+ int pos;
+
+ // seek to EOL or end of the string
+ pos = strcspn (text, "\r\n");
+ // XXX: this will only work when ASCII punctuation and spaces
+ // are used exclusively
+ aft_ellips = 3 * (text[pos] != '\0' && pos > 0 &&
+ !ispunct (text[pos - 1]) && !isspace (text[pos - 1]));
+ pages[page] = HMalloc (sizeof (UNICODE) *
+ (lead_ellips + pos + aft_ellips + 1));
+ if (lead_ellips)
+ strcpy (pages[page], "..");
+ memcpy (pages[page] + lead_ellips, text, pos);
+ pages[page][lead_ellips + pos] = '\0'; // string term
+ if (aft_ellips)
+ strcpy (pages[page] + lead_ellips + pos, "...");
+ timestamp[page] = pos * TEXT_SPEED;
+ if (timestamp[page] < 1000)
+ timestamp[page] = 1000;
+ lead_ellips = aft_ellips ? 2 : 0;
+ text += pos;
+ // Skip any EOL
+ text += strspn (text, "\r\n");
+ }
+
+ return page;
+}
+
+// decodes several tracks into one and adds it to queue
+// track list is NULL-terminated
+// May only be called after at least one SpliceTrack(). This is a limitation
+// for the sake of timestamps, but it does not have to be so.
+void
+SpliceMultiTrack (UNICODE *TrackNames[], UNICODE *TrackText)
+{
+#define MAX_MULTI_TRACKS 20
+#define MAX_MULTI_BUFFERS 100
+ TFB_SoundDecoder* track_decs[MAX_MULTI_TRACKS + 1];
+ int tracks;
+ int slen1, slen2;
+
+ if (!TrackText)
+ {
+ log_add (log_Debug, "SpliceMultiTrack(): no track text");
+ return;
+ }
+
+ if (!sound_sample || !chunks_tail)
+ {
+ log_add (log_Warning, "SpliceMultiTrack(): Cannot be called before SpliceTrack()");
+ return;
+ }
+
+ log_add (log_Info, "SpliceMultiTrack(): loading...");
+ for (tracks = 0; *TrackNames && tracks < MAX_MULTI_TRACKS; TrackNames++, tracks++)
+ {
+ track_decs[tracks] = SoundDecoder_Load (contentDir, *TrackNames,
+ 32768, 0, - 3 * TEXT_SPEED);
+ if (track_decs[tracks])
+ {
+ log_add (log_Info, " track: %s, decoder: %s, rate %d format %x",
+ *TrackNames,
+ SoundDecoder_GetName (track_decs[tracks]),
+ track_decs[tracks]->frequency,
+ track_decs[tracks]->format);
+ SoundDecoder_DecodeAll (track_decs[tracks]);
+
+ chunks_tail->next = create_SoundChunk (track_decs[tracks], sound_sample->length);
+ chunks_tail = chunks_tail->next;
+ chunks_tail->track_num = track_count - 1;
+ sound_sample->length += track_decs[tracks]->length;
+ }
+ else
+ {
+ log_add (log_Warning, "SpliceMultiTrack(): couldn't load %s\n",
+ *TrackNames);
+ tracks--;
+ }
+ }
+ track_decs[tracks] = 0; // term
+
+ if (tracks == 0)
+ {
+ log_add (log_Warning, "SpliceMultiTrack(): no tracks loaded");
+ return;
+ }
+
+ slen1 = strlen (last_sub->text);
+ slen2 = strlen (TrackText);
+ last_sub->text = HRealloc (last_sub->text, slen1 + slen2 + 1);
+ strcpy (last_sub->text + slen1, TrackText);
+
+ no_page_break = 1;
+}
+
+// XXX: This code and the entire trackplayer are begging to be overhauled
+void
+SpliceTrack (UNICODE *TrackName, UNICODE *TrackText, UNICODE *TimeStamp, CallbackFunction cb)
+{
+ static UNICODE last_track_name[128] = "";
+ static unsigned long dec_offset = 0;
+#define MAX_PAGES 50
+ UNICODE *pages[MAX_PAGES];
+ sint32 time_stamps[MAX_PAGES];
+ int num_pages;
+ int page;
+
+ if (!TrackText)
+ return;
+
+ if (!TrackName)
+ { // Appending a piece of subtitles to the last track
+ int slen1, slen2;
+
+ if (track_count == 0)
+ {
+ log_add (log_Warning, "SpliceTrack(): Tried to append a subtitle,"
+ " but no current track");
+ return;
+ }
+
+ if (!last_sub || !last_sub->text)
+ {
+ log_add (log_Warning, "SpliceTrack(): Tried to append a subtitle"
+ " to a NULL string");
+ return;
+ }
+
+ num_pages = SplitSubPages (TrackText, pages, time_stamps, MAX_PAGES);
+ if (num_pages == 0)
+ {
+ log_add (log_Warning, "SpliceTrack(): Failed to parse subtitles");
+ return;
+ }
+ // The last page's stamp is a suggested value. The track should
+ // actually play to the end.
+ time_stamps[num_pages - 1] = -time_stamps[num_pages - 1];
+
+ // Add the first piece to the last subtitle page
+ slen1 = strlen (last_sub->text);
+ slen2 = strlen (pages[0]);
+ last_sub->text = HRealloc (last_sub->text, slen1 + slen2 + 1);
+ strcpy (last_sub->text + slen1, pages[0]);
+ HFree (pages[0]);
+
+ // Add the rest of the pages
+ for (page = 1; page < num_pages; ++page)
+ {
+ TFB_SoundChunk *next_sub = find_next_page (last_sub);
+ if (next_sub)
+ { // nodes prepared by previous call, just fill in the subs
+ next_sub->text = pages[page];
+ last_sub = next_sub;
+ }
+ else
+ { // probably no timestamps were provided, so need more work
+ TFB_SoundDecoder *decoder = SoundDecoder_Load (contentDir,
+ last_track_name, 4096, dec_offset, time_stamps[page]);
+ if (!decoder)
+ {
+ log_add (log_Warning, "SpliceTrack(): couldn't load %s", TrackName);
+ break;
+ }
+ dec_offset += (unsigned long)(decoder->length * 1000);
+ chunks_tail->next = create_SoundChunk (decoder, sound_sample->length);
+ chunks_tail = chunks_tail->next;
+ chunks_tail->tag_me = 1;
+ chunks_tail->track_num = track_count - 1;
+ chunks_tail->text = pages[page];
+ chunks_tail->callback = cb;
+ // TODO: We may have to tag only one page with a callback
+ //cb = NULL;
+ last_sub = chunks_tail;
+ sound_sample->length += decoder->length;
+ }
+ }
+ }
+ else
+ { // Adding a new track
+ int num_timestamps = 0;
+
+ utf8StringCopy (last_track_name, sizeof (last_track_name), TrackName);
+
+ num_pages = SplitSubPages (TrackText, pages, time_stamps, MAX_PAGES);
+ if (num_pages == 0)
+ {
+ log_add (log_Warning, "SpliceTrack(): Failed to parse sutitles");
+ return;
+ }
+ // The last page's stamp is a suggested value. The track should
+ // actually play to the end.
+ time_stamps[num_pages - 1] = -time_stamps[num_pages - 1];
+
+ if (no_page_break && track_count)
+ {
+ int slen1, slen2;
+
+ slen1 = strlen (last_sub->text);
+ slen2 = strlen (pages[0]);
+ last_sub->text = HRealloc (last_sub->text, slen1 + slen2 + 1);
+ strcpy (last_sub->text + slen1, pages[0]);
+ HFree (pages[0]);
+ }
+ else
+ track_count++;
+
+ log_add (log_Info, "SpliceTrack(): loading %s", TrackName);
+
+ if (TimeStamp)
+ {
+ num_timestamps = GetTimeStamps (TimeStamp, time_stamps) + 1;
+ if (num_timestamps < num_pages)
+ {
+ log_add (log_Warning, "SpliceTrack(): number of timestamps"
+ " doesn't match number of pages!");
+ }
+ else if (num_timestamps > num_pages)
+ { // We most likely will get more subtitles appended later
+ // Set the last page to the rest of the track
+ time_stamps[num_timestamps - 1] = -100000;
+ }
+ }
+ else
+ {
+ num_timestamps = num_pages;
+ }
+
+ // Reset the offset for the new track
+ dec_offset = 0;
+ for (page = 0; page < num_timestamps; ++page)
+ {
+ TFB_SoundDecoder *decoder = SoundDecoder_Load (contentDir,
+ TrackName, 4096, dec_offset, time_stamps[page]);
+ if (!decoder)
+ {
+ log_add (log_Warning, "SpliceTrack(): couldn't load %s", TrackName);
+ break;
+ }
+
+ if (!sound_sample)
+ {
+ sound_sample = TFB_CreateSoundSample (NULL, 8, &trackCBs);
+ chunks_head = create_SoundChunk (decoder, 0.0);
+ chunks_tail = chunks_head;
+ }
+ else
+ {
+ chunks_tail->next = create_SoundChunk (decoder, sound_sample->length);
+ chunks_tail = chunks_tail->next;
+ }
+ dec_offset += (unsigned long)(decoder->length * 1000);
+#if 0
+ log_add (log_Debug, "page (%d of %d): %d ts: %d",
+ page, num_pages,
+ dec_offset, time_stamps[page]);
+#endif
+ sound_sample->length += decoder->length;
+ chunks_tail->track_num = track_count - 1;
+ if (!no_page_break)
+ {
+ chunks_tail->tag_me = 1;
+ if (page < num_pages)
+ {
+ chunks_tail->text = pages[page];
+ last_sub = chunks_tail;
+ }
+ chunks_tail->callback = cb;
+ // TODO: We may have to tag only one page with a callback
+ //cb = NULL;
+ }
+ no_page_break = 0;
+ }
+ }
+}
+
+// This function figures out the chunk that should be playing based on
+// 'offset' into the total playing time of all tracks. It then sets
+// the speech source's sample to the necessary decoder and seeks the
+// decoder to the proper point.
+// XXX: This means that whatever speech has already been queued on the
+// source will continue playing, so we may need some small timing
+// adjustments. It may be simpler to just call PlayStream().
+static void
+seek_track (sint32 offset)
+{
+ TFB_SoundChunk *cur;
+ TFB_SoundChunk *last_tag = NULL;
+
+ if (!sound_sample)
+ return; // nothing to recompute
+
+ if (offset < 0)
+ offset = 0;
+ else if ((uint32)offset > tracks_length)
+ offset = tracks_length + 1;
+
+ // Adjusting the stream start time is the only way we can arbitrarily
+ // seek the stream right now
+ soundSource[SPEECH_SOURCE].start_time = GetTimeCounter () - offset;
+
+ // Find the chunk that should be playing at this time offset
+ for (cur = chunks_head; cur && offset >= chunk_end_time (cur);
+ cur = cur->next)
+ {
+ // .. looking for the last callback as we go along
+ // XXX: this effectively set the last point where Fot is looking at.
+ // TODO: this should be somehow changed if we implement more
+ // callbacks, like Melnorme trading, offloading at Starbase, etc.
+ if (cur->tag_me)
+ last_tag = cur;
+ }
+
+ if (cur)
+ {
+ cur_chunk = cur;
+ SoundDecoder_Seek (cur->decoder, (uint32) (((float)offset / ONE_SECOND
+ - cur->start_time) * 1000));
+ sound_sample->decoder = cur->decoder;
+
+ if (cur->tag_me)
+ last_tag = cur;
+ if (last_tag)
+ DoTrackTag (last_tag);
+ }
+ else
+ { // The offset is beyond the length of all tracks
+ StopStream (SPEECH_SOURCE);
+ cur_chunk = NULL;
+ cur_sub_chunk = NULL;
+ }
+}
+
+static sint32
+get_current_track_pos (void)
+{
+ sint32 start_time = soundSource[SPEECH_SOURCE].start_time;
+ sint32 pos = GetTimeCounter () - start_time;
+ if (pos < 0)
+ pos = 0;
+ else if ((uint32)pos > tracks_length)
+ pos = tracks_length;
+ return pos;
+}
+
+void
+FastReverse_Smooth (void)
+{
+ sint32 offset;
+
+ if (!sound_sample)
+ return; // nothing is playing, so.. bye!
+
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+
+ offset = get_current_track_pos ();
+ offset -= ACCEL_SCROLL_SPEED;
+ seek_track (offset);
+
+ // Restart the stream in case it ended previously
+ if (!PlayingStream (SPEECH_SOURCE))
+ PlayStream (sound_sample, SPEECH_SOURCE, false, true, false);
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+}
+
+void
+FastForward_Smooth (void)
+{
+ sint32 offset;
+
+ if (!sound_sample)
+ return; // nothing is playing, so.. bye!
+
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+
+ offset = get_current_track_pos ();
+ offset += ACCEL_SCROLL_SPEED;
+ seek_track (offset);
+
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+}
+
+void
+FastReverse_Page (void)
+{
+ TFB_SoundChunk *prev;
+
+ if (!sound_sample)
+ return; // nothing is playing, so.. bye!
+
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+ prev = find_prev_page (cur_sub_chunk);
+ if (prev)
+ { // Set the chunk to be played
+ cur_chunk = prev;
+ cur_sub_chunk = prev;
+ // Decoder will be set in OnStreamStart()
+ PlayStream (sound_sample, SPEECH_SOURCE, false, true, true);
+ }
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+}
+
+void
+FastForward_Page (void)
+{
+ TFB_SoundChunk *next;
+
+ if (!sound_sample)
+ return; // nothing is playing, so.. bye!
+
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+ next = find_next_page (cur_sub_chunk);
+ if (next)
+ { // Set the chunk to be played
+ cur_chunk = next;
+ cur_sub_chunk = next;
+ // Decoder will be set in OnStreamStart()
+ PlayStream (sound_sample, SPEECH_SOURCE, false, true, true);
+ }
+ else
+ { // End of the tracks (pun intended)
+ seek_track (tracks_length + 1);
+ }
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+}
+
+// Tells current position of streaming speech in the units
+// specified by the caller.
+// This is normally called on the ambient_anim_task thread.
+int
+GetTrackPosition (int in_units)
+{
+ uint32 offset;
+ uint32 length = tracks_length;
+ // detach from the static one, otherwise, we can race for
+ // it and thus divide by 0
+
+ if (!sound_sample || length == 0)
+ return 0; // nothing is playing
+
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+ offset = get_current_track_pos ();
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+
+ return in_units * offset / length;
+}
+
+TFB_SoundChunk *
+create_SoundChunk (TFB_SoundDecoder *decoder, float start_time)
+{
+ TFB_SoundChunk *chunk;
+ chunk = HCalloc (sizeof (*chunk));
+ chunk->decoder = decoder;
+ chunk->start_time = start_time;
+ return chunk;
+}
+
+void
+destroy_SoundChunk_list (TFB_SoundChunk *chunk)
+{
+ TFB_SoundChunk *next = NULL;
+ for ( ; chunk; chunk = next)
+ {
+ next = chunk->next;
+ if (chunk->decoder)
+ SoundDecoder_Free (chunk->decoder);
+ HFree (chunk->text);
+ HFree (chunk);
+ }
+}
+
+// Returns the next chunk with a subtitle
+TFB_SoundChunk *
+find_next_page (TFB_SoundChunk *cur)
+{
+ if (!cur)
+ return NULL;
+ for (cur = cur->next; cur && !cur->tag_me; cur = cur->next)
+ ;
+ return cur;
+}
+
+// Returns the previous chunk with a subtitle.
+// cur == 0 is treated as end of the list.
+TFB_SoundChunk *
+find_prev_page (TFB_SoundChunk *cur)
+{
+ TFB_SoundChunk *prev;
+ TFB_SoundChunk *last_valid = chunks_head;
+
+ if (cur == chunks_head)
+ return cur; // cannot go below the first track
+
+ for (prev = chunks_head; prev && prev != cur; prev = prev->next)
+ {
+ if (prev->tag_me)
+ last_valid = prev;
+ }
+ return last_valid;
+}
+
+
+// External access to the chunks list
+SUBTITLE_REF
+GetFirstTrackSubtitle (void)
+{
+ return chunks_head;
+}
+
+// External access to the chunks list
+SUBTITLE_REF
+GetNextTrackSubtitle (SUBTITLE_REF LastRef)
+{
+ if (!LastRef)
+ return NULL; // enumeration already ended
+
+ return find_next_page (LastRef);
+}
+
+// External access to the chunk subtitles
+const UNICODE *
+GetTrackSubtitleText (SUBTITLE_REF SubRef)
+{
+ if (!SubRef)
+ return NULL;
+
+ return SubRef->text;
+}
+
+// External access to currently active subtitle text
+// Returns NULL is none is active
+const UNICODE *
+GetTrackSubtitle (void)
+{
+ const UNICODE *cur_sub = NULL;
+
+ if (!sound_sample)
+ return NULL; // not playing anything
+
+ LockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+ if (cur_sub_chunk)
+ cur_sub = cur_sub_chunk->text;
+ UnlockMutex (soundSource[SPEECH_SOURCE].stream_mutex);
+
+ return cur_sub;
+}