File: playwave.c
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2020-2025 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the “Software”), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 You can build this command-line app by running
  27 
  28 sudo apt install libpulse-dev
  29 cc -s -O3 -march=native -mtune=native -flto -o ./playwwave ./playwwave.c \
  30     -lpulse -lpulse-simple
  31 */
  32 
  33 #include <errno.h>
  34 #include <stdbool.h>
  35 #include <stdint.h>
  36 #include <stdio.h>
  37 #include <stdlib.h>
  38 #include <string.h>
  39 
  40 #ifdef _WIN32
  41 #include <fcntl.h>
  42 #include <windows.h>
  43 #endif
  44 
  45 #include <pulse/simple.h>
  46 
  47 #ifdef RED_ERRORS
  48 #define ERROR_STYLE "\x1b[38;2;204;0;0m"
  49 #ifdef __APPLE__
  50 #define ERROR_STYLE "\x1b[31m"
  51 #endif
  52 #define RESET_STYLE "\x1b[0m"
  53 #else
  54 #define ERROR_STYLE
  55 #define RESET_STYLE
  56 #endif
  57 
  58 #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n")
  59 
  60 const char* info = ""
  61 "playwwave [options...] [volume...]\n"
  62 "\n"
  63 "Play WAV-format data, either from the file given, or from standard input.\n"
  64 "Only 8-bit and 16-bit integer samples are currently supported.\n"
  65 "\n"
  66 "Options, all of which can start with either 1 or 2 dashes:\n"
  67 "\n"
  68 "  -h          show this help message\n"
  69 "  -help       show this help message\n"
  70 "";
  71 
  72 const char* pa_init_error = "can't use pulse-audio for playback";
  73 const char* pa_write_error = "can't keep playing data";
  74 
  75 // read_uint16_le reads a 16-bit little-endian integer platform-independently
  76 bool read_uint16_le(FILE* r, uint16_t* v) {
  77     const int a = fgetc(r);
  78     if (a == EOF) {
  79         return false;
  80     }
  81     const int b = fgetc(r);
  82     if (b == EOF) {
  83         return false;
  84     }
  85 
  86     *v = (b << 8) + a;
  87     return true;
  88 }
  89 
  90 // read_uint32_le reads a 32-bit little-endian integer platform-independently
  91 bool read_uint32_le(FILE* r, uint32_t* v) {
  92     const int a = fgetc(r);
  93     if (a == EOF) {
  94         return false;
  95     }
  96     const int b = fgetc(r);
  97     if (b == EOF) {
  98         return false;
  99     }
 100     const int c = fgetc(r);
 101     if (c == EOF) {
 102         return false;
 103     }
 104     const int d = fgetc(r);
 105     if (d == EOF) {
 106         return false;
 107     }
 108 
 109     *v = (a << 0) + (b << 8) + (c << 16) + (d << 24);
 110     return true;
 111 }
 112 
 113 // wave_info has the wav-format metadata needed to playback sound samples
 114 typedef struct wave_info {
 115     uint32_t fmt_chunk_size;
 116     uint16_t pcm_format;
 117     uint16_t channels;
 118     uint32_t sample_rate;
 119     uint32_t byte_rate;
 120     uint16_t bytes_per_sample_round; // could have a better name
 121     uint16_t bits_per_sample;
 122     uint32_t data_size;
 123 } wave_info;
 124 
 125 bool demand_string(FILE* r, const char* s) {
 126     for (; *s != 0; s++) {
 127         const int b = fgetc(r);
 128         if ((b == EOF) || (b != *s)) {
 129             return false;
 130         }
 131     }
 132     return true;
 133 }
 134 
 135 void bad_input(const char* msg) {
 136     fprintf(stderr, ERROR_LINE("invalid input data: %s"), msg);
 137 }
 138 
 139 bool skip_bytes(FILE* r, size_t n) {
 140     unsigned char buf[32 * 1024];
 141     while (n >= sizeof(buf)) {
 142         size_t got = fread(buf, 1, sizeof(buf), r);
 143         n -= got;
 144     }
 145 
 146     if ((n > 0) && (fread(buf, 1, n, r) != n)) {
 147         return false;
 148     }
 149     return !feof(r);
 150 }
 151 
 152 bool seek_data_section(FILE* r) {
 153     while (!feof(r)) {
 154         const int a = fgetc(r);
 155         const int b = fgetc(r);
 156         const int c = fgetc(r);
 157         const int d = fgetc(r);
 158         if (feof(r)) {
 159             return false;
 160         }
 161 
 162         if ((a == 'd') && (b == 'a') && (c == 't') && (d == 'a')) {
 163             return true;
 164         }
 165 
 166         uint32_t n = 0;
 167         if (!read_uint32_le(r, &n)) {
 168             return false;
 169         }
 170         if (!skip_bytes(r, n)) {
 171             return false;
 172         }
 173     }
 174 
 175     return false;
 176 }
 177 
 178 bool read_wave_intro(FILE* r, wave_info* info) {
 179     if (feof(r)) {
 180         bad_input("empty input");
 181         return false;
 182     }
 183 
 184     uint32_t total_size = 0;
 185     if (!demand_string(r, "RIFF")) {
 186         bad_input("no RIFF WAVE tag");
 187         return false;
 188     }
 189     if (!read_uint32_le(r, &total_size)) {
 190         bad_input("missing RIFF WAVE total-size");
 191         return false;
 192     }
 193     if (!demand_string(r, "WAVEfmt ")) {
 194         bad_input("missing RIFF WAVE format info");
 195         return false;
 196     }
 197 
 198     if (!read_uint32_le(r, &info->fmt_chunk_size)) {
 199         bad_input("missing size of RIFF WAVE format info");
 200         return false;
 201     }
 202     // if (info->fmt_chunk_size != 16) {
 203     //     bad_input("invalid RIFF WAVE format info chunk");
 204     //     return false;
 205     // }
 206     if (!read_uint16_le(r, &info->pcm_format)) {
 207         bad_input("missing PCM format in RIFF WAVE format info");
 208         return false;
 209     }
 210     if (!read_uint16_le(r, &info->channels)) {
 211         bad_input("missing channel-count in RIFF WAVE format info");
 212         return false;
 213     }
 214     if (!read_uint32_le(r, &info->sample_rate)) {
 215         bad_input("missing sample-rate in RIFF WAVE format info");
 216         return false;
 217     }
 218     if (!read_uint32_le(r, &info->byte_rate)) {
 219         bad_input("missing byte-rate in RIFF WAVE format info");
 220         return false;
 221     }
 222     if (!read_uint16_le(r, &info->bytes_per_sample_round)) {
 223         bad_input("missing byte-rate in RIFF WAVE format info");
 224         return false;
 225     }
 226     if (!read_uint16_le(r, &info->bits_per_sample)) {
 227         bad_input("missing bits-per-sample in RIFF WAVE format info");
 228         return false;
 229     }
 230 
 231     switch (info->pcm_format) {
 232     case 1:
 233         // supported
 234         break;
 235     default:
 236         bad_input("only integer PCM samples are supported");
 237         return false;
 238     }
 239 
 240     skip_bytes(r, info->fmt_chunk_size - 16);
 241 
 242     switch (info->bits_per_sample) {
 243     case 8:
 244     case 16:
 245         // bits-per-sample value is supported
 246         break;
 247     default:
 248         bad_input("only 8 and 16 bits per sample are supported");
 249         return false;
 250     }
 251 
 252     if (!seek_data_section(r)) {
 253         bad_input("no data section found in RIFF WAVE format info");
 254         return false;
 255     }
 256     if (!read_uint32_le(r, &info->data_size)) {
 257         bad_input("invalid data section in RIFF WAVE format info");
 258         return false;
 259     }
 260 
 261     return true;
 262 }
 263 
 264 // decode_pa_format only handles a subset of all possible WAVE formats;
 265 // this function assumes input formats are already checked elsewhere
 266 int decode_pa_format(const wave_info* info) {
 267     switch (info->pcm_format) {
 268     case 1:
 269         switch (info->bits_per_sample) {
 270         case 8:
 271             return PA_SAMPLE_U8;
 272         case 16:
 273             return PA_SAMPLE_S16LE;
 274         case 24:
 275             return PA_SAMPLE_S24LE;
 276         default:
 277             return -1;
 278         }
 279     default:
 280         return -1;
 281     }
 282 }
 283 
 284 // keep_volume handles the case when the volume is exactly 1
 285 void keep_volume(const void* chunk, size_t n, float volume) {
 286     // do nothing on purpose, since the volume is 1
 287 }
 288 
 289 // adjust_volume_uint8 handles bytes, so it works the same on any platform
 290 void adjust_volume_uint8(void* chunk, size_t n, float volume) {
 291     uint8_t* samples = (uint8_t*)chunk;
 292     const float k = 255.0 * volume;
 293     for (size_t i = 0; i < n; i++) {
 294         samples[i] = k * ((float)samples[i] / 255.0);
 295     }
 296 }
 297 
 298 // adjust_volume_int16le_native is used on little-endian platforms
 299 void adjust_volume_int16le_native(void* chunk, size_t n, float volume) {
 300     int16_t* samples = (int16_t*)chunk;
 301     const float k = 32767.0 * volume;
 302     const size_t end = n / 2;
 303 
 304     for (size_t i = 0; i < end; i++) {
 305         samples[i] = k * ((float)samples[i] / 32767.0);
 306     }
 307 }
 308 
 309 // adjust_volume_int16_multiplatform handles little-endian 16-bit integers
 310 // independently of the platform's endianness; it's only called on big-endian
 311 // platforms in practice
 312 void adjust_volume_int16_multiplatform(void* chunk, size_t n, float volume) {
 313     uint8_t* data = (uint8_t*)chunk;
 314     const float k = 32767.0 * volume;
 315     const size_t end = n - 2;
 316     if (n < 2) {
 317         return;
 318     }
 319 
 320     for (size_t i = 0; i < end; i += 2) {
 321         volatile int16_t v = 0;
 322         v = (data[i + 1] << 8) + (data[i + 0] << 0);
 323         v = k * ((float)v / 32767.0);
 324         data[i + 0] = (v >> 0);
 325         data[i + 1] = (v >> 8);
 326     }
 327 }
 328 
 329 // is_big_endian checks the platform's endianness when input is 16-bit samples
 330 bool is_big_endian() {
 331     const uint8_t pair[2] = {255, 0};
 332     return *((uint16_t*)pair) >= 256;
 333 }
 334 
 335 // show_format_info has been useful to check assumptions while chasing bugs
 336 void show_format_info(FILE* w, const wave_info* info) {
 337     fprintf(w, "data size: %ld\n", (long)info->data_size);
 338     fprintf(w, "channels: %ld\n", (long)info->channels);
 339     fprintf(w, "format: %ld\n", (long)info->pcm_format);
 340     fprintf(w, "bps: %ld\n", (long)info->bits_per_sample);
 341     fprintf(w, "sample rate: %ld\n", (long)info->sample_rate);
 342     fprintf(w, "byte rate: %ld\n", (long)info->byte_rate);
 343     fflush(w);
 344 }
 345 
 346 int play(FILE* r, const wave_info* info, float volume) {
 347     // show_format_info(stderr, info);
 348 
 349     if (info->data_size == 0) {
 350         return true;
 351     }
 352     if (volume < 0 || volume > 1) {
 353         volume = 1;
 354     }
 355 
 356     // use a large buffer to avoid stuttering during playback
 357     unsigned char buf[128 * 1024];
 358     pa_simple* pa;
 359     pa_sample_spec spec;
 360     memset(&spec, 0, sizeof(spec));
 361     spec.format = decode_pa_format(info);
 362     spec.rate = info->sample_rate;
 363     spec.channels = info->channels;
 364 
 365     void (*adjust_volume)(void*, size_t, float) = NULL;
 366 
 367     switch (spec.format) {
 368     case PA_SAMPLE_U8:
 369         adjust_volume = adjust_volume_uint8;
 370         break;
 371     case PA_SAMPLE_S16LE:
 372         adjust_volume = is_big_endian() ?
 373             adjust_volume_int16_multiplatform :
 374             adjust_volume_int16le_native;
 375         break;
 376     default:
 377         // technically this case is prevented from happening
 378         adjust_volume = (void (*)(void*, size_t, float))keep_volume;
 379         break;
 380     }
 381 
 382     if (volume == 1) {
 383         adjust_volume = (void (*)(void*, size_t, float))keep_volume;
 384     }
 385 
 386     pa = pa_simple_new(
 387         NULL, "playwave", PA_STREAM_PLAYBACK, NULL, "<<playwave>>", &spec,
 388         NULL, NULL, &errno
 389     );
 390     if (pa == NULL) {
 391         fprintf(stderr, ERROR_LINE("can't use pulse-audio for playback"));
 392         return false;
 393     }
 394 
 395     const size_t item_size = info->bits_per_sample / 8;
 396     const size_t cap = sizeof(buf) - (sizeof(buf) % item_size);
 397     int64_t bytes_left = info->data_size;
 398 
 399     // all-bits-on on the data-size means at least those many bytes: using
 400     // an enormous value (almost 8 million terabytes) allows playing up to
 401     // hundreds of thousands of years of multi-channel high-quality sound
 402     if (bytes_left == UINT32_MAX) {
 403         bytes_left = INT64_MAX;
 404     }
 405 
 406     while (bytes_left > 0) {
 407         const size_t ask = bytes_left >= cap ? cap : bytes_left;
 408         const size_t len = fread(&buf, 1, ask, r);
 409         if (len < 1) {
 410             break;
 411         }
 412 
 413         adjust_volume(buf, len, volume);
 414         if (pa_simple_write(pa, buf, len, NULL) < 0) {
 415             fprintf(stderr, ERROR_LINE("can't keep playing data"));
 416             pa_simple_drain(pa, NULL);
 417             pa_simple_free(pa);
 418             return false;
 419         }
 420         bytes_left -= len;
 421     }
 422 
 423     pa_simple_drain(pa, NULL);
 424     pa_simple_free(pa);
 425     return true;
 426 }
 427 
 428 bool handle_reader(FILE* r, double volume) {
 429     wave_info wi;
 430     if (!read_wave_intro(r, &wi)) {
 431         return false;
 432     }
 433     return play(r, &wi, volume);
 434 }
 435 
 436 // handle_file handles data from the filename given; returns false only when
 437 // the file can't be opened
 438 bool handle_file(const char* path, double volume) {
 439     FILE* f = fopen(path, "rb");
 440     if (f == NULL) {
 441         fprintf(stderr, ERROR_LINE("can't open file named '%s'"), path);
 442         return false;
 443     }
 444 
 445     const bool ok = handle_reader(f, volume);
 446     fclose(f);
 447     return ok;
 448 }
 449 
 450 // runs returns the number of errors
 451 int run(char** args, size_t nargs, double volume) {
 452     size_t dashes = 0;
 453     for (size_t i = 0; i < nargs; i++) {
 454         if (strcmp(args[i], "-") == 0) {
 455             dashes++;
 456         }
 457     }
 458 
 459     if (dashes > 1) {
 460         const char* m = "can't use the standard input (dash) more than once";
 461         fprintf(stderr, ERROR_LINE("%s"), m);
 462         return 1;
 463     }
 464 
 465     size_t errors = 0;
 466     for (size_t i = 0; i < nargs; i++) {
 467         if (strcmp(args[i], "-") == 0) {
 468             if (!handle_reader(stdin, volume)) {
 469                 errors++;
 470             }
 471             continue;
 472         }
 473 
 474         if (!handle_file(args[i], volume)) {
 475             errors++;
 476         }
 477     }
 478 
 479     if (nargs == 0) {
 480         if (!handle_reader(stdin, volume)) {
 481             errors++;
 482         }
 483     }
 484     return errors;
 485 }
 486 
 487 int main(int argc, char** argv) {
 488 #ifdef _WIN32
 489     setmode(fileno(stdin), O_BINARY);
 490     // ensure output lines end in LF instead of CRLF on windows
 491     setmode(fileno(stdout), O_BINARY);
 492     setmode(fileno(stderr), O_BINARY);
 493 #endif
 494 
 495     if (argc > 1) {
 496         if (
 497             strcmp(argv[1], "-h") == 0 ||
 498             strcmp(argv[1], "-help") == 0 ||
 499             strcmp(argv[1], "--h") == 0 ||
 500             strcmp(argv[1], "--help") == 0
 501         ) {
 502             fprintf(stdout, "%s", info);
 503             return 0;
 504         }
 505     }
 506 
 507     size_t nargs = argc - 1;
 508     char** args = argv + 1;
 509 
 510     double volume = 1.0;
 511     if (nargs > 0) {
 512         char* end;
 513         const double n = strtod(args[0], &end);
 514         if (*end == 0 && args[0] != end) {
 515             volume = n >= 0 ? n : 1.0;
 516             nargs--;
 517             args++;
 518         }
 519     }
 520 
 521     if (nargs > 0 && strcmp(args[0], "--") == 0) {
 522         nargs--;
 523         args++;
 524     }
 525 
 526     // enable full/block-buffering for standard input
 527     setvbuf(stdin, NULL, _IOFBF, 0);
 528 
 529     return run(args, nargs, volume) == 0 ? 0 : 1;
 530 }