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 }