Mercurial > audlegacy
view Plugins/Input/vorbis/vorbis.c @ 553:82346a6312c2 trunk
[svn] make WMA plugin strings translatable
author | nenolod |
---|---|
date | Wed, 01 Feb 2006 13:09:00 -0800 |
parents | e85198a7f34d |
children | d539e5c5f730 |
line wrap: on
line source
/* * Copyright (C) Tony Arcieri <bascule@inferno.tusculum.edu> * Copyright (C) 2001-2002 Haavard Kvaalen <havardk@xmms.org> * * ReplayGain processing Copyright (C) 2002 Gian-Carlo Pascutto <gcp@sjeng.org> * * 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. * */ /* * 2002-01-11 ReplayGain processing added by Gian-Carlo Pascutto <gcp@sjeng.org> */ /* * Note that this uses vorbisfile, which is not (currently) * thread-safe. */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include <glib.h> #include <glib/gi18n.h> #include <gtk/gtk.h> #include <stdlib.h> #include <math.h> #include <string.h> #include <fcntl.h> #include <ogg/ogg.h> #include <vorbis/codec.h> #include <vorbis/vorbisfile.h> #include "audacious/plugin.h" #include "audacious/output.h" #include "libaudacious/util.h" #include "libaudacious/configdb.h" #include "libaudacious/titlestring.h" #include "vorbis.h" #include "http.h" extern vorbis_config_t vorbis_cfg; static int vorbis_check_file(char *filename); static void vorbis_play(char *filename); static void vorbis_stop(void); static void vorbis_pause(short p); static void vorbis_seek(int time); static int vorbis_time(void); static void vorbis_get_song_info(char *filename, char **title, int *length); static gchar *vorbis_generate_title(OggVorbis_File * vorbisfile, const gchar * fn); static void vorbis_aboutbox(void); static void vorbis_init(void); static void vorbis_cleanup(void); static long vorbis_process_replaygain(float **pcm, int samples, int ch, char *pcmout, float rg_scale); static gboolean vorbis_update_replaygain(float *scale); static size_t ovcb_read(void *ptr, size_t size, size_t nmemb, void *datasource); static int ovcb_seek(void *datasource, int64_t offset, int whence); static int ovcb_close(void *datasource); static long ovcb_tell(void *datasource); ov_callbacks vorbis_callbacks = { ovcb_read, ovcb_seek, ovcb_close, ovcb_tell }; InputPlugin vorbis_ip = { NULL, NULL, NULL, /* description */ vorbis_init, /* init */ vorbis_aboutbox, /* aboutbox */ vorbis_configure, /* configure */ vorbis_check_file, /* is_our_file */ NULL, vorbis_play, vorbis_stop, vorbis_pause, vorbis_seek, NULL, /* set eq */ vorbis_time, NULL, NULL, vorbis_cleanup, NULL, NULL, NULL, NULL, vorbis_get_song_info, vorbis_file_info_box, /* file info box, tag editing */ NULL, }; static OggVorbis_File vf; static GThread *thread; int vorbis_playing = 0; static int vorbis_eos = 0; static int vorbis_is_streaming = 0; static int vorbis_bytes_streamed = 0; static volatile int seekneeded = -1; static int samplerate, channels; GMutex *vf_mutex; static gboolean output_error; gchar **vorbis_tag_encoding_list = NULL; InputPlugin * get_iplugin_info(void) { vorbis_ip.description = g_strdup_printf(_("Ogg Vorbis Audio Plugin")); return &vorbis_ip; } static int vorbis_check_file(char *filename) { VFSFile *stream; OggVorbis_File vfile; /* avoid thread interaction */ char *ext; gint result; /* is this our http resource? */ if (strncasecmp(filename, "http://", 7) == 0) { ext = strrchr(filename, '.'); if (ext) { if (!strncasecmp(ext, ".ogg", 4)) { return TRUE; } } return FALSE; } if (!(stream = vfs_fopen(filename, "r"))) { return FALSE; } /* * The open function performs full stream detection and machine * initialization. If it returns zero, the stream *is* Vorbis and * we're fully ready to decode. */ /* libvorbisfile isn't thread safe... */ memset(&vfile, 0, sizeof(vfile)); g_mutex_lock(vf_mutex); result = ov_test_callbacks(stream, &vfile, NULL, 0, vorbis_callbacks); switch (result) { case OV_EREAD: #ifdef DEBUG g_message("** vorbis.c: Media read error: %s", filename); #endif g_mutex_unlock(vf_mutex); vfs_fclose(stream); return FALSE; break; case OV_ENOTVORBIS: #ifdef DEBUG g_message("** vorbis.c: Not Vorbis data: %s", filename); #endif g_mutex_unlock(vf_mutex); vfs_fclose(stream); return FALSE; break; case OV_EVERSION: #ifdef DEBUG g_message("** vorbis.c: Version mismatch: %s", filename); #endif g_mutex_unlock(vf_mutex); vfs_fclose(stream); return FALSE; break; case OV_EBADHEADER: #ifdef DEBUG g_message("** vorbis.c: Invalid Vorbis bistream header: %s", filename); #endif g_mutex_unlock(vf_mutex); vfs_fclose(stream); return FALSE; break; case OV_EFAULT: #ifdef DEBUG g_message("** vorbis.c: Internal logic fault while reading %s", filename); #endif g_mutex_unlock(vf_mutex); vfs_fclose(stream); return FALSE; break; case 0: break; default: break; } ov_clear(&vfile); /* once the ov_open succeeds, the stream belongs to vorbisfile.a. ov_clear will fclose it */ g_mutex_unlock(vf_mutex); return TRUE; } static void vorbis_jump_to_time(long time) { g_mutex_lock(vf_mutex); /* * We need to guard against seeking to the end, or things * don't work right. Instead, just seek to one second prior * to this */ if (time == ov_time_total(&vf, -1)) time--; vorbis_ip.output->flush(time * 1000); ov_time_seek(&vf, time); g_mutex_unlock(vf_mutex); } static void do_seek(void) { if (seekneeded != -1 && !vorbis_is_streaming) { vorbis_jump_to_time(seekneeded); seekneeded = -1; vorbis_eos = FALSE; } } static int vorbis_process_data(int last_section, gboolean use_rg, float rg_scale) { char pcmout[4096]; int bytes; float **pcm; /* * A vorbis physical bitstream may consist of many logical * sections (information for each of which may be fetched from * the vf structure). This value is filled in by ov_read to * alert us what section we're currently decoding in case we * need to change playback settings at a section boundary */ int current_section; g_mutex_lock(vf_mutex); if (use_rg) { bytes = ov_read_float(&vf, &pcm, sizeof(pcmout) / 2 / channels, ¤t_section); if (bytes > 0) bytes = vorbis_process_replaygain(pcm, bytes, channels, pcmout, rg_scale); } else { bytes = ov_read(&vf, pcmout, sizeof(pcmout), (int) (G_BYTE_ORDER == G_BIG_ENDIAN), 2, 1, ¤t_section); } switch (bytes) { case 0: /* EOF */ g_mutex_unlock(vf_mutex); vorbis_ip.output->buffer_free(); vorbis_ip.output->buffer_free(); vorbis_eos = TRUE; return last_section; case OV_HOLE: case OV_EBADLINK: /* * error in the stream. Not a problem, just * reporting it in case we (the app) cares. * In this case, we don't. */ g_mutex_unlock(vf_mutex); return last_section; } if (current_section != last_section) { /* * The info struct is different in each section. vf * holds them all for the given bitstream. This * requests the current one */ vorbis_info *vi = ov_info(&vf, -1); if (vi->channels > 2) { vorbis_eos = TRUE; g_mutex_unlock(vf_mutex); return current_section; } if (vi->rate != samplerate || vi->channels != channels) { samplerate = vi->rate; channels = vi->channels; vorbis_ip.output->buffer_free(); vorbis_ip.output->buffer_free(); vorbis_ip.output->close_audio(); if (!vorbis_ip.output-> open_audio(FMT_S16_NE, vi->rate, vi->channels)) { output_error = TRUE; vorbis_eos = TRUE; g_mutex_unlock(vf_mutex); return current_section; } vorbis_ip.output->flush(ov_time_tell(&vf) * 1000); } } g_mutex_unlock(vf_mutex); if (!vorbis_playing) return current_section; if (seekneeded != -1) do_seek(); produce_audio(vorbis_ip.output->written_time(), FMT_S16_NE, channels, bytes, pcmout, &vorbis_playing); return current_section; } static gpointer vorbis_play_loop(gpointer arg) { char *filename = (char *) arg; gchar *title = NULL; double time; long timercount = 0; vorbis_info *vi; int last_section = -1; VFSFile *stream = NULL; void *datasource = NULL; gboolean use_rg; float rg_scale = 1.0; memset(&vf, 0, sizeof(vf)); if (strncasecmp("http://", filename, 7) != 0) { /* file is a real file */ if ((stream = vfs_fopen(filename, "r")) == NULL) { vorbis_eos = TRUE; goto play_cleanup; } datasource = (void *) stream; } else { /* file is a stream */ vorbis_is_streaming = 1; vorbis_http_open(filename); datasource = "NULL"; } /* * The open function performs full stream detection and * machine initialization. None of the rest of ov_xx() works * without it */ g_mutex_lock(vf_mutex); if (ov_open_callbacks(datasource, &vf, NULL, 0, vorbis_callbacks) < 0) { vorbis_callbacks.close_func(datasource); g_mutex_unlock(vf_mutex); vorbis_eos = TRUE; goto play_cleanup; } vi = ov_info(&vf, -1); if (vorbis_is_streaming) time = -1; else time = ov_time_total(&vf, -1) * 1000; if (vi->channels > 2) { vorbis_eos = TRUE; g_mutex_unlock(vf_mutex); goto play_cleanup; } samplerate = vi->rate; channels = vi->channels; title = vorbis_generate_title(&vf, filename); use_rg = vorbis_update_replaygain(&rg_scale); vorbis_ip.set_info(title, time, ov_bitrate(&vf, -1), samplerate, channels); if (!vorbis_ip.output->open_audio(FMT_S16_NE, vi->rate, vi->channels)) { output_error = TRUE; g_mutex_unlock(vf_mutex); goto play_cleanup; } g_mutex_unlock(vf_mutex); seekneeded = -1; /* * Note that chaining changes things here; A vorbis file may * be a mix of different channels, bitrates and sample rates. * You can fetch the information for any section of the file * using the ov_ interface. */ while (vorbis_playing) { int current_section; if (seekneeded != -1) do_seek(); if (vorbis_eos) { xmms_usleep(20000); continue; } current_section = vorbis_process_data(last_section, use_rg, rg_scale); if (current_section != last_section) { /* * set total play time, bitrate, rate, and channels of * current section */ if (title) g_free(title); g_mutex_lock(vf_mutex); title = vorbis_generate_title(&vf, filename); use_rg = vorbis_update_replaygain(&rg_scale); if (vorbis_is_streaming) time = -1; else time = ov_time_total(&vf, -1) * 1000; vorbis_ip.set_info(title, time, ov_bitrate(&vf, current_section), samplerate, channels); g_mutex_unlock(vf_mutex); timercount = vorbis_ip.output->output_time(); last_section = current_section; } if (!(vi->bitrate_upper == vi->bitrate_lower == vi->bitrate_nominal) && (vorbis_ip.output->output_time() > timercount + 1000 || vorbis_ip.output->output_time() < timercount)) { /* * simple hack to avoid updating too * often */ long br; g_mutex_lock(vf_mutex); br = ov_bitrate_instant(&vf); g_mutex_unlock(vf_mutex); if (br > 0) vorbis_ip.set_info(title, time, br, samplerate, channels); timercount = vorbis_ip.output->output_time(); } } if (!output_error) vorbis_ip.output->close_audio(); /* fall through intentional */ play_cleanup: g_free(title); g_free(filename); /* * ov_clear closes the stream if its open. Safe to call on an * uninitialized structure as long as we've zeroed it */ g_mutex_lock(vf_mutex); ov_clear(&vf); g_mutex_unlock(vf_mutex); vorbis_is_streaming = 0; return NULL; } static void vorbis_play(char *filename) { vorbis_playing = 1; vorbis_bytes_streamed = 0; vorbis_eos = 0; output_error = FALSE; thread = g_thread_create(vorbis_play_loop, g_strdup(filename), TRUE, NULL); } static void vorbis_stop(void) { if (vorbis_playing) { vorbis_playing = 0; g_thread_join(thread); } } static void vorbis_pause(short p) { vorbis_ip.output->pause(p); } static int vorbis_time(void) { if (output_error) return -2; if (vorbis_eos && !vorbis_ip.output->buffer_playing()) return -1; return vorbis_ip.output->output_time(); } static gchar * convert_tag_title(gchar * title) { gchar **encoding = vorbis_tag_encoding_list; gchar *new_title = NULL; if (g_utf8_validate(title, -1, NULL)) return title; while (*encoding && !new_title) { new_title = g_convert(title, strlen(title), "UTF-8", *encoding++, NULL, NULL, NULL); } if (new_title) { g_free(title); return new_title; } /* FIXME: We're relying on BMP core to provide fallback * conversion */ return title; } static void vorbis_seek(int time) { if (vorbis_is_streaming) return; seekneeded = time; while (seekneeded != -1) xmms_usleep(20000); } static void vorbis_get_song_info(char *filename, char **title, int *length) { VFSFile *stream; OggVorbis_File vf; /* avoid thread interaction */ if (strncasecmp(filename, "http://", 7)) { if ((stream = vfs_fopen(filename, "r")) == NULL) return; /* * The open function performs full stream detection and * machine initialization. If it returns zero, the stream * *is* Vorbis and we're fully ready to decode. */ g_mutex_lock(vf_mutex); if (ov_open_callbacks(stream, &vf, NULL, 0, vorbis_callbacks) < 0) { g_mutex_unlock(vf_mutex); vfs_fclose(stream); return; } /* Retrieve the length */ *length = ov_time_total(&vf, -1) * 1000; *title = NULL; *title = vorbis_generate_title(&vf, filename); /* * once the ov_open succeeds, the stream belongs to * vorbisfile.a. ov_clear will fclose it */ ov_clear(&vf); g_mutex_unlock(vf_mutex); } else { /* streaming song info */ *length = -1; *title = (char *) vorbis_http_get_title(filename); /* Encoding patch */ if (vorbis_cfg.title_encoding_enabled) *title = convert_tag_title(*title); /* Encoding patch */ } } static const gchar * get_extension(const gchar * filename) { const gchar *ext; if ((ext = strrchr(filename, '.'))) ++ext; return ext; } /* Make sure you've locked vf_mutex */ static gboolean vorbis_update_replaygain(float *scale) { vorbis_comment *comment; char *rg_gain = NULL, *rg_peak_str = NULL; float rg_peak; if (!vorbis_cfg.use_replaygain && !vorbis_cfg.use_anticlip) return FALSE; if ((comment = ov_comment(&vf, -1)) == NULL) return FALSE; *scale = 1.0; if (vorbis_cfg.use_replaygain) { if (vorbis_cfg.replaygain_mode == REPLAYGAIN_MODE_ALBUM) { rg_gain = vorbis_comment_query(comment, "replaygain_album_gain", 0); if (!rg_gain) rg_gain = vorbis_comment_query(comment, "rg_audiophile", 0); /* Old */ } if (!rg_gain) rg_gain = vorbis_comment_query(comment, "replaygain_track_gain", 0); if (!rg_gain) rg_gain = vorbis_comment_query(comment, "rg_radio", 0); /* Old */ /* FIXME: Make sure this string is the correct format first? */ if (rg_gain) *scale = pow(10., atof(rg_gain) / 20); } if (vorbis_cfg.use_anticlip) { if (vorbis_cfg.replaygain_mode == REPLAYGAIN_MODE_ALBUM) rg_peak_str = vorbis_comment_query(comment, "replaygain_album_peak", 0); if (!rg_peak_str) rg_peak_str = vorbis_comment_query(comment, "replaygain_track_peak", 0); if (!rg_peak_str) rg_peak_str = vorbis_comment_query(comment, "rg_peak", 0); /* Old */ if (rg_peak_str) rg_peak = atof(rg_peak_str); else rg_peak = 1; if (*scale * rg_peak > 1.0) *scale = 1.0 / rg_peak; } if (*scale != 1.0 || vorbis_cfg.use_booster) { /* safety */ if (*scale > 15.0) *scale = 15.0; return TRUE; } return FALSE; } #if (G_BYTE_ORDER == G_BIG_ENDIAN) # define GET_BYTE1(val) ((val) >> 8) # define GET_BYTE2(val) ((val) & 0xff) #else # define GET_BYTE1(val) ((val) & 0xff) # define GET_BYTE2(val) ((val) >> 8) #endif static long vorbis_process_replaygain(float **pcm, int samples, int ch, char *pcmout, float rg_scale) { int i, j; /* ReplayGain processing */ for (i = 0; i < samples; i++) for (j = 0; j < ch; j++) { float sample = pcm[j][i] * rg_scale; int value; if (vorbis_cfg.use_booster) { sample *= 2; /* hard 6dB limiting */ if (sample < -0.5) sample = tanh((sample + 0.5) / 0.5) * 0.5 - 0.5; else if (sample > 0.5) sample = tanh((sample - 0.5) / 0.5) * 0.5 + 0.5; } value = sample * 32767; if (value > 32767) value = 32767; else if (value < -32767) value = -32767; *pcmout++ = GET_BYTE1(value); *pcmout++ = GET_BYTE2(value); } return 2 * ch * samples; } static gchar * vorbis_generate_title(OggVorbis_File * vorbisfile, const gchar * filename) { /* Caller should hold vf_mutex */ gchar *displaytitle = NULL; vorbis_comment *comment; TitleInput *input; input = bmp_title_input_new(); input->file_name = g_path_get_basename(filename); input->file_ext = get_extension(filename); input->file_path = g_path_get_dirname(filename); if ((comment = ov_comment(vorbisfile, -1))) { input->track_name = g_strdup(vorbis_comment_query(comment, "title", 0)); input->performer = g_strdup(vorbis_comment_query(comment, "artist", 0)); input->album_name = g_strdup(vorbis_comment_query(comment, "album", 0)); if (vorbis_comment_query(comment, "tracknumber", 0)) input->track_number = atoi(vorbis_comment_query(comment, "tracknumber", 0)); input->date = g_strdup(vorbis_comment_query(comment, "date", 0)); input->genre = g_strdup(vorbis_comment_query(comment, "genre", 0)); input->comment = g_strdup(vorbis_comment_query(comment, "comment", 0)); } if (!(displaytitle = xmms_get_titlestring(vorbis_cfg.tag_override ? vorbis_cfg.tag_format : xmms_get_gentitle_format(), input))) { if (!vorbis_is_streaming) displaytitle = g_strdup(input->file_name); else displaytitle = vorbis_http_get_title(filename); } g_free(input->file_name); g_free(input->file_path); g_free(input->track_name); g_free(input->performer); g_free(input->album_name); g_free(input->date); g_free(input->genre); g_free(input->comment); g_free(input); /* Encoding patch */ if (vorbis_cfg.title_encoding_enabled) displaytitle = convert_tag_title(displaytitle); /* Encoding patch */ return displaytitle; } static void vorbis_aboutbox() { static GtkWidget *about_window; if (about_window) gdk_window_raise(about_window->window); about_window = xmms_show_message(_("About Ogg Vorbis Audio Plugin"), /* * I18N: UTF-8 Translation: "Haavard Kvaalen" -> * "H\303\245vard Kv\303\245len" */ _ ("Ogg Vorbis Plugin by the Xiph.org Foundation\n\n" "Original code by\n" "Tony Arcieri <bascule@inferno.tusculum.edu>\n" "Contributions from\n" "Chris Montgomery <monty@xiph.org>\n" "Peter Alm <peter@xmms.org>\n" "Michael Smith <msmith@labyrinth.edu.au>\n" "Jack Moffitt <jack@icecast.org>\n" "Jorn Baayen <jorn@nl.linux.org>\n" "Haavard Kvaalen <havardk@xmms.org>\n" "Gian-Carlo Pascutto <gcp@sjeng.org>\n\n" "Visit the Xiph.org Foundation at http://www.xiph.org/\n"), _("Ok"), FALSE, NULL, NULL); g_signal_connect(G_OBJECT(about_window), "destroy", G_CALLBACK(gtk_widget_destroyed), &about_window); } static void vorbis_init(void) { ConfigDb *db; memset(&vorbis_cfg, 0, sizeof(vorbis_config_t)); vorbis_cfg.http_buffer_size = 128; vorbis_cfg.http_prebuffer = 25; vorbis_cfg.proxy_port = 8080; vorbis_cfg.proxy_use_auth = FALSE; vorbis_cfg.proxy_user = NULL; vorbis_cfg.proxy_pass = NULL; vorbis_cfg.tag_override = FALSE; vorbis_cfg.tag_format = NULL; vorbis_cfg.use_anticlip = FALSE; vorbis_cfg.use_replaygain = FALSE; vorbis_cfg.replaygain_mode = REPLAYGAIN_MODE_TRACK; vorbis_cfg.use_booster = FALSE; /* Encoding patch */ vorbis_cfg.title_encoding_enabled = FALSE; vorbis_cfg.title_encoding = NULL; /* Encoding patch */ db = bmp_cfg_db_open(); bmp_cfg_db_get_int(db, "vorbis", "http_buffer_size", &vorbis_cfg.http_buffer_size); bmp_cfg_db_get_int(db, "vorbis", "http_prebuffer", &vorbis_cfg.http_prebuffer); bmp_cfg_db_get_bool(db, "vorbis", "save_http_stream", &vorbis_cfg.save_http_stream); if (!bmp_cfg_db_get_string(db, "vorbis", "save_http_path", &vorbis_cfg.save_http_path)) vorbis_cfg.save_http_path = g_strdup(g_get_home_dir()); bmp_cfg_db_get_bool(db, "vorbis", "use_proxy", &vorbis_cfg.use_proxy); if (!bmp_cfg_db_get_string (db, "vorbis", "proxy_host", &vorbis_cfg.proxy_host)) vorbis_cfg.proxy_host = g_strdup("localhost"); bmp_cfg_db_get_int(db, "vorbis", "proxy_port", &vorbis_cfg.proxy_port); bmp_cfg_db_get_bool(db, "vorbis", "proxy_use_auth", &vorbis_cfg.proxy_use_auth); bmp_cfg_db_get_string(db, "vorbis", "proxy_user", &vorbis_cfg.proxy_user); bmp_cfg_db_get_string(db, "vorbis", "proxy_pass", &vorbis_cfg.proxy_pass); bmp_cfg_db_get_bool(db, "vorbis", "tag_override", &vorbis_cfg.tag_override); if (!bmp_cfg_db_get_string(db, "vorbis", "tag_format", &vorbis_cfg.tag_format)) vorbis_cfg.tag_format = g_strdup("%p - %t"); bmp_cfg_db_get_bool(db, "vorbis", "use_anticlip", &vorbis_cfg.use_anticlip); bmp_cfg_db_get_bool(db, "vorbis", "use_replaygain", &vorbis_cfg.use_replaygain); bmp_cfg_db_get_int(db, "vorbis", "replaygain_mode", &vorbis_cfg.replaygain_mode); bmp_cfg_db_get_bool(db, "vorbis", "use_booster", &vorbis_cfg.use_booster); /* Encoding patch */ bmp_cfg_db_get_bool(db, "vorbis", "title_encoding_enabled", &vorbis_cfg.title_encoding_enabled); bmp_cfg_db_get_string(db, "vorbis", "title_encoding", &vorbis_cfg.title_encoding); if (vorbis_cfg.title_encoding_enabled) vorbis_tag_encoding_list = g_strsplit_set(vorbis_cfg.title_encoding, ENCODING_SEPARATOR, 0); /* Encoding patch */ bmp_cfg_db_close(db); vf_mutex = g_mutex_new(); } static void vorbis_cleanup(void) { g_strfreev(vorbis_tag_encoding_list); g_mutex_free(vf_mutex); } static size_t ovcb_read(void *ptr, size_t size, size_t nmemb, void *datasource) { size_t tmp; if (vorbis_is_streaming) { /* this is a stream */ tmp = vorbis_http_read(ptr, size * nmemb); vorbis_bytes_streamed += tmp; return tmp; } return vfs_fread(ptr, size, nmemb, (VFSFile *) datasource); } static int ovcb_seek(void *datasource, int64_t offset, int whence) { if (vorbis_is_streaming) { /* this is a stream */ /* streams aren't seekable */ return -1; } return vfs_fseek((VFSFile *) datasource, offset, whence); } static int ovcb_close(void *datasource) { if (vorbis_is_streaming) { /* this is a stream */ vorbis_http_close(); return 0; } return vfs_fclose((VFSFile *) datasource); } static long ovcb_tell(void *datasource) { if (vorbis_is_streaming) { /* this is a stream */ /* return bytes read */ return vorbis_bytes_streamed; } return vfs_ftell((VFSFile *) datasource); }