music_mad.c
author Sam Lantinga <slouken@libsdl.org>
Sat, 21 Oct 2017 14:40:31 -0700
changeset 823 bcd59adacdcc
parent 815 cac4c0288bbd
child 848 3907db698eb5
permissions -rw-r--r--
Fixed linker order so -lmingw32 comes before -lSDL2main
slouken@357
     1
/*
slouken@518
     2
  SDL_mixer:  An audio mixer library based on the SDL library
slouken@725
     3
  Copyright (C) 1997-2017 Sam Lantinga <slouken@libsdl.org>
slouken@357
     4
slouken@518
     5
  This software is provided 'as-is', without any express or implied
slouken@518
     6
  warranty.  In no event will the authors be held liable for any damages
slouken@518
     7
  arising from the use of this software.
slouken@357
     8
slouken@518
     9
  Permission is granted to anyone to use this software for any purpose,
slouken@518
    10
  including commercial applications, and to alter it and redistribute it
slouken@518
    11
  freely, subject to the following restrictions:
slouken@357
    12
slouken@518
    13
  1. The origin of this software must not be misrepresented; you must not
slouken@518
    14
     claim that you wrote the original software. If you use this software
slouken@518
    15
     in a product, an acknowledgment in the product documentation would be
slouken@518
    16
     appreciated but is not required.
slouken@518
    17
  2. Altered source versions must be plainly marked as such, and must not be
slouken@518
    18
     misrepresented as being the original software.
slouken@518
    19
  3. This notice may not be removed or altered from any source distribution.
slouken@357
    20
*/
slouken@357
    21
slouken@777
    22
#ifdef MUSIC_MP3_MAD
slouken@357
    23
slouken@357
    24
#include "music_mad.h"
slouken@357
    25
slouken@777
    26
#include "mad.h"
slouken@777
    27
slouken@797
    28
slouken@797
    29
/* NOTE: The dithering functions are GPL, which should be fine if your
slouken@797
    30
         application is GPL (which would need to be true if you enabled
slouken@797
    31
         libmad support in SDL_mixer). If you're using libmad under the
slouken@797
    32
         commercial license, you need to disable this code.
slouken@797
    33
*/
slouken@797
    34
/************************ dithering functions ***************************/
slouken@797
    35
slouken@797
    36
#ifdef MUSIC_MP3_MAD_GPL_DITHERING
slouken@797
    37
slouken@797
    38
/* All dithering done here is taken from the GPL'ed xmms-mad plugin. */
slouken@797
    39
slouken@797
    40
/* Copyright (C) 1997 Makoto Matsumoto and Takuji Nishimura.       */
slouken@797
    41
/* Any feedback is very welcome. For any question, comments,       */
slouken@797
    42
/* see http://www.math.keio.ac.jp/matumoto/emt.html or email       */
slouken@797
    43
/* matumoto@math.keio.ac.jp                                        */
slouken@797
    44
slouken@797
    45
/* Period parameters */
slouken@797
    46
#define MP3_DITH_N 624
slouken@797
    47
#define MP3_DITH_M 397
slouken@797
    48
#define MATRIX_A 0x9908b0df   /* constant vector a */
slouken@797
    49
#define UPPER_MASK 0x80000000 /* most significant w-r bits */
slouken@797
    50
#define LOWER_MASK 0x7fffffff /* least significant r bits */
slouken@797
    51
slouken@797
    52
/* Tempering parameters */
slouken@797
    53
#define TEMPERING_MASK_B 0x9d2c5680
slouken@797
    54
#define TEMPERING_MASK_C 0xefc60000
slouken@797
    55
#define TEMPERING_SHIFT_U(y)  (y >> 11)
slouken@797
    56
#define TEMPERING_SHIFT_S(y)  (y << 7)
slouken@797
    57
#define TEMPERING_SHIFT_T(y)  (y << 15)
slouken@797
    58
#define TEMPERING_SHIFT_L(y)  (y >> 18)
slouken@797
    59
slouken@797
    60
static unsigned long mt[MP3_DITH_N]; /* the array for the state vector  */
slouken@797
    61
static int mti=MP3_DITH_N+1; /* mti==MP3_DITH_N+1 means mt[MP3_DITH_N] is not initialized */
slouken@797
    62
slouken@797
    63
/* initializing the array with a NONZERO seed */
slouken@797
    64
