diff --git a/Changelog b/Changelog index a3e9f631..4bc1add8 100644 --- a/Changelog +++ b/Changelog @@ -3,11 +3,19 @@ Stable versions 4.2.1 (?): Changes by Alice Rowan + - Support 24-bit and 32-bit output for the PulseAudio, ALSA, + WAV, file drivers (requires libxmp 4.7.0+) (FIXME). + - Use XMP_MAX_SRATE as the limit for -f instead of 48000 + (requires libxmp 4.7.0+). - Report pan value "---" for instruments/samples without a default panning value (sub->pan < 0). - Don't unmute muted IT channels unless explicitly unmuted by the -S/--solo option. - - Use XMP_MAX_SRATE as the limit for -f instead of 48000. + - wav: fix incorrect RIFF length in output. + - wav: add terminal pad byte after odd length data chunks. + - file: fix incorrect handling of -Dendian ("big" would output + little endian, anything else would output big endian). + - pulseaudio: support 8-bit output. 4.2.0 (20230615): Changes by Özkan Sezer: diff --git a/README b/README index 8b4e9d11..fdcbdd2a 100644 --- a/README +++ b/README @@ -59,7 +59,7 @@ directly to cmatsuoka@gmail.com. LICENSE Extended Module Player -Copyright (C) 1996-2022 Claudio Matsuoka and Hipolito Carraro Jr +Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr 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 diff --git a/src/common.h b/src/common.h index 6df19f8d..aac5154b 100644 --- a/src/common.h +++ b/src/common.h @@ -22,6 +22,11 @@ #include +/* Allow building with <4.7.0 for now... */ +#if XMP_VERCODE < 0x040700 +#define XMP_FORMAT_32BIT (1 << 3) +#endif + struct player_mode { const char *name; const char *desc; @@ -33,6 +38,7 @@ struct options { int amplify; /* amplification factor */ int rate; /* sampling rate */ int format; /* sample format */ + int format_downmix; /* XMP_FORMAT_32BIT may require downmix to 24 */ int max_time; /* max. replay time */ int mix; /* channel separation */ int defpan; /* default pan */ diff --git a/src/main.c b/src/main.c index 780959c7..275d607d 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -335,13 +335,22 @@ int main(int argc, char **argv) } if (opt.verbose > 0) { + int format_bits; + if (opt.format & XMP_FORMAT_32BIT) { + format_bits = opt.format_downmix == 24 ? 24 : 32; + } else if (~opt.format & XMP_FORMAT_8BIT) { + format_bits = 16; + } else { + format_bits = 8; + } + report("Extended Module Player " VERSION "\n" - "Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr\n"); + "Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr\n"); report("Using %s\n", sound->description()); - report("Mixer set to %d Hz, %dbit, %s%s%s\n", opt.rate, - opt.format & XMP_FORMAT_8BIT ? 8 : 16, + report("Mixer set to %d Hz, %dbit, %s%s%s%s\n", opt.rate, format_bits, + opt.format & XMP_FORMAT_UNSIGNED ? "unsigned " : "signed ", opt.interp == XMP_INTERP_LINEAR ? "linear interpolated " : opt.interp == XMP_INTERP_SPLINE ? "cubic spline interpolated " : "", opt.format & XMP_FORMAT_MONO ? "mono" : "stereo", diff --git a/src/options.c b/src/options.c index 27fb9045..856f64d4 100644 --- a/src/options.c +++ b/src/options.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -96,7 +96,7 @@ static void usage(char *s, struct options *options) "\nMixer options:\n" " -A --amiga Use Paula simulation mixer in Amiga formats\n" " -a --amplify {0|1|2|3} Amplification factor: 0=Normal, 1=x2, 2=x4, 3=x8\n" -" -b --bits {8|16} Software mixer resolution (8 or 16 bits)\n" +" -b --bits {8|16|24|32} Software mixer resolution (8, 16, 24, or 32 bits)\n" " -c --stdout Mix the module to stdout\n" " -f --frequency rate Sampling rate in hertz (default 44100)\n" " -i --interpolation {nearest|linear|spline}\n" @@ -190,8 +190,19 @@ void get_options(int argc, char **argv, struct options *options) options->amplify = atoi(optarg); break; case 'b': - if (atoi(optarg) == 8) { + options->format &= ~(XMP_FORMAT_8BIT | XMP_FORMAT_32BIT); + options->format_downmix = 0; + switch (atoi(optarg)) { + case 8: options->format |= XMP_FORMAT_8BIT; + break; + case 24: + options->format |= XMP_FORMAT_32BIT; + options->format_downmix = 24; + break; + case 32: + options->format |= XMP_FORMAT_32BIT; + break; } break; case 'C': @@ -387,7 +398,11 @@ void get_options(int argc, char **argv, struct options *options) if (xmp_vercode < 0x040700) { if (options->rate > 48000) - options->rate = 48000; /* Old max. rate 48kHz */ + options->rate = 48000; /* Old max. rate 48 kHz */ + + options->format &= ~XMP_FORMAT_32BIT; + options->format_downmix = 0; + } /* apply guess if no driver selected */ diff --git a/src/sound.c b/src/sound.c index 2bacd6fb..1c0a94b5 100644 --- a/src/sound.c +++ b/src/sound.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -101,16 +101,105 @@ const struct sound_driver *select_sound_driver(struct options *options) return NULL; } -/* Convert little-endian 16 bit samples to big-endian */ -void convert_endian(unsigned char *p, int l) +/* Downmix 32-bit to 24-bit aligned (in-place) */ +void downmix_32_to_24_aligned(unsigned char *buffer, int buffer_bytes) +{ + /* Note: most 24-bit support ignores the high byte. + * sndio, however, expects 24-bit to be sign extended. */ + int *buf32 = (int *)buffer; + int i; + + for (i = 0; i < buffer_bytes; i += 4) { + *buf32 >>= 8; + buf32++; + } +} + +/* Downmix 32-bit to 24-bit packed (in-place). + * Returns the new number of useful bytes in the buffer. */ +int downmix_32_to_24_packed(unsigned char *buffer, int buffer_bytes) +{ + unsigned char *out = buffer; + int buffer_samples = buffer_bytes >> 2; + int i; + + /* Big endian 32-bit (22 11 00 XX) -> 24-bit (22 11 00) + * Little endian 32-bit (XX 00 11 22) -> 24-bit (00 11 22) + * Skip the first byte for little endian to allow reusing the same loop. + */ + if (!is_big_endian()) { + buffer++; + } + + for (i = 0; i < buffer_samples; i++) { + out[0] = buffer[0]; + out[1] = buffer[1]; + out[2] = buffer[2]; + out += 3; + buffer += 4; + } + + return buffer_samples * 3; +} + + +/* Convert native endian 16-bit samples for file IO */ +void convert_endian_16bit(unsigned char *buffer, int buffer_bytes) { unsigned char b; int i; - for (i = 0; i < l; i++) { - b = p[0]; - p[0] = p[1]; - p[1] = b; - p += 2; + for (i = 0; i < buffer_bytes; i += 2) { + b = buffer[0]; + buffer[0] = buffer[1]; + buffer[1] = b; + buffer += 2; + } +} + +/* Convert native endian 24-bit unaligned samples for file IO */ +void convert_endian_24bit(unsigned char *buffer, int buffer_bytes) +{ + unsigned char b; + int i; + + for (i = 0; i < buffer_bytes; i += 3) { + b = buffer[0]; + buffer[0] = buffer[2]; + buffer[2] = b; + buffer += 3; + } +} + +/* Convert native endian 32-bit samples for file IO */ +void convert_endian_32bit(unsigned char *buffer, int buffer_bytes) +{ + unsigned char a, b; + int i; + + for (i = 0; i < buffer_bytes; i += 4) { + a = buffer[0]; + b = buffer[1]; + buffer[0] = buffer[3]; + buffer[1] = buffer[2]; + buffer[2] = b; + buffer[3] = a; + buffer += 4; + } +} + +/* Convert native endian 16-bit, 24-bit, or 32-bit samples for file IO */ +void convert_endian(unsigned char *buffer, int buffer_bytes, int bits) +{ + switch (bits) { + case 16: + convert_endian_16bit(buffer, buffer_bytes); + break; + case 24: + convert_endian_24bit(buffer, buffer_bytes); + break; + case 32: + convert_endian_32bit(buffer, buffer_bytes); + break; } } diff --git a/src/sound.h b/src/sound.h index b8eaa6ff..297379c7 100644 --- a/src/sound.h +++ b/src/sound.h @@ -58,13 +58,59 @@ extern const struct sound_driver *const sound_driver_list[]; #define chkparm2(x,y,z,w) { if (!strcmp(s, x)) { \ if (2 > sscanf(token, y, z, w)) parm_error(); } } -static inline int is_big_endian(void) { +static inline int is_big_endian(void) +{ unsigned short w = 0x00ff; return (*(char *)&w == 0x00); } +static inline int get_bits_from_format(const struct options *options) +{ + if ((options->format & XMP_FORMAT_32BIT) && options->format_downmix != 24) { + return 32; + } else if (options->format & XMP_FORMAT_32BIT) { + return 24; + } else if (~options->format & XMP_FORMAT_8BIT) { + return 16; + } else { + return 8; + } +} + +static inline int get_signed_from_format(const struct options *options) +{ + return !(options->format & XMP_FORMAT_UNSIGNED); +} + +static inline void update_format(struct options *options, int bits, int is_signed) +{ + options->format &= ~(XMP_FORMAT_32BIT | XMP_FORMAT_8BIT | XMP_FORMAT_UNSIGNED); + options->format_downmix = 0; + + switch (bits) { + case 8: + options->format |= XMP_FORMAT_8BIT; + break; + case 24: + options->format |= XMP_FORMAT_32BIT; + options->format_downmix = 24; + break; + case 32: + options->format |= XMP_FORMAT_32BIT; + break; + } + if (!is_signed) { + options->format |= XMP_FORMAT_UNSIGNED; + } +} + void init_sound_drivers(void); const struct sound_driver *select_sound_driver(struct options *); -void convert_endian(unsigned char *, int); +void downmix_32_to_24_aligned(unsigned char *, int); +int downmix_32_to_24_packed(unsigned char *, int); +void convert_endian_16bit(unsigned char *, int); +void convert_endian_24bit(unsigned char *, int); +void convert_endian_32bit(unsigned char *, int); +void convert_endian(unsigned char *, int, int); #endif diff --git a/src/sound_ahi.c b/src/sound_ahi.c index 0f3efb61..7b7bbae9 100644 --- a/src/sound_ahi.c +++ b/src/sound_ahi.c @@ -92,6 +92,7 @@ static int init(struct options *options) { if (AHIBuf[1]) { /* driver is initialized before calling libxmp, so this is OK : */ options->format &= ~XMP_FORMAT_UNSIGNED;/* no unsigned with AHI */ + options->format &= ~XMP_FORMAT_32BIT; return 0; } } diff --git a/src/sound_aiff.c b/src/sound_aiff.c index a25c09ae..e7c01008 100644 --- a/src/sound_aiff.c +++ b/src/sound_aiff.c @@ -99,6 +99,7 @@ static int init(struct options *options) } else { fd = stdout; } + options->format &= ~XMP_FORMAT_32BIT; fwrite(hed, 1, 54, fd); @@ -107,8 +108,8 @@ static int init(struct options *options) static void play(void *b, int len) { - if (swap_endian && bits == 16) { - convert_endian((unsigned char *)b, len); + if (swap_endian) { + convert_endian((unsigned char *)b, len, bits); } fwrite(b, 1, len, fd); size += len; diff --git a/src/sound_aix.c b/src/sound_aix.c index b607a951..224e15d0 100644 --- a/src/sound_aix.c +++ b/src/sound_aix.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -83,6 +83,8 @@ static int init(struct options *options) close(audio_fd); return -1; } + options->format &= ~XMP_FORMAT_32BIT; + return 0; } diff --git a/src/sound_alsa.c b/src/sound_alsa.c index 89050cf5..94df0e41 100644 --- a/src/sound_alsa.c +++ b/src/sound_alsa.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -11,6 +11,7 @@ #include "sound.h" static snd_pcm_t *pcm_handle; +static int downmix_24; static int init(struct options *options) { @@ -39,12 +40,20 @@ static int init(struct options *options) } channels = format & XMP_FORMAT_MONO ? 1 : 2; - if (format & XMP_FORMAT_UNSIGNED) { - fmt = format & XMP_FORMAT_8BIT ? - SND_PCM_FORMAT_U8 : SND_PCM_FORMAT_U16; + downmix_24 = 0; + if ((format & XMP_FORMAT_32BIT) && options->format_downmix != 24) { + fmt = format & XMP_FORMAT_UNSIGNED ? + SND_PCM_FORMAT_U32 : SND_PCM_FORMAT_S32; + } else if ((format & XMP_FORMAT_32BIT) && options->format_downmix == 24) { + fmt = format & XMP_FORMAT_UNSIGNED ? + SND_PCM_FORMAT_U24 : SND_PCM_FORMAT_S24; + downmix_24 = 1; + } else if (~format & XMP_FORMAT_8BIT) { + fmt = format & XMP_FORMAT_UNSIGNED ? + SND_PCM_FORMAT_U16 : SND_PCM_FORMAT_S16; } else { - fmt = format & XMP_FORMAT_8BIT ? - SND_PCM_FORMAT_S8 : SND_PCM_FORMAT_S16; + fmt = format & XMP_FORMAT_UNSIGNED ? + SND_PCM_FORMAT_U8 : SND_PCM_FORMAT_S8; } snd_pcm_hw_params_alloca(&hwparams); @@ -84,6 +93,10 @@ static void play(void *b, int i) { int frames = snd_pcm_bytes_to_frames(pcm_handle, i); + if (downmix_24) { + downmix_32_to_24_aligned((unsigned char *)b, i); + } + if (snd_pcm_writei(pcm_handle, b, frames) < 0) { snd_pcm_prepare(pcm_handle); } diff --git a/src/sound_alsa05.c b/src/sound_alsa05.c index a6dbf486..9ef8ad2d 100644 --- a/src/sound_alsa05.c +++ b/src/sound_alsa05.c @@ -159,6 +159,7 @@ static int init(struct options *options) printf("Unable to setup channel: %s\n", snd_strerror(rc)); return -1; } + options->format &= ~XMP_FORMAT_32BIT; return 0; } diff --git a/src/sound_beos.cpp b/src/sound_beos.cpp index 95ae6077..70b9fa2f 100644 --- a/src/sound_beos.cpp +++ b/src/sound_beos.cpp @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -121,7 +121,7 @@ static int read_buffer(unsigned char *data, int len) return len; } -static void render_proc(void *theCookie, void *buffer, size_t req, +static void render_proc(void *theCookie, void *buffer, size_t req, const media_raw_audio_format &format) { size_t amt; @@ -161,6 +161,7 @@ static int init(struct options *options) player = new BSoundPlayer(&fmt, "xmp output", render_proc); + options->format &= ~ XMP_FORMAT_32BIT; return 0; } @@ -182,7 +183,7 @@ static void play(void *b, int i) } if (paused) { - player->Start(); + player->Start(); player->SetHasData(true); paused = 0; } @@ -190,7 +191,7 @@ static void play(void *b, int i) static void deinit(void) { - player->Stop(); + player->Stop(); be_app->Lock(); be_app->Quit(); } diff --git a/src/sound_bsd.c b/src/sound_bsd.c index 11b1d0db..93c0d3d9 100644 --- a/src/sound_bsd.c +++ b/src/sound_bsd.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -60,6 +60,7 @@ static int init(struct options *options) close(audio_fd); return -1; } + options->format &= ~XMP_FORMAT_32BIT; return 0; } diff --git a/src/sound_coreaudio.c b/src/sound_coreaudio.c index aa47e4c7..67679fb2 100644 --- a/src/sound_coreaudio.c +++ b/src/sound_coreaudio.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -208,6 +208,7 @@ static int init(struct options *options) kAudioUnitScope_Input, 0, &rc, sizeof(rc)))) goto err2; + options->format &= ~XMP_FORMAT_32BIT; return 0; err2: diff --git a/src/sound_dart.c b/src/sound_dart.c index 57d92a49..97022076 100644 --- a/src/sound_dart.c +++ b/src/sound_dart.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -152,6 +152,7 @@ static int init(struct options *options) memset(MixBuffers[1].pBuffer, /*32767 */ 0, bsize); MixSetupParms.pmixWrite(MixSetupParms.ulMixHandle, MixBuffers, 2); + options->format &= ~XMP_FORMAT_32BIT; return 0; } diff --git a/src/sound_file.c b/src/sound_file.c index a794a07c..52507e17 100644 --- a/src/sound_file.c +++ b/src/sound_file.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -11,6 +11,7 @@ #include "sound.h" static FILE *fd; +static int bits; static long size; static int swap_endian; @@ -18,11 +19,19 @@ static int init(struct options *options) { char **parm = options->driver_parm; + if (options->format & XMP_FORMAT_32BIT) { + bits = options->format_downmix == 24 ? 24 : 32; + } else if (~options->format & XMP_FORMAT_8BIT) { + bits = 16; + } else { + bits = 8; + } + swap_endian = 0; parm_init(parm); chkparm1("endian", - swap_endian = (is_big_endian() ^ strcmp(token, "big"))); + swap_endian = (is_big_endian() ^ !strcmp(token, "big"))); parm_end(); if (options->out_file == NULL) { @@ -42,8 +51,11 @@ static int init(struct options *options) static void play(void *b, int len) { + if (bits == 24) { + len = downmix_32_to_24_packed((unsigned char *)b, len); + } if (swap_endian) { - convert_endian((unsigned char *)b, len); + convert_endian((unsigned char *)b, len, bits); } fwrite(b, 1, len, fd); size += len; diff --git a/src/sound_hpux.c b/src/sound_hpux.c index d7fc02ba..395060b7 100644 --- a/src/sound_hpux.c +++ b/src/sound_hpux.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -101,6 +101,7 @@ static int init(struct options *options) ioctl(audio_fd, AUDIO_SET_TXBUFSIZE, bsize); + options->format &= ~XMP_FORMAT_32BIT; return 0; err1: diff --git a/src/sound_netbsd.c b/src/sound_netbsd.c index ff9892e8..e0dd505a 100644 --- a/src/sound_netbsd.c +++ b/src/sound_netbsd.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -22,14 +22,76 @@ static int audio_fd; static int audioctl_fd; +static const struct options *opts; +#define MATCH_ENCODING(s, r) \ + ((r) == AUDIO_ENCODING_ULINEAR ? \ + ((s) == AUDIO_ENCODING_ULINEAR || (s) == native_unsigned) : \ + ((s) == AUDIO_ENCODING_SLINEAR || (s) == native_signed)) + +#define ANY_LINEAR(s) \ + ((s) == AUDIO_ENCODING_ULINEAR || (s) == native_unsigned || \ + (s) == AUDIO_ENCODING_SLINEAR || (s) == native_signed) + +static void to_fmt(int *precision, int *encoding, const struct options *options) +{ + /* The NE defines only exist in kernel space for some reason... */ + int native_signed = is_big_endian() ? AUDIO_ENCODING_SLINEAR_BE : + AUDIO_ENCODING_SLINEAR_LE; + int native_unsigned = is_big_endian() ? AUDIO_ENCODING_ULINEAR_BE : + AUDIO_ENCODING_ULINEAR_LE; + + audio_encoding_t enc; + int match_precision = -1; + int match_encoding = -1; + int depth, uns, i; + + depth = get_bits_from_format(options); + uns = get_signed_from_format(options) ? AUDIO_ENCODING_SLINEAR : + AUDIO_ENCODING_ULINEAR; + + memset(&enc, 0, sizeof(enc)); + + for (i = 0; i < 100; i++) { + enc.index = i; + if (ioctl(audioctl_fd, AUDIO_GETENC, &enc) < 0) { + break; + } + if (enc.precision < depth || + (enc.precision > depth && match_precision == depth) || + !ANY_LINEAR(enc.encoding)) { + continue; + } + if (enc.precision == depth || MATCH_ENCODING(enc.encoding, uns)) { + match_precision = depth; + match_encoding = uns; + } + if (enc.precision == depth && MATCH_ENCODING(enc.encoding, uns)) { + break; + } + } + if (match_precision >= 0) { + *precision = match_precision; + *encoding = match_encoding; + } +} + +static int is_signed(int encoding) +{ + return encoding == AUDIO_ENCODING_SLINEAR || + encoding == AUDIO_ENCODING_SLINEAR_BE || + encoding == AUDIO_ENCODING_SLINEAR_LE; +} + static int init(struct options *options) { char **parm = options->driver_parm; audio_info_t ainfo; int gain = 128; int bsize = 32 * 1024; + int precision = 16; + int encoding = AUDIO_ENCODING_SLINEAR; parm_init(parm); chkparm1("gain", gain = strtoul(token, NULL, 0)); @@ -59,6 +121,8 @@ static int init(struct options *options) return -1; } + to_fmt(&precision, &encoding, options); + close(audioctl_fd); if (gain < AUDIO_MIN_GAIN) @@ -71,17 +135,8 @@ static int init(struct options *options) ainfo.mode = AUMODE_PLAY_ALL; ainfo.play.sample_rate = options->rate; ainfo.play.channels = options->format & XMP_FORMAT_MONO ? 1 : 2; - - if (options->format & XMP_FORMAT_8BIT) { - ainfo.play.precision = 8; - ainfo.play.encoding = AUDIO_ENCODING_ULINEAR; - options->format |= XMP_FORMAT_UNSIGNED; - } else { - ainfo.play.precision = 16; - ainfo.play.encoding = AUDIO_ENCODING_SLINEAR; - options->format &= ~XMP_FORMAT_UNSIGNED; - } - + ainfo.play.precision = precision; + ainfo.play.encoding = encoding; ainfo.play.gain = gain; ainfo.play.buffer_size = bsize; ainfo.blocksize = 0; @@ -91,6 +146,9 @@ static int init(struct options *options) return -1; } + update_format(options, precision, is_signed(encoding)); + opts = options; + return 0; } @@ -98,6 +156,10 @@ static void play(void *b, int i) { int j; + if (opts->format_downmix == 24) { + downmix_32_to_24_aligned(b, i); + } + while (i) { if ((j = write(audio_fd, b, i)) > 0) { i -= j; diff --git a/src/sound_oss.c b/src/sound_oss.c index 463744c8..8676f8e7 100644 --- a/src/sound_oss.c +++ b/src/sound_oss.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -40,21 +40,33 @@ static int audio_fd; static int fragnum, fragsize; static int do_sync = 1; +static const struct options *opts; static const char *desc_default = "OSS PCM audio"; static char descbuf[80] = {0}; -static int to_fmt(int format) +static int to_fmt(const struct options *options) { int fmt; - if (format & XMP_FORMAT_8BIT) + switch (get_bits_from_format(options)) { +#if defined(AFMT_S32_NE) + case 32: + return AFMT_S32_NE; +#endif +#if defined(AFMT_S24_NE) + case 24: + return AFMT_S24_NE; +#endif + case 8: fmt = AFMT_U8 | AFMT_S8; - else { + break; + default: fmt = AFMT_S16_NE | AFMT_U16_NE; + break; } - if (format & XMP_FORMAT_UNSIGNED) + if (!get_signed_from_format(options)) fmt &= AFMT_U8 | AFMT_U16_LE | AFMT_U16_BE; else fmt &= AFMT_S8 | AFMT_S16_LE | AFMT_S16_BE; @@ -62,22 +74,32 @@ static int to_fmt(int format) return fmt; } -static int from_fmt(int fmt) +static void from_fmt(struct options *options, int fmt) { - int format = 0; + int bits = 16; + int sgn = 1; +#if defined(AFMT_S32_NE) + if (fmt == AFMT_S32_NE) { + bits = 32; + } else +#endif +#if defined(AFMT_S24_NE) + if (fmt == AFMT_S24_NE) { + bits = 24; + } else +#endif if (!(fmt & (AFMT_S16_LE | AFMT_S16_BE | AFMT_U16_LE | AFMT_U16_BE))) { - format |= XMP_FORMAT_8BIT; + bits = 8; } if (fmt & (AFMT_U8 | AFMT_U16_LE | AFMT_U16_BE)) { - format |= XMP_FORMAT_UNSIGNED; + sgn = 0; } - - return format; + update_format(options, bits, sgn); } -static void setaudio(int *rate, int *format) +static void setaudio(struct options *options) { static int fragset = 0; int frag = 0; @@ -85,19 +107,19 @@ static void setaudio(int *rate, int *format) frag = (fragnum << 16) + fragsize; - fmt = to_fmt(*format); + fmt = to_fmt(options); ioctl(audio_fd, SNDCTL_DSP_SETFMT, &fmt); - *format = from_fmt(fmt); + from_fmt(options, fmt); - fmt = !(*format & XMP_FORMAT_MONO); + fmt = !(options->format & XMP_FORMAT_MONO); ioctl(audio_fd, SNDCTL_DSP_STEREO, &fmt); if (fmt) { - *format &= ~XMP_FORMAT_MONO; + options->format &= ~XMP_FORMAT_MONO; } else { - *format |= XMP_FORMAT_MONO; + options->format |= XMP_FORMAT_MONO; } - ioctl(audio_fd, SNDCTL_DSP_SPEED, rate); + ioctl(audio_fd, SNDCTL_DSP_SPEED, &options->rate); /* Set the fragments only once */ if (!fragset) { @@ -140,7 +162,8 @@ static int init(struct options *options) if (audio_fd < 0) return -1; - setaudio(&options->rate, &options->format); + setaudio(options); + opts = options; if (ioctl(audio_fd, SNDCTL_DSP_GETOSPACE, &info) == 0) { snprintf(descbuf, sizeof(descbuf), "%s [%d fragments of %d bytes]", @@ -158,6 +181,10 @@ static void play(void *b, int i) { int j; + if (opts->format_downmix == 24) { + downmix_32_to_24_aligned(b, i); + } + while (i) { if ((j = write(audio_fd, b, i)) > 0) { i -= j; diff --git a/src/sound_pulseaudio.c b/src/sound_pulseaudio.c index 4c9e8fbc..3ac0b729 100644 --- a/src/sound_pulseaudio.c +++ b/src/sound_pulseaudio.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -12,16 +12,45 @@ #include "sound.h" static pa_simple *s; +static int downmix_24; static int init(struct options *options) { pa_sample_spec ss; + int format = -1; int error; - options->format &= ~(XMP_FORMAT_UNSIGNED | XMP_FORMAT_8BIT); + downmix_24 = 0; + if ((options->format & XMP_FORMAT_32BIT) && options->format_downmix == 24) { +#ifdef PA_SAMPLE_S24_32NE + format = PA_SAMPLE_S24_32NE; + downmix_24 = 1; +#else + options->format_downmix = 0; +#endif + } + + if (format < 0 && (options->format & XMP_FORMAT_32BIT)) { +#ifdef PA_SAMPLE_S32NE + format = PA_SAMPLE_S32NE; +#else + options->format &= ~XMP_FORMAT_32BIT; +#endif + } + + if (format < 0 && (options->format & XMP_FORMAT_8BIT)) { + format = PA_SAMPLE_U8; + options->format |= XMP_FORMAT_UNSIGNED; + } else { + options->format &= ~XMP_FORMAT_UNSIGNED; + } - ss.format = PA_SAMPLE_S16NE; + if (format < 0) { + format = PA_SAMPLE_S16NE; + } + + ss.format = format; ss.channels = options->format & XMP_FORMAT_MONO ? 1 : 2; ss.rate = options->rate; @@ -47,6 +76,10 @@ static void play(void *b, int i) { int j, error; + if (downmix_24) { + downmix_32_to_24_aligned(b, i); + } + do { if ((j = pa_simple_write(s, b, i, &error)) > 0) { i -= j; diff --git a/src/sound_qnx.c b/src/sound_qnx.c index 009aa79f..7495039b 100644 --- a/src/sound_qnx.c +++ b/src/sound_qnx.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -66,6 +66,7 @@ static int init(struct options *options) goto error; } + options->format &= ~XMP_FORMAT_32BIT; return 0; error: diff --git a/src/sound_sb.c b/src/sound_sb.c index fd2a41fc..36206906 100644 --- a/src/sound_sb.c +++ b/src/sound_sb.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2021 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -113,6 +113,7 @@ static int init(struct options *options) fprintf(stderr, "Sound Blaster: DMA start failed.\n"); return -1; } + options->format &= ~XMP_FORMAT_32BIT; printf("Sound Blaster %s or compatible (%d bit, %s, %u Hz)\n", card, bits, mode, options->rate); diff --git a/src/sound_sgi.c b/src/sound_sgi.c index ed2c0147..436d3ff7 100644 --- a/src/sound_sgi.c +++ b/src/sound_sgi.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -132,6 +132,7 @@ static int init(struct options *options) if ((audio_port = ALopenport("xmp", "w", config)) == 0) return -1; + options->format &= ~XMP_FORMAT_32BIT; return 0; } diff --git a/src/sound_sndio.c b/src/sound_sndio.c index 14b37dfd..db740238 100644 --- a/src/sound_sndio.c +++ b/src/sound_sndio.c @@ -21,6 +21,7 @@ #include "sound.h" static struct sio_hdl *hdl; +static const struct options *opts; static int init(struct options *options) { @@ -38,15 +39,8 @@ static int init(struct options *options) par.le = SIO_LE_NATIVE; par.appbufsz = par.rate / 4; - if (options->format & XMP_FORMAT_8BIT) { - par.bits = 8; - par.sig = 0; - options->format |= XMP_FORMAT_UNSIGNED; - } else { - par.bits = 16; - par.sig = 1; - options->format &= ~XMP_FORMAT_UNSIGNED; - } + par.bits = get_bits_from_format(options); + par.sig = get_signed_from_format(options); askpar = par; if (!sio_setpar(hdl, &par) || !sio_getpar(hdl, &par)) { @@ -54,9 +48,8 @@ static int init(struct options *options) goto error; } - if ((par.bits == 16 && par.le != askpar.le) || - par.bits != askpar.bits || - par.sig != askpar.sig || + if ((par.bits > 8 && par.le != askpar.le) || + (par.bits != 8 && par.bits != 16 && par.bits != 24 && par.bits != 32) || par.pchan != askpar.pchan || (par.rate * 1000 < askpar.rate * 995) || (par.rate * 1000 > askpar.rate * 1005)) { @@ -68,6 +61,10 @@ static int init(struct options *options) fprintf(stderr, "%s: failed to start audio device\n", __func__); goto error; } + + update_format(options, par.bits, par.sig); + opts = options; + return 0; error: @@ -83,6 +80,10 @@ static void deinit(void) static void play(void *buf, int len) { + if (opts->format_downmix == 24) { + downmix_32_to_24_aligned(buf, len); + } + if (buf != NULL) { sio_write(hdl, buf, len); } diff --git a/src/sound_solaris.c b/src/sound_solaris.c index 275c71de..5f32e983 100644 --- a/src/sound_solaris.c +++ b/src/sound_solaris.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -137,6 +137,7 @@ static int init(struct options *options) /* sound_solaris.description = "Solaris CS4231 PCM audio"; */ } + options->format &= ~XMP_FORMAT_32BIT; return 0; } diff --git a/src/sound_wav.c b/src/sound_wav.c index 0aa78fe0..6c4be460 100644 --- a/src/sound_wav.c +++ b/src/sound_wav.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -10,10 +10,18 @@ #include #include "sound.h" +#define XMP_WAVE_FORMAT_PCM 0x0001 +#define XMP_WAVE_FORMAT_EXTENSIBLE 0xfffe + +static const char subformat[] = { + "\x00\x00\x00\x00\x10\x00\x80\x00\x00\xaa\x00\x38\x9b\x71" +}; + static FILE *fd; -static int format_16bit; +static int bits_per_sample; static int swap_endian; static long size; +static long data_size_offset; static void write_16l(FILE *f, unsigned short v) { @@ -47,7 +55,7 @@ static int init(struct options *options) { unsigned short chan; unsigned int sampling_rate, bytes_per_second; - unsigned short bytes_per_frame, bits_per_sample; + unsigned short bytes_per_frame; swap_endian = is_big_endian(); @@ -70,27 +78,47 @@ static int init(struct options *options) chan = options->format & XMP_FORMAT_MONO ? 1 : 2; sampling_rate = options->rate; - bits_per_sample = options->format & XMP_FORMAT_8BIT ? 8 : 16; + if (options->format & XMP_FORMAT_32BIT) { + bits_per_sample = options->format_downmix == 24 ? 24 : 32; + } else if (~options->format & XMP_FORMAT_8BIT) { + bits_per_sample = 16; + } else { + bits_per_sample = 8; + } + if (bits_per_sample == 8) { options->format |= XMP_FORMAT_UNSIGNED; - format_16bit = 0; } else { options->format &= ~XMP_FORMAT_UNSIGNED; - format_16bit = 1; } bytes_per_frame = chan * bits_per_sample / 8; bytes_per_second = sampling_rate * bytes_per_frame; fwrite("fmt ", 1, 4, fd); - write_32l(fd, 16); - write_16l(fd, 1); + if (bits_per_sample > 16) { + write_32l(fd, 40); + write_16l(fd, XMP_WAVE_FORMAT_EXTENSIBLE); + data_size_offset = 64; + } else { + write_32l(fd, 16); + write_16l(fd, XMP_WAVE_FORMAT_PCM); + data_size_offset = 40; + } write_16l(fd, chan); write_32l(fd, sampling_rate); write_32l(fd, bytes_per_second); write_16l(fd, bytes_per_frame); write_16l(fd, bits_per_sample); + if (bits_per_sample > 16) { + write_16l(fd, 22); /* size of extension */ + write_16l(fd, bits_per_sample); /* valid bits per sample */ + write_32l(fd, 0); /* chn position mask */ + write_16l(fd, XMP_WAVE_FORMAT_PCM); /* subformat code */ + fwrite(subformat, 1, 14, fd); /* subformat suffix */ + } + fwrite("data", 1, 4, fd); write_32l(fd, 0); /* will be written when finished */ @@ -101,8 +129,11 @@ static int init(struct options *options) static void play(void *b, int len) { - if (swap_endian && format_16bit) { - convert_endian((unsigned char *)b, len); + if (bits_per_sample == 24) { + len = downmix_32_to_24_packed((unsigned char *)b, len); + } + if (swap_endian) { + convert_endian((unsigned char *)b, len, bits_per_sample); } fwrite(b, 1, len, fd); size += len; @@ -110,11 +141,18 @@ static void play(void *b, int len) static void deinit(void) { - if (fseek(fd, 40, SEEK_SET) == 0) { + /* Pad chunks to word boundaries, even at the end of the file. */ + int pad = 0; + if (size & 1) { + fputc(0, fd); + pad = 1; + } + + if (fseek(fd, data_size_offset, SEEK_SET) == 0) { write_32l(fd, size); } if (fseek(fd, 4, SEEK_SET) == 0) { - write_32l(fd, size + 40); + write_32l(fd, size + pad + (data_size_offset + 4 - 8)); } if (fd && fd != stdout) { diff --git a/src/sound_win32.c b/src/sound_win32.c index 222bead2..c81ccdc4 100644 --- a/src/sound_win32.c +++ b/src/sound_win32.c @@ -1,5 +1,5 @@ /* Extended Module Player - * Copyright (C) 1996-2016 Claudio Matsuoka and Hipolito Carraro Jr + * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr * * This file is part of the Extended Module Player and is distributed * under the terms of the GNU General Public License. See the COPYING @@ -120,6 +120,7 @@ static int init(struct options *options) freebuffer = nextbuffer = 0; + options->format &= ~XMP_FORMAT_32BIT; return 0; } diff --git a/src/xmp.1 b/src/xmp.1 index b3488594..c521c5bd 100644 --- a/src/xmp.1 +++ b/src/xmp.1 @@ -59,8 +59,8 @@ factors may cause distorted or noisy output\&. .IP "\fB\-A, \-\-amiga\fP" Use a mixer which models the sound of an Amiga 500\&, with or without the led filter\&. .IP "\fB\-b, \-\-bits\fP \fIbits\fP" -Set the software mixer resolution (8 or 16 bits)\&. If omitted, -The audio device will be opened at the highest resolution available\&. +Set the software mixer resolution (8, 16, 24, or 32 bits)\&. If omitted, +The audio device will be opened at 16-bit resolution\&. .IP "\fB\-C, \-\-show\-comments\fP" Display module comment text, if any\&. .IP "\fB\-c, \-\-stdout\fP"