music_mpg123.c
author Sam Lantinga <slouken@libsdl.org>
Mon, 12 Nov 2018 16:54:24 -0800
changeset 925 5945988b4a41
parent 848 3907db698eb5
child 926 d6c9518fb5ee
permissions -rw-r--r--
Fixed bug 4371 - tvOS Simulator devices not listed

Caleb Cornett

In the Xcode-iOS project, when selecting the libSDL_mixer-tvOS target, no tvOS simulators appear in the available device dropdown.

This is easily fixed with the attached patch.
lolisamurai@757
     1
/*
lolisamurai@757
     2
  SDL_mixer:    An audio mixer library based on the SDL library
slouken@848
     3
  Copyright (C) 1997-2018 Sam Lantinga <slouken@libsdl.org>
lolisamurai@757
     4
lolisamurai@757
     5
  This software is provided 'as-is', without any express or implied
lolisamurai@757
     6
  warranty.    In no event will the authors be held liable for any damages
lolisamurai@757
     7
  arising from the use of this software.
lolisamurai@757
     8
lolisamurai@757
     9
  Permission is granted to anyone to use this software for any purpose,
lolisamurai@757
    10
  including commercial applications, and to alter it and redistribute it
lolisamurai@757
    11
  freely, subject to the following restrictions:
lolisamurai@757
    12
lolisamurai@757
    13
  1. The origin of this software must not be misrepresented; you must not
lolisamurai@757
    14
       claim that you wrote the original software. If you use this software
lolisamurai@757
    15
       in a product, an acknowledgment in the product documentation would be
lolisamurai@757
    16
       appreciated but is not required.
lolisamurai@757
    17
  2. Altered source versions must be plainly marked as such, and must not be
lolisamurai@757
    18
       misrepresented as being the original software.
lolisamurai@757
    19
  3. This notice may not be removed or altered from any source distribution.
lolisamurai@757
    20
*/
lolisamurai@757
    21
slouken@777
    22
/* This file supports playing MP3 files with mpg123 */
lolisamurai@757
    23
slouken@777
    24
#ifdef MUSIC_MP3_MPG123
slouken@777
    25
slouken@792
    26
#include <stdio.h>      // For SEEK_SET
slouken@792
    27
slouken@797
    28
#include "SDL_assert.h"
slouken@787
    29
#include "SDL_loadso.h"
slouken@787
    30
slouken@777
    31
#include "music_mpg123.h"
slouken@777
    32
slouken@777
    33
#include <mpg123.h>
slouken@777
    34
slouken@787
    35
slouken@787
    36
typedef struct {
slouken@787
    37
    int loaded;
slouken@787
    38
    void *handle;
slouken@787
    39
slouken@787
    40
    int (*mpg123_close)(mpg123_handle *mh);
slouken@787
    41
    void (*mpg123_delete)(mpg123_handle *mh);
slouken@787
    42
    void (*mpg123_exit)(void);
slouken@787
    43
    int (*mpg123_format)( mpg123_handle *mh, long rate, int channels, int encodings );
slouken@787
    44
    int (*mpg123_format_none)(mpg123_handle *mh);
slouken@787
    45
    int (*mpg123_getformat)( mpg123_handle *mh, long *rate, int *channels, int *encoding );
slouken@787
    46
    int (*mpg123_init)(void);
slouken@787
    47
    mpg123_handle *(*mpg123_new)(const char* decoder, int *error);
slouken@787
    48
    int (*mpg123_open_handle)(mpg123_handle *mh, void *iohandle);
slouken@787
    49
    const char* (*mpg123_plain_strerror)(int errcode);
slouken@797
    50
    void (*mpg123_rates)(const long **list, size_t *number);
slouken@787
    51
    int (*mpg123_read)(mpg123_handle *mh, unsigned char *outmemory, size_t outmemsize, size_t *done );
slouken@787
    52
    int (*mpg123_replace_reader_handle)( mpg123_handle *mh, ssize_t (*r_read) (void *, void *, size_t), off_t (*r_lseek)(void *, off_t, int), void (*cleanup)(void*) );
slouken@787
    53
    off_t (*mpg123_seek)( mpg123_handle *mh, off_t sampleoff, int whence );
slouken@787
    54
    const char* (*mpg123_strerror)(mpg123_handle *mh);
slouken@787
    55
} mpg123_loader;
slouken@787
    56