static void sgenrand(unsigned long seed)
slouken@797
    65
{
slouken@797
    66
    /* setting initial seeds to mt[MP3_DITH_N] using         */
slouken@797
    67
    /* the generator Line 25 of Table 1 in          */
slouken@797
    68
    /* [KNUTH 1981, The Art of Computer Programming */
slouken@797
    69
    /*    Vol. 2 (2nd Ed.), pp102]                  */
slouken@797
    70
    mt[0]= seed & 0xffffffff;
slouken@797
    71
    for (mti=1; mti<MP3_DITH_N; mti++)
slouken@797
    72
        mt[mti] = (69069 * mt[mti-1]) & 0xffffffff;
slouken@797
    73
}
slouken@797
    74
slouken@797
    75
static unsigned long genrand(void)
slouken@797
    76
{
slouken@797
    77
    unsigned long y;
slouken@797
    78
    static unsigned long mag01[2]={0x0, MATRIX_A};
slouken@797
    79
    /* mag01[x] = x * MATRIX_A  for x=0,1 */
slouken@797
    80
slouken@797
    81
    if (mti >= MP3_DITH_N) { /* generate MP3_DITH_N words at one time */
slouken@797
    82
        int kk;
slouken@797
    83
slouken@797
    84
        if (mti == MP3_DITH_N+1)   /* if sgenrand() has not been called, */
slouken@797
    85
            sgenrand(4357); /* a default initial seed is used   */
slouken@797
    86
slouken@797
    87
        for (kk=0;kk<MP3_DITH_N-MP3_DITH_M;kk++) {
slouken@797
    88
            y = (mt[kk]&UPPER_MASK)|(mt[kk+1]&LOWER_MASK);
slouken@797
    89
            mt[kk] = mt[kk+MP3_DITH_M] ^ (y >> 1) ^ mag01[y & 0x1];
slouken@797
    90
        }
slouken@797
    91
        for (;kk<MP3_DITH_N-1;kk++) {
slouken@797
    92
            y = (mt[kk]&UPPER_MASK)|(mt[kk+1]&LOWER_MASK);
slouken@797
    93
            mt[kk] = mt[kk+(MP3_DITH_M-MP3_DITH_N)] ^ (y >> 1) ^ mag01[y & 0x1];
slouken@797
    94
        }
slouken@797
    95
        y = (mt[MP3_DITH_N-1]&UPPER_MASK)|(mt[0]&LOWER_MASK);
slouken@797
    96
        mt[MP3_DITH_N-1] = mt[MP3_DITH_M-1] ^ (y >> 1) ^ mag01[y & 0x1];
slouken@797
    97
slouken@797
    98
        mti = 0;
slouken@797
    99
    }
slouken@797
   100
slouken@797
   101
    y = mt[mti++];
slouken@797
   102
    y ^= TEMPERING_SHIFT_U(y);
slouken@797
   103
    y ^= TEMPERING_SHIFT_S(y) & TEMPERING_MASK_B;
slouken@797
   104
    y ^= TEMPERING_SHIFT_T(y) & TEMPERING_MASK_C;
slouken@797
   105
    y ^= TEMPERING_SHIFT_L(y);
slouken@797
   106
slouken@797
   107
    return y;
slouken@797
   108
}
slouken@797
   109
slouken@797
   110
static long triangular_dither_noise(int nbits) {
slouken@797
   111
    /* parameter nbits : the peak-to-peak amplitude desired (in bits)
slouken@797
   112
     *  use with nbits set to    2 + nber of bits to be trimmed.
slouken@797
   113
     * (because triangular is made from two uniformly distributed processes,
slouken@797
   114
     * it starts at 2 bits peak-to-peak amplitude)
slouken@797
   115
     * see The Theory of Dithered Quantization by Robert Alexander Wannamaker
slouken@797
   116
     * for complete proof of why that's optimal
slouken@797
   117
     */
slouken@797
   118
    long v = (genrand()/2 - genrand()/2); /* in ]-2^31, 2^31[ */
slouken@797
   119
    long P = 1 << (32 - nbits); /* the power of 2 */
slouken@797
   120
    v /= P;
slouken@797
   121
    /* now v in ]-2^(nbits-1), 2^(nbits-1) [ */
slouken@797
   122
slouken@797
   123
    return v;
slouken@797
   124
}
slouken@797
   125
slouken@797
   126
#endif /* MUSIC_MP3_MAD_GPL_DITHERING */
slouken@797
   127
