/* The MIT License (MIT) Copyright © 2020-2025 pacman64 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* You can build this command-line app by running sudo apt install libpulse-dev cc -s -O3 -march=native -mtune=native -flto -o ./playwwave ./playwwave.c -lpulse -lpulse-simple */ #include #include #include #include #include #include #ifdef _WIN32 #include #include #endif #include #ifdef RED_ERRORS #define ERROR_STYLE "\x1b[38;2;204;0;0m" #ifdef __APPLE__ #define ERROR_STYLE "\x1b[31m" #endif #define RESET_STYLE "\x1b[0m" #else #define ERROR_STYLE #define RESET_STYLE #endif #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n") const char* info = "" "playwwave [options...] [volume...]\n" "\n" "Play WAV-format data, either from the file given, or from standard input.\n" "Only 8-bit and 16-bit integer samples are currently supported.\n" "\n" "Options, all of which can start with either 1 or 2 dashes:\n" "\n" " -h show this help message\n" " -help show this help message\n" ""; const char* pa_init_error = "can't use pulse-audio for playback"; const char* pa_write_error = "can't keep playing data"; // is_help_option simplifies control-flow for func main bool is_help_option(const char* s) { return (s[0] == '-') && ( strcmp(s, "-h") == 0 || strcmp(s, "-help") == 0 || strcmp(s, "--h") == 0 || strcmp(s, "--help") == 0 ); } // read_uint16_le reads a 16-bit little-endian integer platform-independently bool read_uint16_le(FILE* r, uint16_t* v) { const int a = fgetc(r); if (a == EOF) { return false; } const int b = fgetc(r); if (b == EOF) { return false; } *v = ((b & 0xff) << 8) + (a & 0xff); return true; } // read_uint32_le reads a 32-bit little-endian integer platform-independently bool read_uint32_le(FILE* r, uint32_t* v) { const int a = fgetc(r); if (a == EOF) { return false; } const int b = fgetc(r); if (b == EOF) { return false; } const int c = fgetc(r); if (c == EOF) { return false; } const int d = fgetc(r); if (d == EOF) { return false; } *v = ((a & 0xff) << 0) + ((b & 0xff) << 8) + ((c & 0xff) << 16) + ((d & 0xff) << 24); return true; } // wave_info has the wav-format metadata needed to playback sound samples typedef struct wave_info { uint32_t fmt_chunk_size; uint16_t pcm_format; uint16_t channels; uint32_t sample_rate; uint32_t byte_rate; uint16_t bytes_per_sample_round; // could have a better name uint16_t bits_per_sample; uint32_t data_size; } wave_info; bool demand_string(FILE* r, const char* s) { for (; *s != 0; s++) { const int b = fgetc(r); if ((b == EOF) || (b != *s)) { return false; } } return true; } void show_input_error(const char* msg) { fprintf(stderr, ERROR_LINE("invalid input data: %s"), msg); } bool skip_bytes(FILE* r, size_t n) { unsigned char buf[4 * 1024]; while (n >= sizeof(buf)) { size_t got = fread(buf, 1, sizeof(buf), r); n -= got; } if ((n > 0) && (fread(buf, 1, n, r) != n)) { return false; } return !feof(r); } bool seek_data_section(FILE* r) { while (!feof(r)) { const int a = fgetc(r); const int b = fgetc(r); const int c = fgetc(r); const int d = fgetc(r); if (feof(r)) { return false; } if ((a == 'd') && (b == 'a') && (c == 't') && (d == 'a')) { return true; } uint32_t n = 0; if (!read_uint32_le(r, &n)) { return false; } if (!skip_bytes(r, n)) { return false; } } return false; } bool read_wave_intro(FILE* r, wave_info* info) { if (feof(r)) { show_input_error("empty input"); return false; } uint32_t total_size = 0; if (!demand_string(r, "RIFF")) { show_input_error("no RIFF WAVE tag"); return false; } if (!read_uint32_le(r, &total_size)) { show_input_error("no RIFF WAVE total-size"); return false; } if (!demand_string(r, "WAVEfmt ")) { show_input_error("no RIFF WAVE format info"); return false; } if (!read_uint32_le(r, &info->fmt_chunk_size)) { show_input_error("no size of RIFF WAVE format info"); return false; } // if (info->fmt_chunk_size != 16) { // show_input_error("invalid RIFF WAVE format info chunk"); // return false; // } if (!read_uint16_le(r, &info->pcm_format)) { show_input_error("no PCM format in RIFF WAVE format info"); return false; } if (!read_uint16_le(r, &info->channels)) { show_input_error("no channel-count in RIFF WAVE format info"); return false; } if (!read_uint32_le(r, &info->sample_rate)) { show_input_error("no sample-rate in RIFF WAVE format info"); return false; } if (!read_uint32_le(r, &info->byte_rate)) { show_input_error("no byte-rate in RIFF WAVE format info"); return false; } if (!read_uint16_le(r, &info->bytes_per_sample_round)) { show_input_error("incomplete byte-rate in RIFF WAVE format info"); return false; } if (!read_uint16_le(r, &info->bits_per_sample)) { show_input_error("no bits-per-sample in RIFF WAVE format info"); return false; } switch (info->pcm_format) { case 1: // supported break; default: show_input_error("only integer PCM samples are supported"); return false; } skip_bytes(r, info->fmt_chunk_size - 16); switch (info->bits_per_sample) { case 8: case 16: // bits-per-sample value is supported break; default: show_input_error("only 8 and 16 bits per sample are supported"); return false; } if (!seek_data_section(r)) { show_input_error("no data section in RIFF WAVE format info"); return false; } if (!read_uint32_le(r, &info->data_size)) { show_input_error("no size for data section in RIFF WAVE format info"); return false; } return true; } int decode_pa_format(const wave_info* info) { switch (info->pcm_format) { case 1: switch (info->bits_per_sample) { case 8: return PA_SAMPLE_U8; case 16: return PA_SAMPLE_S16LE; case 24: return PA_SAMPLE_S24LE; default: return PA_SAMPLE_S16LE; } } return PA_SAMPLE_S16LE; } // keep_volume handles the case when the volume is exactly 1 void keep_volume(const void* chunk, size_t n, float volume) { // do nothing on purpose, since the volume is 1 } // adjust_volume_uint8 handles bytes, so it works the same on any platform void adjust_volume_uint8(void* chunk, size_t n, float volume) { uint8_t* samples = (uint8_t*)chunk; const float k = 255.0 * volume; for (size_t i = 0; i < n; i++) { samples[i] = k * ((float)samples[i] / 255.0); } } // adjust_volume_int16le_native is used on little-endian platforms void adjust_volume_int16le_native(void* chunk, size_t n, float volume) { int16_t* samples = (int16_t*)chunk; const float k = 32767.0 * volume; const size_t end = n / 2; for (size_t i = 0; i < end; i++) { samples[i] = k * ((float)samples[i] / 32767.0); } } // adjust_volume_int16_multiplatform handles little-endian 16-bit integers // independently of the platform's endianness; it's only called on big-endian // platforms in practice void adjust_volume_int16_multiplatform(void* chunk, size_t n, float volume) { uint8_t* data = (uint8_t*)chunk; const float k = 32767.0 * volume; const size_t end = n - 2; if (n < 2) { return; } for (size_t i = 0; i < end; i += 2) { volatile int16_t v = 0; v = ((data[i + 1] & 0xff) << 8) + ((data[i + 0] & 0xff) << 0); v = k * ((float)v / 32767.0); data[i + 0] = (v >> 0) & 0xff; data[i + 1] = (v >> 8) & 0xff; } } // is_big_endian checks the platform's endianness when input is 16-bit samples bool is_big_endian() { const uint8_t pair[2] = {255, 0}; return *((uint16_t*)pair) >= 256; } // show_format_info has been useful to check assumptions while chasing bugs void show_format_info(FILE* w, const wave_info* info) { fprintf(w, "data size: %ld\n", (long)info->data_size); fprintf(w, "channels: %ld\n", (long)info->channels); fprintf(w, "format: %ld\n", (long)info->pcm_format); fprintf(w, "bps: %ld\n", (long)info->bits_per_sample); fprintf(w, "sample rate: %ld\n", (long)info->sample_rate); fprintf(w, "byte rate: %ld\n", (long)info->byte_rate); fflush(w); } int play(FILE* r, const wave_info* info, float volume) { // show_format_info(stderr, info); if (info->data_size == 0) { return true; } if (volume < 0 || volume > 1) { volume = 1; } // use a large buffer to avoid gaps in playback unsigned char buf[128 * 1024]; pa_simple* pa; pa_sample_spec spec; memset(&spec, 0, sizeof(spec)); spec.format = decode_pa_format(info); spec.rate = info->sample_rate; spec.channels = info->channels; void (*adjust_volume)(void*, size_t, float) = NULL; switch (spec.format) { case PA_SAMPLE_U8: adjust_volume = adjust_volume_uint8; break; case PA_SAMPLE_S16LE: if (is_big_endian()) { adjust_volume = adjust_volume_int16_multiplatform; } else { adjust_volume = adjust_volume_int16le_native; } break; default: // technically this case is prevented from happening adjust_volume = (void (*)(void*, size_t, float))keep_volume; break; } if (volume == 1) { adjust_volume = (void (*)(void*, size_t, float))keep_volume; } pa = pa_simple_new( NULL, "playwave", PA_STREAM_PLAYBACK, NULL, "<>", &spec, NULL, NULL, &errno ); if (pa == NULL) { fprintf(stderr, ERROR_LINE("can't use pulse-audio for playback")); return false; } const size_t item_size = info->bits_per_sample / 8; const size_t cap = sizeof(buf) - (sizeof(buf) % item_size); int64_t bytes_left = info->data_size; // all-bits-on on the data-size means at least those many bytes: using an // enormous value (almost 8 million terabytes) to handle that amounts to // using the whole input/file in practice if (bytes_left == UINT32_MAX) { bytes_left = INT64_MAX; } while (bytes_left > 0) { const size_t len = fread(&buf, 1, cap, r); if (len < 1) { break; } adjust_volume(buf, len, volume); if (pa_simple_write(pa, buf, len, NULL) < 0) { fprintf(stderr, ERROR_LINE("can't keep playing data")); pa_simple_drain(pa, NULL); pa_simple_free(pa); return false; } bytes_left -= len; } pa_simple_drain(pa, NULL); pa_simple_free(pa); return true; } bool handle_reader(FILE* r, double volume) { wave_info wi; if (!read_wave_intro(r, &wi)) { return false; } return play(r, &wi, volume); } // handle_file handles data from the filename given; returns false only when // the file can't be opened bool handle_file(const char* path, double volume) { FILE* f = fopen(path, "rb"); if (f == NULL) { fprintf(stderr, ERROR_LINE("can't open file named '%s'"), path); return false; } const bool ok = handle_reader(f, volume); fclose(f); return ok; } // runs returns the number of errors int run(int argc, char** argv, double volume) { size_t errors = 0; for (size_t i = 1; i < argc; i++) { if (!handle_file(argv[i], volume)) { errors++; } } if (argc < 2) { if (!handle_reader(stdin, volume)) { errors++; } } return errors; } int main(int argc, char** argv) { #ifdef _WIN32 setmode(fileno(stdin), O_BINARY); // ensure output lines end in LF instead of CRLF on windows setmode(fileno(stdout), O_BINARY); setmode(fileno(stderr), O_BINARY); #endif if (argc > 1 && is_help_option(argv[1])) { printf("%s", info); return 0; } double volume = 1.0; if (argc > 1) { char* end; const double n = strtod(argv[1], &end); if (*end == 0 && argv[1] != end) { volume = n >= 0 ? n : 1.0; argc--; argv++; } } return run(argc, argv, volume) == 0 ? 0 : 1; }