File: playwave.c
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 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 -O2 -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/error.h>
  46 #include <pulse/simple.h>
  47 
  48 #ifdef RED_ERRORS
  49 #define ERROR_STYLE "\x1b[38;2;204;0;0m"
  50 #ifdef __APPLE__
  51 #define ERROR_STYLE "\x1b[31m"
  52 #endif
  53 #define RESET_STYLE "\x1b[0m"
  54 #else
  55 #define ERROR_STYLE
  56 #define RESET_STYLE
  57 #endif
  58 
  59 #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n")
  60 
  61 const char* info = ""
  62 "playwwave [options...] [volume...]\n"
  63 "\n"
  64 "Play WAV-format sound data, either from the files given, or standard input.\n"
  65 "\n"
  66 "The sample formats supported are\n"
  67 "\n"
  68 "     8-bit integer\n"
  69 "    16-bit signed-integer little-endian\n"
  70 "    32-bit floating-point little-endian\n"
  71 "\n"
  72 "Options, all of which can start with either 1 or 2 dashes:\n"
  73 "\n"
  74 "  -h          show this help message\n"
  75 "  -help       show this help message\n"
  76 "";
  77 
  78 // read_uint16_le reads a 16-bit little-endian integer platform-independently
  79 bool read_uint16_le(FILE* r, uint16_t* v) {
  80     const int a = fgetc(r);
  81     if (a == EOF) {
  82         return false;
  83     }
  84     const int b = fgetc(r);
  85     if (b == EOF) {
  86         return false;
  87     }
  88 
  89     *v = (b << 8) + a;
  90     return true;
  91 }
  92 
  93 // read_uint32_le reads a 32-bit little-endian integer platform-independently
  94 bool read_uint32_le(FILE* r, uint32_t* v) {
  95     const int a = fgetc(r);
  96     if (a == EOF) {
  97         return false;
  98     }
  99     const int b = fgetc(r);
 100     if (b == EOF) {
 101         return false;
 102     }
 103     const int c = fgetc(r);
 104     if (c == EOF) {
 105         return false;
 106     }
 107     const int d = fgetc(r);
 108     if (d == EOF) {
 109         return false;
 110     }
 111 
 112     *v = (a << 0) + (b << 8) + (c << 16) + (d << 24);
 113     return true;
 114 }
 115 
 116 // wave_info has the wav-format metadata needed to playback sound samples
 117 typedef struct wave_info {
 118     uint32_t fmt_chunk_size;
 119     uint16_t pcm_format;
 120     uint16_t channels;
 121     uint32_t sample_rate;
 122     uint32_t byte_rate;
 123     uint16_t bytes_per_sample_round; // could have a better name
 124     uint16_t bits_per_sample;
 125     uint32_t data_size;
 126 } wave_info;
 127 
 128 bool demand_string(FILE* r, const char* s) {
 129     for (; *s != 0; s++) {
 130         const int b = fgetc(r);
 131         if ((b == EOF) || (b != *s)) {
 132             return false;
 133         }
 134     }
 135     return true;
 136 }
 137 
 138 void bad_input(const char* msg) {
 139     fprintf(stderr, ERROR_LINE("invalid input data: %s"), msg);
 140 }
 141 
 142 bool skip_bytes(FILE* r, size_t n) {
 143     unsigned char buf[32 * 1024];
 144     while (n >= sizeof(buf)) {
 145         size_t got = fread(buf, 1, sizeof(buf), r);
 146         n -= got;
 147     }
 148 
 149     if ((n > 0) && (fread(buf, 1, n, r) != n)) {
 150         return false;
 151     }
 152     return !feof(r);
 153 }
 154 
 155 bool seek_data_section(FILE* r) {
 156     while (!feof(r)) {
 157         const int a = fgetc(r);
 158         const int b = fgetc(r);
 159         const int c = fgetc(r);
 160         const int d = fgetc(r);
 161         if (feof(r)) {
 162             return false;
 163         }
 164 
 165         if ((a == 'd') && (b == 'a') && (c == 't') && (d == 'a')) {
 166             return true;
 167         }
 168 
 169         uint32_t n = 0;
 170         if (!read_uint32_le(r, &n)) {
 171             return false;
 172         }
 173         if (!skip_bytes(r, n)) {
 174             return false;
 175         }
 176     }
 177 
 178     return false;
 179 }
 180 
 181 bool read_wave_intro(FILE* r, wave_info* info) {
 182     if (feof(r)) {
 183         bad_input("empty input");
 184         return false;
 185     }
 186 
 187     uint32_t total_size = 0;
 188     if (!demand_string(r, "RIFF")) {
 189         bad_input("no RIFF WAVE tag");
 190         return false;
 191     }
 192     if (!read_uint32_le(r, &total_size)) {
 193         bad_input("missing RIFF WAVE total-size");
 194         return false;
 195     }
 196     if (!demand_string(r, "WAVEfmt ")) {
 197         bad_input("missing RIFF WAVE format info");
 198         return false;
 199     }
 200 
 201     if (!read_uint32_le(r, &info->fmt_chunk_size)) {
 202         bad_input("missing size of RIFF WAVE format info");
 203         return false;
 204     }
 205     // if (info->fmt_chunk_size != 16) {
 206     //     bad_input("invalid RIFF WAVE format info chunk");
 207     //     return false;
 208     // }
 209     if (!read_uint16_le(r, &info->pcm_format)) {
 210         bad_input("missing PCM format in RIFF WAVE format info");
 211         return false;
 212     }
 213     if (!read_uint16_le(r, &info->channels)) {
 214         bad_input("missing channel-count in RIFF WAVE format info");
 215         return false;
 216     }
 217     if (!read_uint32_le(r, &info->sample_rate)) {
 218         bad_input("missing sample-rate in RIFF WAVE format info");
 219         return false;
 220     }
 221     if (!read_uint32_le(r, &info->byte_rate)) {
 222         bad_input("missing byte-rate in RIFF WAVE format info");
 223         return false;
 224     }
 225     if (!read_uint16_le(r, &info->bytes_per_sample_round)) {
 226         bad_input("missing byte-rate in RIFF WAVE format info");
 227         return false;
 228     }
 229     if (!read_uint16_le(r, &info->bits_per_sample)) {
 230         bad_input("missing bits-per-sample in RIFF WAVE format info");
 231         return false;
 232     }
 233 
 234     switch (info->pcm_format) {
 235     case 1:
 236         // integer samples are supported
 237     case 3:
 238         // floating-point samples are supported
 239         break;
 240 
 241     default:
 242         bad_input("only integer or floating-point PCM samples are supported");
 243         return false;
 244     }
 245 
 246     skip_bytes(r, info->fmt_chunk_size - 16);
 247 
 248     switch (info->bits_per_sample) {
 249     case 8:
 250     case 16:
 251     case 32:
 252         // bits-per-sample value is supported
 253         break;
 254 
 255     default:
 256         bad_input("only 8, 16, and 32 bits per sample are supported");
 257         return false;
 258     }
 259 
 260     if (!seek_data_section(r)) {
 261         bad_input("no data section found in RIFF WAVE format info");
 262         return false;
 263     }
 264     if (!read_uint32_le(r, &info->data_size)) {
 265         bad_input("invalid data section in RIFF WAVE format info");
 266         return false;
 267     }
 268 
 269     return true;
 270 }
 271 
 272 // decode_pa_format only handles a subset of all possible WAVE formats;
 273 // this function assumes input formats are already checked elsewhere
 274 int decode_pa_format(const wave_info* info) {
 275     switch (info->pcm_format) {
 276     case 1:
 277         switch (info->bits_per_sample) {
 278         case 8:
 279             return PA_SAMPLE_U8;
 280         case 16:
 281             return PA_SAMPLE_S16LE;
 282         case 24:
 283             return PA_SAMPLE_S24LE;
 284         case 32:
 285             return PA_SAMPLE_S32LE;
 286         default:
 287             return -1;
 288         }
 289 
 290     case 3:
 291         switch (info->bits_per_sample) {
 292         case 32:
 293             return PA_SAMPLE_FLOAT32LE;
 294         default:
 295             return -1;
 296         }
 297 
 298     default:
 299         return -1;
 300     }
 301 }
 302 
 303 // keep_volume handles the case when the volume is exactly 1
 304 void keep_volume(const void* chunk, size_t n, float volume) {
 305     // do nothing on purpose, since the volume is 1
 306 }
 307 
 308 // adjust_volume_uint8 handles bytes, so it works the same on any platform
 309 void adjust_volume_uint8(void* chunk, size_t n, float volume) {
 310     uint8_t* samples = (uint8_t*)chunk;
 311     const float k = 255.0 * volume;
 312 
 313     for (size_t i = 0; i < n; i++) {
 314         const float v = k * ((float)samples[i] / 255.0);
 315         samples[i] = (uint8_t)v;
 316     }
 317 }
 318 
 319 // adjust_volume_int16le_native is used on little-endian platforms
 320 void adjust_volume_int16le_native(void* chunk, size_t n, float volume) {
 321     int16_t* samples = (int16_t*)chunk;
 322     const float k = 32767.0 * volume;
 323     const size_t end = n / sizeof(samples[0]);
 324 
 325     for (size_t i = 0; i < end; i++) {
 326         samples[i] = k * ((float)samples[i] / 32767.0);
 327     }
 328 }
 329 
 330 // adjust_volume_int16_multiplatform handles little-endian 16-bit integers
 331 // independently of the platform's endianness; it's only called on big-endian
 332 // platforms in practice
 333 void adjust_volume_int16_multiplatform(void* chunk, size_t n, float volume) {
 334     uint8_t* data = (uint8_t*)chunk;
 335     const float k = 32767.0 * volume;
 336     const size_t end = n - 2;
 337     if (n < 2) {
 338         return;
 339     }
 340 
 341     for (size_t i = 0; i < end; i += 2) {
 342         int16_t v = (data[i + 1] << 8) + (data[i + 0] << 0);
 343         const float f = k * ((float)v / 32767.0);
 344         v = (int16_t)f;
 345         data[i + 0] = (v >> 0);
 346         data[i + 1] = (v >> 8);
 347     }
 348 }
 349 
 350 // adjust_volume_float32le_native is used on little-endian platforms
 351 void adjust_volume_float32le_native(void* chunk, size_t n, float volume) {
 352     float* samples = (float*)chunk;
 353     const size_t end = n / sizeof(samples[0]);
 354 
 355     for (size_t i = 0; i < end; i++) {
 356         samples[i] = volume * (float)samples[i];
 357     }
 358 }
 359 
 360 // adjust_volume_float32_multiplatform handles little-endian 32-bit floats
 361 // independently of the platform's endianness; it's only called on big-endian
 362 // platforms in practice
 363 void adjust_volume_float32_multiplatform(void* chunk, size_t n, float volume) {
 364     uint8_t* data = (uint8_t*)chunk;
 365     const size_t end = n - 4;
 366     if (n < 4) {
 367         return;
 368     }
 369 
 370     for (size_t i = 0; i < end; i += 4) {
 371         union {
 372             uint32_t n;
 373             float f;
 374         } v;
 375 
 376         v.n = (data[i + 3] << 24) + (data[i + 2] << 16) +
 377               (data[i + 1] << 8) + (data[i + 0] << 0);
 378         v.f *= volume;
 379 
 380         data[i + 0] = (v.n >> 0);
 381         data[i + 1] = (v.n >> 8);
 382         data[i + 2] = (v.n >> 16);
 383         data[i + 3] = (v.n >> 24);
 384     }
 385 }
 386 
 387 // is_big_endian checks the platform's endianness when input is 16-bit samples
 388 // bool is_big_endian() {
 389 //     const uint8_t pair[2] = {255, 0};
 390 //     return *((uint16_t*)pair) >= 256;
 391 // }
 392 bool is_big_endian() {
 393     return true;
 394 }
 395 
 396 // show_format_info has been useful to check assumptions while chasing bugs
 397 void show_format_info(FILE* w, const wave_info* info) {
 398     fprintf(w, "data size: %ld\n", (long)info->data_size);
 399     fprintf(w, "channels: %ld\n", (long)info->channels);
 400     fprintf(w, "format: %ld\n", (long)info->pcm_format);
 401     fprintf(w, "bps: %ld\n", (long)info->bits_per_sample);
 402     fprintf(w, "sample rate: %ld\n", (long)info->sample_rate);
 403     fprintf(w, "byte rate: %ld\n", (long)info->byte_rate);
 404     fflush(w);
 405 }
 406 
 407 int play(FILE* r, const wave_info* info, float volume) {
 408     // show_format_info(stderr, info);
 409 
 410     if (info->data_size == 0) {
 411         return true;
 412     }
 413     if (volume < 0 || volume > 1) {
 414         volume = 1;
 415     }
 416 
 417     // use a large buffer to avoid stuttering during playback
 418     unsigned char buf[128 * 1024];
 419     pa_simple* pa;
 420     pa_sample_spec spec;
 421     memset(&spec, 0, sizeof(spec));
 422     spec.format = decode_pa_format(info);
 423     spec.rate = info->sample_rate;
 424     spec.channels = info->channels;
 425 
 426     void (*adjust_volume)(void*, size_t, float) = NULL;
 427 
 428     switch (spec.format) {
 429     case PA_SAMPLE_U8:
 430         adjust_volume = adjust_volume_uint8;
 431         break;
 432 
 433     case PA_SAMPLE_S16LE:
 434         adjust_volume = is_big_endian() ?
 435             adjust_volume_int16_multiplatform :
 436             adjust_volume_int16le_native;
 437         break;
 438 
 439     case PA_SAMPLE_FLOAT32LE:
 440         adjust_volume = is_big_endian() ?
 441             adjust_volume_float32_multiplatform :
 442             adjust_volume_float32le_native;
 443         break;
 444 
 445     default:
 446         // technically this case is prevented from happening
 447         adjust_volume = (void (*)(void*, size_t, float))keep_volume;
 448         break;
 449     }
 450 
 451     if (volume == 1) {
 452         adjust_volume = (void (*)(void*, size_t, float))keep_volume;
 453     }
 454 
 455     pa = pa_simple_new(
 456         NULL, "playwave", PA_STREAM_PLAYBACK, NULL, "<<playwave>>", &spec,
 457         NULL, NULL, &errno
 458     );
 459     if (pa == NULL) {
 460         fprintf(stderr, ERROR_LINE("can't use pulse-audio for playback"));
 461         fprintf(stderr, ERROR_LINE("%s"), pa_strerror(errno));
 462         return false;
 463     }
 464 
 465     const size_t item_size = info->bits_per_sample / 8;
 466     const size_t cap = sizeof(buf) - (sizeof(buf) % item_size);
 467     int64_t bytes_left = info->data_size;
 468 
 469     // all-bits-on on the data-size means at least those many bytes: using
 470     // an enormous value (almost 8 million terabytes) allows playing up to
 471     // hundreds of thousands of years of multi-channel high-quality sound
 472     if (bytes_left == UINT32_MAX) {
 473         bytes_left = INT64_MAX;
 474     }
 475 
 476     while (bytes_left > 0) {
 477         const size_t ask = bytes_left >= cap ? cap : bytes_left;
 478         const size_t len = fread(&buf, 1, ask, r);
 479         if (len < 1) {
 480             break;
 481         }
 482 
 483         adjust_volume(buf, len, volume);
 484         if (pa_simple_write(pa, buf, len, NULL) < 0) {
 485             fprintf(stderr, ERROR_LINE("can't keep playing data"));
 486             pa_simple_drain(pa, NULL);
 487             pa_simple_free(pa);
 488             return false;
 489         }
 490         bytes_left -= len;
 491     }
 492 
 493     pa_simple_drain(pa, NULL);
 494     pa_simple_free(pa);
 495     return true;
 496 }
 497 
 498 bool handle_reader(FILE* r, double volume) {
 499     wave_info wi;
 500     if (!read_wave_intro(r, &wi)) {
 501         return false;
 502     }
 503     return play(r, &wi, volume);
 504 }
 505 
 506 // handle_file handles data from the filename given; returns false only when
 507 // the file can't be opened
 508 bool handle_file(const char* path, double volume) {
 509     FILE* f = fopen(path, "rb");
 510     if (f == NULL) {
 511         fprintf(stderr, ERROR_LINE("can't open file named '%s'"), path);
 512         return false;
 513     }
 514 
 515     const bool ok = handle_reader(f, volume);
 516     fclose(f);
 517     return ok;
 518 }
 519 
 520 // runs returns the number of errors
 521 int run(char** args, size_t nargs, double volume) {
 522     size_t dashes = 0;
 523     for (size_t i = 0; i < nargs; i++) {
 524         if (strcmp(args[i], "-") == 0) {
 525             dashes++;
 526         }
 527     }
 528 
 529     if (dashes > 1) {
 530         const char* m = "can't use the standard input (dash) more than once";
 531         fprintf(stderr, ERROR_LINE("%s"), m);
 532         return 1;
 533     }
 534 
 535     size_t errors = 0;
 536     for (size_t i = 0; i < nargs; i++) {
 537         if (strcmp(args[i], "-") == 0) {
 538             if (!handle_reader(stdin, volume)) {
 539                 errors++;
 540             }
 541             continue;
 542         }
 543 
 544         if (!handle_file(args[i], volume)) {
 545             errors++;
 546         }
 547     }
 548 
 549     if (nargs == 0) {
 550         if (!handle_reader(stdin, volume)) {
 551             errors++;
 552         }
 553     }
 554     return errors;
 555 }
 556 
 557 int main(int argc, char** argv) {
 558 #ifdef _WIN32
 559     setmode(fileno(stdin), O_BINARY);
 560     // ensure output lines end in LF instead of CRLF on windows
 561     setmode(fileno(stdout), O_BINARY);
 562     setmode(fileno(stderr), O_BINARY);
 563 #endif
 564 
 565     if (argc > 1) {
 566         if (
 567             strcmp(argv[1], "-h") == 0 ||
 568             strcmp(argv[1], "-help") == 0 ||
 569             strcmp(argv[1], "--h") == 0 ||
 570             strcmp(argv[1], "--help") == 0
 571         ) {
 572             fprintf(stdout, "%s", info);
 573             return 0;
 574         }
 575     }
 576 
 577     size_t nargs = argc - 1;
 578     char** args = argv + 1;
 579 
 580     double volume = 1.0;
 581     if (nargs > 0) {
 582         char* end;
 583         const double n = strtod(args[0], &end);
 584         if (*end == 0 && args[0] != end) {
 585             volume = n >= 0 ? n : 1.0;
 586             nargs--;
 587             args++;
 588         }
 589     }
 590 
 591     if (nargs > 0 && strcmp(args[0], "--") == 0) {
 592         nargs--;
 593         args++;
 594     }
 595 
 596     // enable full/block-buffering for standard input
 597     setvbuf(stdin, NULL, _IOFBF, 0);
 598 
 599     return run(args, nargs, volume) == 0 ? 0 : 1;
 600 }