slouken@797
   128
slouken@777
   129
#define MAD_INPUT_BUFFER_SIZE   (5*8192)
slouken@777
   130
slouken@777
   131
enum {
slouken@777
   132
    MS_input_eof      = 0x0001,
slouken@777
   133
    MS_input_error    = 0x0001,
slouken@797
   134
    MS_decode_error   = 0x0002,
slouken@777
   135
    MS_error_flags    = 0x000f,
slouken@777
   136
};
slouken@777
   137
slouken@777
   138
typedef struct {
slouken@797
   139
    int play_count;
slouken@777
   140
    SDL_RWops *src;
slouken@777
   141
    int freesrc;
slouken@777
   142
    struct mad_stream stream;
slouken@777
   143
    struct mad_frame frame;
slouken@777
   144
    struct mad_synth synth;
slouken@777
   145
    mad_timer_t next_frame_start;
slouken@777
   146
    int volume;
slouken@777
   147
    int status;
slouken@797
   148
    SDL_AudioStream *audiostream;
slouken@777
   149
slouken@777
   150
    unsigned char input_buffer[MAD_INPUT_BUFFER_SIZE + MAD_BUFFER_GUARD];
slouken@797
   151
} MAD_Music;
slouken@777
   152
slouken@777
   153
slouken@797
   154
static int MAD_Seek(void *context, double position);
slouken@797
   155
slouken@777
   156
static void *MAD_CreateFromRW(SDL_RWops *src, int freesrc)
slouken@521
   157
{
slouken@797
   158
    MAD_Music *music;
slouken@357
   159
slouken@797
   160
    music = (MAD_Music *)SDL_calloc(1, sizeof(MAD_Music));
slouken@797
   161
    if (!music) {
slouken@797
   162
        SDL_OutOfMemory();
slouken@797
   163
        return NULL;
slouken@777
   164
    }
slouken@797
   165
    music->src = src;
slouken@797
   166
    music->volume = MIX_MAX_VOLUME;
slouken@797
   167
slouken@797
   168
    mad_stream_init(&music->stream);
slouken@797
   169
    mad_frame_init(&music->frame);
slouken@797
   170
    mad_synth_init(&music->synth);
slouken@797
   171
    mad_timer_reset(&music->next_frame_start);
slouken@797
   172
slouken@797
   173
    music->freesrc = freesrc;
slouken@797
   174
    return music;
slouken@357
   175
}
slouken@357
   176
slouken@777
   177
static void MAD_SetVolume(void *context, int volume)
slouken@521
   178
{
slouken@797
   179
    MAD_Music *music = (MAD_Music *)context;
slouken@797
   180
    music->volume = volume;
slouken@357
   181
}
slouken@357
   182
slouken@357
   183
/* Starts the playback. */
slouken@797
   184
static int MAD_Play(void *context, int play_count)
slouken@777
   185
{
slouken@797
   186
    MAD_Music *music = (MAD_Music *)context;
slouken@797
   187
    music->play_count = play_count;
slouken@797
   188
    return MAD_Seek(music, 0.0);
slouken@357
   189
}
slouken@357
   190
slouken@797
   191
/* Reads the next frame from the file.
slouken@797
   192
   Returns true on success or false on failure.
slouken@797
   193
 */
slouken@797
   194