slouken@787
    57
static mpg123_loader mpg123 = {
slouken@787
    58
    0, NULL
slouken@787
    59
};
slouken@787
    60
slouken@787
    61
#ifdef MPG123_DYNAMIC
slouken@787
    62
#define FUNCTION_LOADER(FUNC, SIG) \
slouken@787
    63
    mpg123.FUNC = (SIG) SDL_LoadFunction(mpg123.handle, #FUNC); \
slouken@787
    64
    if (mpg123.FUNC == NULL) { SDL_UnloadObject(mpg123.handle); return -1; }
slouken@787
    65
#else
slouken@787
    66
#define FUNCTION_LOADER(FUNC, SIG) \
slouken@787
    67
    mpg123.FUNC = FUNC;
slouken@787
    68
#endif
slouken@787
    69
slouken@792
    70
static int MPG123_Load(void)
slouken@787
    71
{
slouken@787
    72
    if (mpg123.loaded == 0) {
slouken@787
    73
#ifdef MPG123_DYNAMIC
slouken@787
    74
        mpg123.handle = SDL_LoadObject(MPG123_DYNAMIC);
slouken@787
    75
        if (mpg123.handle == NULL) {
slouken@787
    76
            return -1;
slouken@787
    77
        }
slouken@787
    78
#elif defined(__MACOSX__)
slouken@787
    79
        extern int mpg123_init(void) __attribute__((weak_import));
slouken@787
    80
        if (mpg123_init == NULL)
slouken@787
    81
        {
slouken@787
    82
            /* Missing weakly linked framework */
slouken@787
    83
            Mix_SetError("Missing mpg123.framework");
slouken@787
    84
            return -1;
slouken@787
    85
        }
slouken@787
    86
#endif
slouken@787
    87
        FUNCTION_LOADER(mpg123_close, int (*)(mpg123_handle *mh))
slouken@787
    88
        FUNCTION_LOADER(mpg123_delete, void (*)(mpg123_handle *mh))
slouken@787
    89
        FUNCTION_LOADER(mpg123_exit, void (*)(void))
slouken@787
    90
        FUNCTION_LOADER(mpg123_format, int (*)( mpg123_handle *mh, long rate, int channels, int encodings ))
slouken@787
    91
        FUNCTION_LOADER(mpg123_format_none, int (*)(mpg123_handle *mh))
slouken@787
    92
        FUNCTION_LOADER(mpg123_getformat, int (*)( mpg123_handle *mh, long *rate, int *channels, int *encoding ))
slouken@787
    93
        FUNCTION_LOADER(mpg123_init, int (*)(void))
slouken@787
    94
        FUNCTION_LOADER(mpg123_new, mpg123_handle *(*)(const char* decoder, int *error))
slouken@787
    95
        FUNCTION_LOADER(mpg123_open_handle, int (*)(mpg123_handle *mh, void *iohandle))
slouken@787
    96
        FUNCTION_LOADER(mpg123_plain_strerror, const char* (*)(int errcode))
slouken@797
    97
        FUNCTION_LOADER(mpg123_rates, void (*)(const long **list, size_t *number));
slouken@787
    98
        FUNCTION_LOADER(mpg123_read, int (*)(mpg123_handle *mh, unsigned char *outmemory, size_t outmemsize, size_t *done ))
slouken@787
    99
        FUNCTION_LOADER(mpg123_replace_reader_handle, int (*)( mpg123_handle *mh, ssize_t (*r_read) (void *, void *, size_t), off_t (*r_lseek)(void *, off_t, int), void (*cleanup)(void*) ))
slouken@787
   100
        FUNCTION_LOADER(mpg123_seek, off_t (*)( mpg123_handle *mh, off_t sampleoff, int whence ))
slouken@787
   101
        FUNCTION_LOADER(mpg123_strerror, const char* (*)(mpg123_handle *mh))
slouken@787
   102
    }
slouken@787
   103
    ++mpg123.loaded;
slouken@787
   104
slouken@787
   105
    return 0;
slouken@787
   106
}
slouken@787
   107
slouken@792
   108
static void MPG123_Unload(void)
slouken@787
   109
{
slouken@787
   110
    if (mpg123.loaded == 0) {
slouken@787
   111
        return;
slouken@787
   112
    }
slouken@787
   113
    if (mpg123.loaded == 1) {
slouken@787
   114
#ifdef MPG123_DYNAMIC
slouken@787
   115
        SDL_UnloadObject(mpg123.handle);
slouken@787
   116
#endif
slouken@787
   117
    }
slouken@787
   118
    --mpg123.loaded;
slouken@787
   119
}
slouken@787
   120
slouken@787
   121
slouken@777
   122
typedef struct
slouken@777
   123
{
slouken@797
   124
    int play_count;
slouken@777
   125
    SDL_RWops* src;
slouken@777
   126
    int freesrc;
slouken@777
   127
    int volume;
slouken@777
   128
slouken@777
   129
    mpg123_handle* handle;
slouken@797
   130
    SDL_AudioStream *stream;
slouken@797
   131
    unsigned char *buffer;
slouken@797
   132
    size_t buffer_size;
slouken@797
   133
} MPG123_Music;
slouken@777
   134
lolisamurai@757
   135
slouken@797
   136
static int MPG123_Seek(void *context, double secs);
slouken@797
   137
static void MPG123_Delete(void *context);
lolisamurai@757
   138
slouken@797
   139
static int mpg123_format_to_sdl(int fmt)
lolisamurai@757
   140
{
lolisamurai@757
   141
    switch (fmt)
lolisamurai@757
   142
    {
slouken@797
   143
        case MPG123_ENC_SIGNED_8:       return AUDIO_S8;
slouken@797
   144
        case MPG123_ENC_UNSIGNED_8:     return AUDIO_U8;
slouken@797
   145
        case MPG123_ENC_SIGNED_16:      return AUDIO_S16SYS;
slouken@797
   146
        case MPG123_ENC_UNSIGNED_16:    return AUDIO_U16SYS;
slouken@797
   147
        case MPG123_ENC_SIGNED_32:      return AUDIO_S32SYS;
slouken@797
   148
        case MPG123_ENC_FLOAT_32:       return AUDIO_F32SYS;
slouken@797
   149
        default:                        return -1;
lolisamurai@757
   150
    }
lolisamurai@757
   151
}
lolisamurai@757
   152
slouken@797
   153
/*
slouken@797
   154
static const char *mpg123_format_str(int fmt)
lolisamurai@757
   155
{
lolisamurai@757
   156
    switch (fmt)
lolisamurai@757
   157
    {
lolisamurai@757
   158
#define f(x) case x: return #x;
lolisamurai@757
   159
        f(MPG123_ENC_UNSIGNED_8)
lolisamurai@757
   160
        f(MPG123_ENC_UNSIGNED_16)
lolisamurai@757
   161
        f(MPG123_ENC_SIGNED_8)
lolisamurai@757
   162
        f(MPG123_ENC_SIGNED_16)
lolisamurai@757
   163
        f(MPG123_ENC_SIGNED_32)
slouken@797
   164
        f(MPG123_ENC_FLOAT_32)
lolisamurai@757
   165
#undef f
lolisamurai@757
   166
    }
lolisamurai@757
   167
    return "unknown";
lolisamurai@757
   168
}
slouken@797
   169
*/
lolisamurai@757
   170
slouken@797
   171
static char const* mpg_err(mpg123_handle* mpg, int result)
lolisamurai@757
   172
{
lolisamurai@757
   173
    char const* err = "unknown error";
lolisamurai@757
   174
slouken@797
   175
    if (mpg && result == MPG123_ERR) {
slouken@787
   176
        err = mpg123.mpg123_strerror(mpg);
lolisamurai@757
   177
    } else {
slouken@797
   178
        err = mpg123.mpg123_plain_strerror(result);
lolisamurai@757
   179
    }
lolisamurai@757
   180
    return err;
lolisamurai@757
   181
}
lolisamurai@757
   182
lolisamurai@757
   183
/* we're gonna override mpg123's I/O with these wrappers for RWops */
slouken@797
   184
static ssize_t rwops_read(void* p, void* dst, size_t n)
slouken@797
   185
{
lolisamurai@757
   186
    return (ssize_t)SDL_RWread((SDL_RWops*)p, dst, 1, n);
lolisamurai@757
   187
}
lolisamurai@757
   188
slouken@797
   189
static off_t rwops_seek(void* p, off_t offset, int whence)
slouken@797
   190
{
lolisamurai@757
   191
    return (off_t)SDL_RWseek((SDL_RWops*)p, (Sint64)offset, whence);
lolisamurai@757
   192
}
lolisamurai@757
   193
slouken@797
   194
static void rwops_cleanup(void* p)
slouken@797
   195
{
lolisamurai@757
   196
    (void)p;
lolisamurai@757
   197
    /* do nothing, we will free the file later */
lolisamurai@757
   198
}
lolisamurai@757
   199
slouken@777
   200
slouken@777
   201
static int MPG123_Open(const SDL_AudioSpec *spec)
slouken@777
   202
{
slouken@787
   203
    if (mpg123.mpg123_init() != MPG123_OK) {
slouken@777
   204
        Mix_SetError("mpg123_init() failed");
slouken@777
   205
        return -1;
slouken@777
   206
    }
slouken@777
   207
    return 0;
slouken@777
   208
}
slouken@777
   209
slouken@777
   210
static void *MPG123_CreateFromRW(SDL_RWops *src, int freesrc)
lolisamurai@757
   211
{
slouken@797
   212
    MPG123_Music *music;
lolisamurai@757
   213
    int result;
slouken@797
   214
    const long *rates;
slouken@797
   215
    size_t i, num_rates;
lolisamurai@757
   216
slouken@797
   217
    music = (MPG123_Music*)SDL_calloc(1, sizeof(*music));
slouken@797
   218
    if (!music) {
slouken@797
   219
        return NULL;
slouken@797
   220
    }
slouken@797
   221
    music->src = src;
slouken@797
   222
    music->volume = MIX_MAX_VOLUME;
slouken@797
   223
slouken@797
   224
    /* Just assume 16-bit 2 channel audio for now */
slouken@797
   225
    music->buffer_size = music_spec.samples * sizeof(Sint16) * 2;
slouken@797
   226
    music->buffer = (unsigned char *)SDL_malloc(music->buffer_size);
slouken@797
   227
    if (!music->buffer) {
slouken@797
   228
        MPG123_Delete(music);
slouken@797
   229
        SDL_OutOfMemory();
slouken@797
   230
        return NULL;
slouken@797
   231
    }
slouken@797
   232
slouken@797
   233
    music->handle = mpg123.mpg123_new(0, &result);
slouken@797
   234
    if (result != MPG123_OK) {
slouken@797
   235
        MPG123_Delete(music);
slouken@797
   236
        Mix_SetError("mpg123_new failed");
slouken@797
   237
        return NULL;
slouken@797
   238
    }
slouken@797
   239
slouken@797
   240
    result = mpg123.mpg123_replace_reader_handle(
slouken@797
   241
        music->handle,
slouken@797
   242
        rwops_read, rwops_seek, rwops_cleanup
slouken@797
   243
    );
slouken@797
   244
    if (result != MPG123_OK) {
slouken@797
   245
        MPG123_Delete(music);
slouken@797
   246
        Mix_SetError("mpg123_replace_reader_handle: %s", mpg_err(music->handle, result));
slouken@797
   247
        return NULL;
slouken@797
   248
    }
slouken@797
   249
slouken@797
   250
    result = mpg123.mpg123_format_none(music->handle);
slouken@797
   251
    if (result != MPG123_OK) {
slouken@797
   252
        MPG123_Delete(music);
slouken@797
   253
        Mix_SetError("mpg123_format_none: %s", mpg_err(music->handle, result));
slouken@797
   254
        return NULL;
slouken@797
   255
    }
slouken@797
   256
slouken@797
   257
    mpg123.mpg123_rates(&rates, &num_rates);
slouken@797
   258
    for (i = 0; i < num_rates; ++i) {
slouken@797
   259
        const int channels = (MPG123_MONO|MPG123_STEREO);
slouken@797
   260
        const int formats = (MPG123_ENC_SIGNED_8 |
slouken@797
   261
                             MPG123_ENC_UNSIGNED_8 |
slouken@797
   262
                             MPG123_ENC_SIGNED_16 |
slouken@797
   263
                             MPG123_ENC_UNSIGNED_16 |
slouken@797
   264
                             MPG123_ENC_SIGNED_32 |
slouken@797
   265
                             MPG123_ENC_FLOAT_32);
slouken@797
   266
slouken@797
   267
        mpg123.mpg123_format(music->handle, rates[i], channels, formats);
slouken@797
   268
    }
slouken@797
   269
slouken@797
   270
    result = mpg123.mpg123_open_handle(music->handle, music->src);
slouken@797
   271
    if (result != MPG123_OK) {
slouken@797
   272
        MPG123_Delete(music);
slouken@797
   273
        Mix_SetError("mpg123_open_handle: %s", mpg_err(music->handle, result));
slouken@797
   274
        return NULL;
slouken@797
   275
    }
slouken@797
   276
slouken@797
   277
    music->freesrc = freesrc;
slouken@797
   278
    return music;
slouken@797
   279
}
slouken@797
   280
slouken@797
   281
static void MPG123_SetVolume(void *context, int volume)
slouken@797
   282
{
slouken@797
   283
    MPG123_Music *music = (MPG123_Music *)context;
slouken@797
   284
    music->volume = volume;
slouken@797
   285
}
slouken@797
   286
slouken@797
   287
static int MPG123_Play(void *context, int play_count)
slouken@797
   288
{
slouken@797
   289
    MPG123_Music *music = (MPG123_Music *)context;
slouken@797
   290
    music->play_count = play_count;
slouken@797
   291
    return MPG123_Seek(music, 0.0);
slouken@797
   292
}
slouken@797
   293
slouken@797
   294
/* read some mp3 stream data and convert it for output */
slouken@797
   295
static int MPG123_GetSome(void *context, void *data, int bytes, SDL_bool *done)
slouken@797
   296
{
slouken@797
   297
    MPG123_Music *music = (MPG123_Music *)context;
slouken@797
   298
    int filled, result;
slouken@797
   299
    size_t amount;
slouken@797
   300
    long rate;
slouken@797
   301
    int channels, encoding, format;
slouken@797
   302
slouken@797
   303
    if (music->stream) {
slouken@797
   304
        filled = SDL_AudioStreamGet(music->stream, data, bytes);
slouken@797
   305
        if (filled != 0) {
slouken@797
   306
            return filled;
slouken@797
   307
        }
slouken@797
   308
    }
slouken@797
   309
slouken@797
   310
    if (!music->play_count) {
slouken@797
   311
        /* All done */
slouken@797
   312
        *done = SDL_TRUE;
lolisamurai@757
   313
        return 0;
lolisamurai@757
   314
    }
lolisamurai@757
   315
slouken@797
   316
    result = mpg123.mpg123_read(music->handle, music->buffer, music->buffer_size, &amount);
slouken@797
   317
    switch (result) {
slouken@797
   318
    case MPG123_OK:
slouken@797
   319
        if (SDL_AudioStreamPut(music->stream, music->buffer, (int)amount) < 0) {
slouken@797
   320
            return -1;
slouken@797
   321
        }
slouken@797
   322
        break;
lolisamurai@757
   323
slouken@797
   324
    case MPG123_NEW_FORMAT:
slouken@797
   325
        result = mpg123.mpg123_getformat(music->handle, &rate, &channels, &encoding);
slouken@797
   326
        if (result != MPG123_OK) {
slouken@797
   327
            Mix_SetError("mpg123_getformat: %s", mpg_err(music->handle, result));
slouken@797
   328
            return -1;
slouken@797
   329
        }
slouken@797
   330
/*printf("MPG123 format: %s, channels = %d, rate = %ld\n", mpg123_format_str(encoding), channels, rate);*/
slouken@797
   331
slouken@797
   332
        format = mpg123_format_to_sdl(encoding);
slouken@797
   333
        SDL_assert(format != -1);
slouken@797
   334
slouken@797
   335
        music->stream = SDL_NewAudioStream(format, channels, (int)rate,
slouken@797
   336
                                           music_spec.format, music_spec.channels, music_spec.freq);
slouken@797
   337
        if (!music->stream) {
slouken@797
   338
            return -1;
slouken@797
   339
        }
slouken@797
   340
        break;
slouken@797
   341
slouken@797
   342
    case MPG123_DONE:
slouken@797
   343
        if (music->play_count == 1) {
slouken@797
   344
            music->play_count = 0;
slouken@797
   345
            SDL_AudioStreamFlush(music->stream);
slouken@797
   346
        } else {
slouken@797
   347
            int play_count = -1;
slouken@797
   348
            if (music->play_count > 0) {
slouken@797
   349
                play_count = (music->play_count - 1);
slouken@797
   350
            }
slouken@797
   351
            if (MPG123_Play(music, play_count) < 0) {
slouken@797
   352
                return -1;
slouken@797
   353
            }
slouken@797
   354
        }
slouken@797
   355
        break;
slouken@797
   356
    default:
slouken@797
   357
        Mix_SetError("mpg123_read: %s", mpg_err(music->handle, result));
slouken@797
   358
        return -1;
lolisamurai@757
   359
    }
slouken@777
   360
    return 0;
lolisamurai@757
   361
}
slouken@777
   362
static int MPG123_GetAudio(void *context, void *data, int bytes)
lolisamurai@757
   363
{
slouken@797
   364
    MPG123_Music *music = (MPG123_Music *)context;
slouken@797
   365
    return music_pcm_getaudio(context, data, bytes, music->volume, MPG123_GetSome);
lolisamurai@757
   366
}
lolisamurai@757
   367
slouken@777
   368
static int MPG123_Seek(void *context, double secs)
lolisamurai@757
   369
{
slouken@797
   370
    MPG123_Music *music = (MPG123_Music *)context;
slouken@797
   371
    off_t offset = (off_t)(music_spec.freq * secs);
lolisamurai@757
   372
slouken@797
   373
    if ((offset = mpg123.mpg123_seek(music->handle, offset, SEEK_SET)) < 0) {
slouken@797
   374
        return Mix_SetError("mpg123_seek: %s", mpg_err(music->handle, (int)-offset));
lolisamurai@757
   375
    }
slouken@777
   376
    return 0;
lolisamurai@757
   377
}
lolisamurai@757
   378
slouken@777
   379
static void MPG123_Delete(void *context)
slouken@777
   380
{
slouken@797
   381
    MPG123_Music *music = (MPG123_Music *)context;
lolisamurai@757
   382
slouken@797
   383
    if (music->handle) {
slouken@797
   384
        mpg123.mpg123_close(music->handle);
slouken@797
   385
        mpg123.mpg123_delete(music->handle);
slouken@777
   386
    }
slouken@797
   387
    if (music->stream) {
slouken@797
   388
        SDL_FreeAudioStream(music->stream);
slouken@777
   389
    }
slouken@797
   390
    if (music->buffer) {
slouken@797
   391
        SDL_free(music->buffer);
slouken@797
   392
    }
slouken@797
   393
    if (music->freesrc) {
slouken@797
   394
        SDL_RWclose(music->src);
slouken@797
   395
    }
slouken@797
   396
    SDL_free(music);
slouken@777
   397
}
slouken@777
   398
slouken@777
   399
static void MPG123_Close(void)
slouken@777
   400
{
slouken@787
   401
    mpg123.mpg123_exit();
slouken@777
   402
}
slouken@777
   403
slouken@777
   404
Mix_MusicInterface Mix_MusicInterface_MPG123 =
slouken@777
   405
{
slouken@777
   406
    "MPG123",
slouken@777
   407
    MIX_MUSIC_MPG123,
slouken@777
   408
    MUS_MP3,
slouken@777
   409
    SDL_FALSE,
slouken@777
   410
    SDL_FALSE,
slouken@777
   411
slouken@787
   412
    MPG123_Load,
slouken@777
   413
    MPG123_Open,
slouken@777
   414
    MPG123_CreateFromRW,
slouken@777
   415
    NULL,   /* CreateFromFile */
slouken@777
   416
    MPG123_SetVolume,
slouken@777
   417
    MPG123_Play,
slouken@797
   418
    NULL,   /* IsPlaying */
slouken@777
   419
    MPG123_GetAudio,
slouken@777
   420
    MPG123_Seek,
slouken@777
   421
    NULL,   /* Pause */
slouken@777
   422
    NULL,   /* Resume */
slouken@797
   423
    NULL,   /* Stop */
slouken@777
   424
    MPG123_Delete,
slouken@777
   425
    MPG123_Close,
slouken@787
   426
    MPG123_Unload
slouken@777
   427
};
slouken@777
   428
slouken@777
   429
#endif /* MUSIC_MP3_MPG123 */
slouken@777
   430
slouken@777
   431
/* vi: set ts=4 sw=4 expandtab: */