From 086360352150cd032523090bbd75eb2f4208e6ed Mon Sep 17 00:00:00 2001 From: waker Date: Sat, 13 Nov 2010 21:35:53 +0100 Subject: implemented conversion between streams with different channel masks --- Makefile.am | 1 + deadbeef.h | 9 +--- plugins/alsa/alsa.c | 42 +++++++++++++---- plugins/gtkui/gtkui.c | 4 +- plugins/mpgmad/mpgmad.c | 27 ++++++++++- plugins/supereq/supereq.c | 2 +- premix.c | 90 ++++++++++++++++++++++++++++++++++++ premix.h | 27 +++++++++++ streamer.c | 113 +++++++++++++++++++++++++--------------------- 9 files changed, 241 insertions(+), 74 deletions(-) create mode 100644 premix.c create mode 100644 premix.h diff --git a/Makefile.am b/Makefile.am index 77db2954..cab4aa3b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -21,6 +21,7 @@ deadbeef_SOURCES =\ plugins.c plugins.h moduleconf.h\ playlist.c playlist.h \ streamer.c streamer.h\ + premix.c premix.h\ messagepump.c messagepump.h\ conf.c conf.h\ threading_pthread.c threading.h\ diff --git a/deadbeef.h b/deadbeef.h index 435bd934..93b6ea39 100644 --- a/deadbeef.h +++ b/deadbeef.h @@ -685,12 +685,7 @@ typedef struct DB_decoder_s { // read is called by streamer to decode specified number of bytes // must return number of bytes that were successfully decoded (sample aligned) - - // read_int16 must always output 16 bit signed integer samples - int (*read_int16) (DB_fileinfo_t *info, char *buffer, int size); - - // read_float32 must always output 32 bit floating point samples - int (*read_float32) (DB_fileinfo_t *info, char *buffer, int size); + int (*read) (DB_fileinfo_t *info, char *buffer, int nbytes); int (*seek) (DB_fileinfo_t *info, float seconds); @@ -747,7 +742,7 @@ typedef struct DB_dsp_s { // process gets called before SRC // stereo samples are stored in interleaved format // stereo sample is counted as 1 sample - int (*process_int16) (int16_t *samples, int nsamples, int nch, int bps, int srate); + int (*process) (char * restrict samples, ddb_waveformat_t * restrict fmt); void (*reset) (void); void (*enable) (int e); int (*enabled) (void); diff --git a/plugins/alsa/alsa.c b/plugins/alsa/alsa.c index 837305cd..be744b34 100644 --- a/plugins/alsa/alsa.c +++ b/plugins/alsa/alsa.c @@ -191,7 +191,7 @@ palsa_set_hw_params (ddb_waveformat_t *fmt) { plugin.fmt.samplerate = val; trace ("chosen samplerate: %d Hz\n", val); - if ((err = snd_pcm_hw_params_set_channels (audio, hw_params, fmt->channels)) < 0) { + if ((err = snd_pcm_hw_params_set_channels (audio, hw_params, /*fmt->channels*/6)) < 0) { fprintf (stderr, "cannot set channel count (%s)\n", snd_strerror (err)); goto error; @@ -236,12 +236,34 @@ palsa_set_hw_params (ddb_waveformat_t *fmt) { break; } - plugin.fmt.bps = sample_fmt; plugin.fmt.channels = nchan; plugin.fmt.is_float = 0; plugin.fmt.is_multichannel = 0; plugin.fmt.channelmask = 0; - + if (nchan == 1) { + plugin.fmt.channelmask = DDB_SPEAKER_FRONT_LEFT; + } + if (nchan == 2) { + plugin.fmt.channelmask = DDB_SPEAKER_FRONT_LEFT | DDB_SPEAKER_FRONT_RIGHT; + } + if (nchan == 3) { + plugin.fmt.channelmask = DDB_SPEAKER_FRONT_LEFT | DDB_SPEAKER_FRONT_RIGHT | DDB_SPEAKER_LOW_FREQUENCY; + } + if (nchan == 4) { + plugin.fmt.channelmask = DDB_SPEAKER_FRONT_LEFT | DDB_SPEAKER_FRONT_RIGHT | DDB_SPEAKER_BACK_LEFT | DDB_SPEAKER_BACK_RIGHT; + } + if (nchan == 5) { + plugin.fmt.channelmask = DDB_SPEAKER_FRONT_LEFT | DDB_SPEAKER_FRONT_RIGHT | DDB_SPEAKER_BACK_LEFT | DDB_SPEAKER_BACK_RIGHT | DDB_SPEAKER_FRONT_CENTER; + } + if (nchan == 6) { + plugin.fmt.channelmask = DDB_SPEAKER_FRONT_LEFT | DDB_SPEAKER_FRONT_RIGHT | DDB_SPEAKER_BACK_LEFT | DDB_SPEAKER_BACK_RIGHT | DDB_SPEAKER_FRONT_CENTER | DDB_SPEAKER_LOW_FREQUENCY; + } + if (nchan == 7) { + plugin.fmt.channelmask = DDB_SPEAKER_FRONT_LEFT | DDB_SPEAKER_FRONT_RIGHT | DDB_SPEAKER_BACK_LEFT | DDB_SPEAKER_BACK_RIGHT | DDB_SPEAKER_FRONT_CENTER | DDB_SPEAKER_SIDE_LEFT | DDB_SPEAKER_SIDE_RIGHT; + } + if (nchan == 8) { + plugin.fmt.channelmask = DDB_SPEAKER_FRONT_LEFT | DDB_SPEAKER_FRONT_RIGHT | DDB_SPEAKER_BACK_LEFT | DDB_SPEAKER_BACK_RIGHT | DDB_SPEAKER_FRONT_CENTER | DDB_SPEAKER_SIDE_LEFT | DDB_SPEAKER_SIDE_RIGHT | DDB_SPEAKER_LOW_FREQUENCY; + } error: if (hw_params) { snd_pcm_hw_params_free (hw_params); @@ -512,10 +534,10 @@ palsa_thread (void *context) { break; } err = 0; - char buf[period_size * 4]; - int bytes_to_write = palsa_callback (buf, period_size * 4); + char buf[period_size * (plugin.fmt.bps>>3) * plugin.fmt.channels]; + int bytes_to_write = palsa_callback (buf, period_size * (plugin.fmt.bps>>3) * plugin.fmt.channels); - if ( bytes_to_write >= 4 ) { + if (bytes_to_write >= (plugin.fmt.bps>>3) * plugin.fmt.channels) { err = snd_pcm_writei (audio, buf, snd_pcm_bytes_to_frames(audio, bytes_to_write)); } else { @@ -593,10 +615,10 @@ palsa_callback (char *stream, int len) { ); #else - int16_t ivolume = deadbeef->volume_get_amp () * 1000; - for (int i = 0; i < bytesread/2; i++) { - ((int16_t*)stream)[i] = (int16_t)(((int32_t)(((int16_t*)stream)[i])) * ivolume / 1000); - } +// int16_t ivolume = deadbeef->volume_get_amp () * 1000; +// for (int i = 0; i < bytesread/2; i++) { +// ((int16_t*)stream)[i] = (int16_t)(((int32_t)(((int16_t*)stream)[i])) * ivolume / 1000); +// } #endif return bytesread; } diff --git a/plugins/gtkui/gtkui.c b/plugins/gtkui/gtkui.c index 46b3edf8..a2da9d74 100644 --- a/plugins/gtkui/gtkui.c +++ b/plugins/gtkui/gtkui.c @@ -45,8 +45,8 @@ #include "eq.h" #include "actions.h" -//#define trace(...) { fprintf(stderr, __VA_ARGS__); } -#define trace(fmt,...) +#define trace(...) { fprintf(stderr, __VA_ARGS__); } +//#define trace(fmt,...) static DB_gui_t plugin; DB_functions_t *deadbeef; diff --git a/plugins/mpgmad/mpgmad.c b/plugins/mpgmad/mpgmad.c index 1d5890ac..6b4d96ea 100644 --- a/plugins/mpgmad/mpgmad.c +++ b/plugins/mpgmad/mpgmad.c @@ -654,6 +654,7 @@ cmp3_init (DB_fileinfo_t *_info, DB_playItem_t *it) { _info->fmt.bps = info->buffer.bitspersample; _info->fmt.samplerate = info->buffer.samplerate; _info->fmt.channels = info->buffer.channels; + _info->fmt.channelmask = _info->fmt.channels == 1 ? DDB_SPEAKER_FRONT_LEFT : (DDB_SPEAKER_FRONT_LEFT | DDB_SPEAKER_FRONT_RIGHT); mad_stream_init(&info->stream); mad_stream_options (&info->stream, MAD_OPTION_IGNORECRC); @@ -936,6 +937,28 @@ cmp3_free (DB_fileinfo_t *_info) { free (info); } +static int +cmp3_read (DB_fileinfo_t *_info, char *bytes, int size) { +#if WRITE_DUMP + if (!out) { + out = fopen ("out.raw", "w+b"); + } +#endif + mpgmad_info_t *info = (mpgmad_info_t *)_info; + info->buffer.readsize = size; + info->buffer.out = bytes; + cmp3_decode_int16 (info); + info->buffer.currentsample += (size - info->buffer.readsize) / 4; + _info->readpos = (float)(info->buffer.currentsample - info->buffer.startsample) / info->buffer.samplerate; +#if WRITE_DUMP + if (size - info->buffer.readsize > 0) { + fwrite (bytes, 1, size - info->buffer.readsize, out); + } +#endif + return size - info->buffer.readsize; +} + +#if 0 static int cmp3_read_int16 (DB_fileinfo_t *_info, char *bytes, int size) { #if WRITE_DUMP @@ -968,6 +991,7 @@ cmp3_read_float32 (DB_fileinfo_t *_info, char *bytes, int size) { _info->readpos = (float)(info->buffer.currentsample - info->buffer.startsample) / info->buffer.samplerate; return size - info->buffer.readsize; } +#endif static int cmp3_seek_sample (DB_fileinfo_t *_info, int sample) { @@ -1246,8 +1270,7 @@ static DB_decoder_t plugin = { .open = cmp3_open, .init = cmp3_init, .free = cmp3_free, - .read_int16 = cmp3_read_int16, - .read_float32 = cmp3_read_float32, + .read = cmp3_read, .seek = cmp3_seek, .seek_sample = cmp3_seek_sample, .insert = cmp3_insert, diff --git a/plugins/supereq/supereq.c b/plugins/supereq/supereq.c index 577af84a..6ebf9b24 100644 --- a/plugins/supereq/supereq.c +++ b/plugins/supereq/supereq.c @@ -129,7 +129,7 @@ supereq_regen_table_thread (void *param) { } int -supereq_process_int16 (int16_t *samples, int nsamples, int nch, int bps, int srate) { +supereq_process (char * restrict samples, ddb_waveformat_t * restrict fmt) { if ((nch != 1 && nch != 2) || (bps != 8 && bps != 16 && bps != 24)) return nsamples; if (params_changed && !tid) { tid = deadbeef->thread_start (supereq_regen_table_thread, NULL); diff --git a/premix.c b/premix.c new file mode 100644 index 00000000..36fd4147 --- /dev/null +++ b/premix.c @@ -0,0 +1,90 @@ +/* + DeaDBeeF - ultimate music player for GNU/Linux systems with X11 + Copyright (C) 2009-2010 Alexey Yakovenko + + 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include +#include +#include +#include "deadbeef.h" +#include "premix.h" + +#define trace(...) { fprintf(stderr, __VA_ARGS__); } +//#define trace(fmt,...) + +void +pcm_write_sample (const ddb_waveformat_t * restrict inputfmt, const char * restrict input, const ddb_waveformat_t * restrict outputfmt, char * restrict output) { + memcpy (output, input, 2); +} + +int +pcm_convert (const ddb_waveformat_t * restrict inputfmt, const char * restrict input, const ddb_waveformat_t * restrict outputfmt, char * restrict output, int inputsize) { + // calculate output size + int inputsamplesize = (inputfmt->bps >> 3) * inputfmt->channels; + int outputsamplesize = (outputfmt->bps >> 3) * outputfmt->channels; + int nsamples = inputsize / inputsamplesize; + + uint32_t outchannels = 0; + + if (output) { + // build channelmap + int channelmap[32] = {-1}; + uint32_t inputbitmask = 1; + for (int i = 0; i < inputfmt->channels; i++) { + // find next input channel + while (inputbitmask < 0x80000000 && !(inputfmt->channelmask & inputbitmask)) { + inputbitmask <<= 1; + } + if (!(inputfmt->channelmask & inputbitmask)) { + trace ("pcm_convert: channelmask doesn't correspond inputfmt (channels=%d, channelmask=%X)!", inputfmt->channels, inputfmt->channelmask); + break; + } + if (outputfmt->channelmask & inputbitmask) { + int o = 0; + uint32_t outputbitmask = 1; + while (outputbitmask < 0x80000000 && (outputfmt->channelmask & outputbitmask) != inputbitmask) { + outputbitmask <<= 1; + o++; + } + if (!(inputfmt->channelmask & outputbitmask)) { + // no corresponding output channel -- ignore + continue; + } + outchannels |= outputbitmask; + channelmap[i] = o; // input channel i going to output channel o + trace ("channelmap[%d]=%d\n", i, o); + } + inputbitmask <<= 1; + } + + if (outchannels != outputfmt->channelmask) { + // some of the channels are not used + memset (output, 0, nsamples * outputsamplesize); + } + for (int s = 0; s < nsamples; s++) { + for (int c = 0; c < inputfmt->channels; c++) { + if (channelmap[c] != -1) { + pcm_write_sample (inputfmt, input, outputfmt, output + (outputfmt->bps >> 3) * channelmap[c]); + } + input += inputfmt->bps >> 3; + } + output += outputsamplesize; + } + } + return nsamples * outputsamplesize; +} + diff --git a/premix.h b/premix.h new file mode 100644 index 00000000..e25707da --- /dev/null +++ b/premix.h @@ -0,0 +1,27 @@ +/* + DeaDBeeF - ultimate music player for GNU/Linux systems with X11 + Copyright (C) 2009-2010 Alexey Yakovenko + + 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#ifndef __PREMIX_H +#define __PREMIX_H + +// @returns number of output bytes +int +pcm_convert (const ddb_waveformat_t * restrict inputfmt, const char * restrict input, const ddb_waveformat_t * restrict outputfmt, char * restrict output, int inputsize); + +#endif diff --git a/streamer.c b/streamer.c index 07318570..61af6ab6 100644 --- a/streamer.c +++ b/streamer.c @@ -35,11 +35,12 @@ #include "optmath.h" #include "volume.h" #include "vfs.h" +#include "premix.h" #define trace(...) { fprintf(stderr, __VA_ARGS__); } //#define trace(fmt,...) -// #define WRITE_DUMP 1 +#define WRITE_DUMP 1 #if WRITE_DUMP FILE *out; @@ -117,6 +118,18 @@ src_unlock (void) { mutex_unlock (srcmutex); } +void +adjust_waveformat (ddb_waveformat_t *fmt) { + if (!fmt->is_multichannel) { + if (fmt->channels == 1) { + fmt->channelmask = DDB_SPEAKER_FRONT_LEFT; + } + else if (fmt->channelmask == 2) { + fmt->channelmask = DDB_SPEAKER_FRONT_LEFT | DDB_SPEAKER_FRONT_RIGHT; + } + } +} + static void streamer_abort_files (void) { if (fileinfo && fileinfo->file) { @@ -639,6 +652,7 @@ streamer_set_current (playItem_t *it) { dec->free (fileinfo); fileinfo = NULL; } + adjust_waveformat (&fileinfo->fmt); } if (!dec || !fileinfo) { @@ -819,6 +833,7 @@ streamer_start_new_song (void) { streamer_reset (1); if (fileinfo) { plug_get_output ()->setformat (&fileinfo->fmt); + adjust_waveformat (&plug_get_output ()->fmt); } if (output->play () < 0) { fprintf (stderr, "streamer: failed to start playback; output plugin doesn't work\n"); @@ -833,6 +848,7 @@ streamer_start_new_song (void) { streamer_reset (1); if (fileinfo) { plug_get_output ()->setformat (&fileinfo->fmt); + adjust_waveformat (&plug_get_output ()->fmt); } if (output->play () < 0) { fprintf (stderr, "streamer: failed to start playback; output plugin doesn't work\n"); @@ -931,6 +947,7 @@ streamer_thread (void *ctx) { memcpy (&prevfmt, &output->fmt, sizeof (ddb_waveformat_t)); if (memcmp (&output->fmt, &fileinfo->fmt, sizeof (ddb_waveformat_t))) { output->setformat (&fileinfo->fmt); + adjust_waveformat (&output->fmt); // check if the format actually changed if (memcmp (&output->fmt, &prevfmt, sizeof (ddb_waveformat_t))) { // restart streaming of current track @@ -946,6 +963,7 @@ streamer_thread (void *ctx) { dec->free (fileinfo); fileinfo = NULL; } + adjust_waveformat (&fileinfo->fmt); } if (!dec || !fileinfo) { // FIXME: handle error @@ -961,6 +979,7 @@ streamer_thread (void *ctx) { if (output->state () != OUTPUT_STATE_PLAYING) { if (fileinfo) { plug_get_output ()->setformat (&fileinfo->fmt); + adjust_waveformat (&plug_get_output ()->fmt); } if (output->play () < 0) { fprintf (stderr, "streamer: failed to start playback after samplerate change; output plugin doesn't work\n"); @@ -1012,6 +1031,7 @@ streamer_thread (void *ctx) { dec->free (fileinfo); fileinfo = NULL; } + adjust_waveformat (&fileinfo->fmt); } mutex_unlock (decodemutex); @@ -1056,8 +1076,8 @@ streamer_thread (void *ctx) { usleep(20000); continue; } - int channels = 2; // FIXME: needs to be queried from output plugin - int bytes_in_one_second = rate * sizeof (int16_t) * channels; + int channels = output->fmt.channels; + int bytes_in_one_second = rate * (output->fmt.bps>>3) * channels; const int blocksize = 4096; int alloc_time = 1000 / (bytes_in_one_second * 2 / blocksize); @@ -1075,6 +1095,13 @@ streamer_thread (void *ctx) { assert ((sz&3) == 0); char buf[sz]; streamer_unlock (); + + // ensure that size is possible with current format + int samplesize = output->fmt.channels * (output->fmt.bps>>3); + if (sz % samplesize) { + sz -= (sz % samplesize); + } + int bytesread = streamer_read_async (buf,sz); streamer_lock (); memcpy (streambuffer+streambuffer_fill, buf, sz); @@ -1359,6 +1386,7 @@ float32_to_int16 (float *in, int16_t *out, int nsamples) { */ +#if 0 static int streamer_read_data_for_src (int16_t *buffer, int frames) { int channels = fileinfo->fmt.channels; @@ -1481,51 +1509,9 @@ streamer_decode_src_libsamplerate (uint8_t *bytes, int size) { } return initsize-size; } - -#if 0 -static int -streamer_decode_src (uint8_t *bytes, int size) { - int initsize = size; - int16_t *out = (int16_t *)bytes; - DB_decoder_t *decoder = streaming_track->decoder; - int samplerate = decoder->info.samplerate; - float ratio = (float)samplerate / output->fmt.samplerate; - while (size > 0) { - int n_output_frames = size / sizeof (int16_t) / 2; - int n_input_frames = n_output_frames * samplerate / output->fmt.samplerate + 100; - // read data from decoder - if (n_input_frames > SRC_BUFFER - src_remaining ) { - n_input_frames = SRC_BUFFER - src_remaining; - } - int nread = streamer_read_data_for_src (&g_src_in_buffer[src_remaining*2], n_input_frames); - src_remaining += nread; - // resample - float idx = 0; - int i = 0; - while (i < n_output_frames && idx < src_remaining) { - int k = (int)idx; - out[0] = g_src_in_buffer[k*2+0]; - out[1] = g_src_in_buffer[k*2+1]; - i++; - idx += ratio; - out += 2; - } - size -= i*4; - src_remaining -= idx; - if (src_remaining < 0) { - src_remaining = 0; - } - else if (src_remaining > 0) { - memmove (g_src_in_buffer, &g_src_in_buffer[(SRC_BUFFER-src_remaining)*2], src_remaining*2*sizeof (int16_t)); - } - if (nread != n_input_frames) { - break; - } - } - return initsize-size; -} #endif +// decodes data and converts to current output format // returns number of bytes been read static int streamer_read_async (char *bytes, int size) { @@ -1540,6 +1526,29 @@ streamer_read_async (char *bytes, int size) { break; } if (fileinfo->fmt.samplerate != -1) { + if (!memcmp (&fileinfo->fmt, &output->fmt, sizeof (ddb_waveformat_t))) { + bytesread = fileinfo->plugin->read (fileinfo, bytes, size); + } + else { +// trace ("format mismatch, converting:\n"); +// trace ("input bps=%d, channels=%d, samplerate=%d, channelmask=%X\n", fileinfo->fmt.bps, fileinfo->fmt.channels, fileinfo->fmt.samplerate, fileinfo->fmt.channelmask); +// trace ("output bps=%d, channels=%d, samplerate=%d, channelmask=%X\n", output->fmt.bps, output->fmt.channels, output->fmt.samplerate, output->fmt.channelmask); + + int outputsamplesize = (output->fmt.bps>>3)*output->fmt.channels; + int inputsamplesize = (fileinfo->fmt.bps>>3)*fileinfo->fmt.channels; + int inputsize = size/outputsamplesize*inputsamplesize; + char input[inputsize]; + inputsize = fileinfo->plugin->read (fileinfo, input, inputsize); + bytesread = pcm_convert (&fileinfo->fmt, input, &output->fmt, bytes, inputsize); +// trace ("decoded %d bytes, writing %d bytes, requested %d bytes\n", inputsize, bytesread, size); + } +#if WRITE_DUMP + if (bytesread) { + fwrite (bytes, 1, bytesread, out); + } +#endif + +#if 0 int nchannels = fileinfo->fmt.channels; if (nchannels > 2) { nchannels = 2; @@ -1568,17 +1577,20 @@ streamer_read_async (char *bytes, int size) { // immediately start streaming next track bytes_until_next_song = -1; } +#endif } +#if 0 if (bytesread > 0) { // apply dsp DB_dsp_t **dsp = deadbeef->plug_get_dsp_list (); int srate = output->fmt.samplerate; for (int i = 0; dsp[i]; i++) { if (dsp[i]->enabled ()) { - dsp[i]->process_int16 ((int16_t *)bytes, bytesread/4, 2, 16, srate); + dsp[i]->process (bytes, bytesread/4, &output->fmt); } } } +#endif mutex_unlock (decodemutex); bytes += bytesread; size -= bytesread; @@ -1621,8 +1633,8 @@ streamer_read (char *bytes, int size) { memcpy (bytes, streambuffer, sz); memmove (streambuffer, streambuffer+sz, streambuffer_fill-sz); streambuffer_fill -= sz; - playpos += (float)sz/output->fmt.samplerate/4.f; - playing_track->playtime += (float)sz/output->fmt.samplerate/4.f; + playpos += (float)sz/output->fmt.samplerate/((output->fmt.bps>>3)*output->fmt.channels); + playing_track->playtime += (float)sz/output->fmt.samplerate/((output->fmt.bps>>3)*output->fmt.channels); if (playlist_track) { playing_track->filetype = playlist_track->filetype; } @@ -1669,9 +1681,6 @@ streamer_read (char *bytes, int size) { int ms = (tm2.tv_sec*1000+tm2.tv_usec/1000) - (tm1.tv_sec*1000+tm1.tv_usec/1000); printf ("streamer_read took %d ms\n", ms); -#endif -#if WRITE_DUMP - fwrite (bytes, 1, sz, out); #endif return sz; } -- cgit v1.2.3