static SDL_bool read_next_frame(MAD_Music *music)
slouken@777
   195
{
slouken@797
   196
    if (music->stream.buffer == NULL ||
slouken@797
   197
        music->stream.error == MAD_ERROR_BUFLEN) {
slouken@777
   198
        size_t read_size;
slouken@777
   199
        size_t remaining;
slouken@777
   200
        unsigned char *read_start;
slouken@357
   201
slouken@777
   202
        /* There might be some bytes in the buffer left over from last
slouken@777
   203
           time.    If so, move them down and read more bytes following
slouken@777
   204
           them. */
slouken@797
   205
        if (music->stream.next_frame != NULL) {
slouken@797
   206
            remaining = music->stream.bufend - music->stream.next_frame;
slouken@797
   207
            memmove(music->input_buffer, music->stream.next_frame, remaining);
slouken@797
   208
            read_start = music->input_buffer + remaining;
slouken@777
   209
            read_size = MAD_INPUT_BUFFER_SIZE - remaining;
slouken@617
   210
slouken@777
   211
        } else {
slouken@777
   212
            read_size = MAD_INPUT_BUFFER_SIZE;
slouken@797
   213
            read_start = music->input_buffer;
slouken@777
   214
            remaining = 0;
slouken@777
   215
        }
slouken@777
   216
slouken@777
   217
        /* Now read additional bytes from the input file. */
slouken@797
   218
        read_size = SDL_RWread(music->src, read_start, 1, read_size);
slouken@777
   219
slouken@777
   220
        if (read_size == 0) {
slouken@797
   221
            if ((music->status & (MS_input_eof | MS_input_error)) == 0) {
slouken@777
   222
                /* FIXME: how to detect error? */
slouken@797
   223
                music->status |= MS_input_eof;
slouken@777
   224
slouken@777
   225
                /* At the end of the file, we must stuff MAD_BUFFER_GUARD
slouken@777
   226
                   number of 0 bytes. */
slouken@777
   227
                SDL_memset(read_start + read_size, 0, MAD_BUFFER_GUARD);
slouken@777
   228
                read_size += MAD_BUFFER_GUARD;
slouken@777
   229
            }
slouken@777
   230
        }
slouken@777
   231
slouken@777
   232
        /* Now feed those bytes into the libmad stream. */
slouken@797
   233
        mad_stream_buffer(&music->stream, music->input_buffer,
slouken@777
   234
                                            read_size + remaining);
slouken@797
   235
        music->stream.error = MAD_ERROR_NONE;
slouken@617
   236
    }
slouken@617
   237
slouken@777
   238
    /* Now ask libmad to extract a frame from the data we just put in
slouken@777
   239
       its buffer. */
slouken@797
   240
    if (mad_frame_decode(&music->frame, &music->stream)) {
slouken@797
   241
        if (MAD_RECOVERABLE(music->stream.error)) {
slouken@797
   242
            return SDL_FALSE;
slouken@617
   243
slouken@797
   244
        } else if (music->stream.error == MAD_ERROR_BUFLEN) {
slouken@797
   245
            return SDL_FALSE;
slouken@617
   246
slouken@777
   247
        } else {
slouken@797
   248
            Mix_SetError("mad_frame_decode() failed, corrupt stream?");
slouken@797
   249
            music->status |= MS_decode_error;
slouken@797
   250
            return SDL_FALSE;
slouken@777
   251
        }
slouken@617
   252
    }
slouken@617
   253
slouken@797
   254
    mad_timer_add(&music->next_frame_start, music->frame.header.duration);
slouken@617
   255
slouken@797
   256
    return SDL_TRUE;
slouken@357
   257
}
slouken@357
   258
slouken@357
   259
/* Scale a MAD sample to 16 bits for output. */
slouken@797
   260
static Sint16 scale(mad_fixed_t sample)
slouken@797
   261
{
slouken@797
   262
    const int n_bits_to_loose = MAD_F_FRACBITS + 1 - 16;
slouken@797
   263
slouken@777
   264
    /* round */
slouken@797
   265
    sample += (1L << (n_bits_to_loose - 1));
slouken@797
   266
slouken@797
   267
#ifdef MUSIC_MP3_MAD_GPL_DITHERING
slouken@797
   268
    sample += triangular_dither_noise(n_bits_to_loose + 1);
slouken@797
   269
#endif
slouken@357
   270
slouken@777
   271
    /* clip */
slouken@777
   272
    if (sample >= MAD_F_ONE)
slouken@777
   273
        sample = MAD_F_ONE - 1;
slouken@777
   274
    else if (sample < -MAD_F_ONE)
slouken@777
   275
        sample = -MAD_F_ONE;
slouken@357
   276
slouken@777
   277
    /* quantize */
slouken@797
   278
    return (Sint16)(sample >> n_bits_to_loose);
slouken@357
   279
}
slouken@357
   280
slouken@777
   281
/* Once the frame has been read, copies its samples into the output buffer. */
slouken@797
   282
static SDL_bool decode_frame(MAD_Music *music)
slouken@797
   283
{
slouken@777
   284
    struct mad_pcm *pcm;
slouken@797
   285
    unsigned int i, nchannels, nsamples;
slouken@777
   286
    mad_fixed_t const *left_ch, *right_ch;
slouken@797
   287
    Sint16 *buffer, *dst;
slouken@797
   288
    int result;
slouken@357
   289
slouken@797
   290
    mad_synth_frame(&music->synth, &music->frame);
slouken@797
   291
    pcm = &music->synth.pcm;
slouken@357
   292
slouken@797
   293
    if (!music->audiostream) {
slouken@797
   294
        music->audiostream = SDL_NewAudioStream(AUDIO_S16, pcm->channels, pcm->samplerate, music_spec.format, music_spec.channels, music_spec.freq);
slouken@797
   295
        if (!music->audiostream) {
slouken@797
   296
            return SDL_FALSE;
slouken@777
   297
        }
slouken@357
   298
    }
slouken@357
   299
slouken@797
   300
    nchannels = pcm->channels;
slouken@797
   301
    nsamples = pcm->length;
slouken@797
   302
    left_ch = pcm->samples[0];
slouken@797
   303
    right_ch = pcm->samples[1];
slouken@797
   304
    buffer = SDL_stack_alloc(Sint16, nsamples*nchannels);
slouken@797
   305
    if (!buffer) {
slouken@797
   306
        SDL_OutOfMemory();
slouken@797
   307
        return SDL_FALSE;
slouken@797
   308
    }
slouken@797
   309
slouken@797
   310
    dst = buffer;
slouken@797
   311
    if (nchannels == 1) {
slouken@797
   312
        for (i = nsamples; i--;) {
slouken@797
   313
            *dst++ = scale(*left_ch++);
slouken@797
   314
        }
slouken@797
   315
    } else {
slouken@797
   316
        for (i = nsamples; i--;) {
slouken@797
   317
            *dst++ = scale(*left_ch++);
slouken@797
   318
            *dst++ = scale(*right_ch++);
slouken@797
   319
        }
slouken@797
   320
    }
slouken@797
   321
slouken@797
   322
    result = SDL_AudioStreamPut(music->audiostream, buffer, (nsamples * nchannels * sizeof(Sint16)));
slouken@797
   323
    SDL_stack_free(buffer);
slouken@797
   324
slouken@797
   325
    if (result < 0) {
slouken@797
   326
        return SDL_FALSE;
slouken@797
   327
    }
slouken@797
   328
    return SDL_TRUE;
slouken@357
   329
}
slouken@357
   330
slouken@797
   331
static int MAD_GetSome(void *context, void *data, int bytes, SDL_bool *done)
slouken@777
   332
{
slouken@797
   333
    MAD_Music *music = (MAD_Music *)context;
slouken@797
   334
    int filled;
slouken@357
   335
slouken@797
   336
    if (music->audiostream) {
slouken@797
   337
        filled = SDL_AudioStreamGet(music->audiostream, data, bytes);
slouken@797
   338
        if (filled != 0) {
slouken@797
   339
            return filled;
slouken@797
   340
        }
slouken@797
   341
    }
slouken@797
   342
slouken@797
   343
    if (!music->play_count) {
slouken@797
   344
        /* All done */
slouken@797
   345
        *done = SDL_TRUE;
slouken@777
   346
        return 0;
slouken@617
   347
    }
slouken@357
   348
slouken@797
   349
    if (read_next_frame(music)) {
slouken@797
   350
        if (!decode_frame(music)) {
slouken@797
   351
            return -1;
slouken@797
   352
        }
slouken@815
   353
    } else if (music->status & MS_input_eof) {
slouken@797
   354
        int play_count = -1;
slouken@797
   355
        if (music->play_count > 0) {
slouken@797
   356
            play_count = (music->play_count - 1);
slouken@777
   357
        }
slouken@797
   358
        if (MAD_Play(music, play_count) < 0) {
slouken@797
   359
            return -1;
slouken@777
   360
        }
slouken@815
   361
    } else if (music->status & MS_decode_error) {
slouken@815
   362
        return -1;
slouken@777
   363
    }
slouken@777
   364
    return 0;
slouken@777
   365
}
slouken@797
   366
static int MAD_GetAudio(void *context, void *data, int bytes)
slouken@797
   367
{
slouken@797
   368
    MAD_Music *music = (MAD_Music *)context;
slouken@797
   369
    return music_pcm_getaudio(context, data, bytes, music->volume, MAD_GetSome);
slouken@797
   370
}
slouken@777
   371
slouken@777
   372
static int MAD_Seek(void *context, double position)
slouken@777
   373
{
slouken@797
   374
    MAD_Music *music = (MAD_Music *)context;
slouken@777
   375
    mad_timer_t target;
slouken@777
   376
    int int_part;
slouken@777
   377
slouken@777
   378
    int_part = (int)position;
slouken@777
   379
    mad_timer_set(&target, int_part, (int)((position - int_part) * 1000000), 1000000);
slouken@777
   380
slouken@797
   381
    if (mad_timer_compare(music->next_frame_start, target) > 0) {
slouken@777
   382
        /* In order to seek backwards in a VBR file, we have to rewind and
slouken@777
   383
           start again from the beginning.    This isn't necessary if the
slouken@777
   384
           file happens to be CBR, of course; in that case we could seek
slouken@777
   385
           directly to the frame we want.    But I leave that little
slouken@777
   386
           optimization for the future developer who discovers she really
slouken@777
   387
           needs it. */
slouken@797
   388
        mad_timer_reset(&music->next_frame_start);
slouken@797
   389
        music->status &= ~MS_error_flags;
slouken@777
   390
slouken@797
   391
        SDL_RWseek(music->src, 0, RW_SEEK_SET);
slouken@617
   392
    }
slouken@617
   393
slouken@777
   394
    /* Now we have to skip frames until we come to the right one.
slouken@777
   395
       Again, only truly necessary if the file is VBR. */
slouken@797
   396
    while (mad_timer_compare(music->next_frame_start, target) < 0) {
slouken@797
   397
        if (!read_next_frame(music)) {
slouken@797
   398
            if ((music->status & MS_error_flags) != 0) {
slouken@777
   399
                /* Couldn't read a frame; either an error condition or
slouken@777
   400
                     end-of-file.    Stop. */
slouken@777
   401
                return Mix_SetError("Seek position out of range");
slouken@777
   402
            }
slouken@777
   403
        }
slouken@617
   404
    }
slouken@777
   405
slouken@777
   406
    /* Here we are, at the beginning of the frame that contains the
slouken@777
   407
       target time.    Ehh, I say that's close enough.    If we wanted to,
slouken@777
   408
       we could get more precise by decoding the frame now and counting
slouken@777
   409
       the appropriate number of samples out of it. */
slouken@777
   410
    return 0;
slouken@357
   411
}
slouken@357
   412
slouken@777
   413
static void MAD_Delete(void *context)
slouken@777
   414
{
slouken@797
   415
    MAD_Music *music = (MAD_Music *)context;
slouken@777
   416
slouken@797
   417
    mad_stream_finish(&music->stream);
slouken@797
   418
    mad_frame_finish(&music->frame);
slouken@797
   419
    mad_synth_finish(&music->synth);
slouken@797
   420
slouken@797
   421
    if (music->audiostream) {
slouken@797
   422
        SDL_FreeAudioStream(music->audiostream);
slouken@777
   423
    }
slouken@797
   424
    if (music->freesrc) {
slouken@797
   425
        SDL_RWclose(music->src);
slouken@797
   426
    }
slouken@797
   427
    SDL_free(music);
slouken@357
   428
}
slouken@357
   429
slouken@777
   430
Mix_MusicInterface Mix_MusicInterface_MAD =
slouken@777
   431
{
slouken@777
   432
    "MAD",
slouken@777
   433
    MIX_MUSIC_MAD,
slouken@777
   434
    MUS_MP3,
slouken@777
   435
    SDL_FALSE,
slouken@777
   436
    SDL_FALSE,
slouken@357
   437
slouken@797
   438
    NULL,   /* Load */
slouken@797
   439
    NULL,   /* Open */
slouken@777
   440
    MAD_CreateFromRW,
slouken@797
   441
    NULL,   /* CreateFromFile */
slouken@777
   442
    MAD_SetVolume,
slouken@777
   443
    MAD_Play,
slouken@797
   444
    NULL,   /* IsPlaying */
slouken@777
   445
    MAD_GetAudio,
slouken@777
   446
    MAD_Seek,
slouken@797
   447
    NULL,   /* Pause */
slouken@797
   448
    NULL,   /* Resume */
slouken@797
   449
    NULL,   /* Stop */
slouken@777
   450
    MAD_Delete,
slouken@797
   451
    NULL,   /* Close */
slouken@797
   452
    NULL,   /* Unload */
slouken@777
   453
};
slouken@777
   454
slouken@777
   455
#endif /* MUSIC_MP3_MAD */
slouken@777
   456
slouken@777
   457
/* vi: set ts=4 sw=4 expandtab: */