File: ringtone.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 ./ringtone ./ringtone.c \
  30     -lm -lpulse -lpulse-simple
  31 */
  32 
  33 #include <errno.h>
  34 #include <math.h>
  35 #include <stdbool.h>
  36 #include <stdint.h>
  37 #include <stdio.h>
  38 #include <stdlib.h>
  39 #include <string.h>
  40 
  41 #ifdef _WIN32
  42 #include <fcntl.h>
  43 #include <windows.h>
  44 #endif
  45 
  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 "ringtone [options...] [duration...] [volume...]\n"
  63 "\n"
  64 "\n"
  65 "Emit a ringtone as a wave-sound, lasting the number of seconds given, or 1\n"
  66 "second by default. The audio output format is RIFF WAVE (wav) sampled at\n"
  67 "48khz. There's also an optional volume argument, which is 1 by default.\n"
  68 "\n"
  69 "By default the sound is played directly, but there are options to emit the\n"
  70 "binary data for the sound instead.\n"
  71 "\n"
  72 "\n"
  73 "Options, all of which can start with either 1 or 2 dashes:\n"
  74 "\n"
  75 "\n"
  76 "  -h          show this help message\n"
  77 "  -help       show this help message\n"
  78 "\n"
  79 "  -o          output ringtone to standard output as WAV-format data\n"
  80 "  -out        output ringtone to standard output as WAV-format data\n"
  81 "  -output     output ringtone to standard output as WAV-format data\n"
  82 "";
  83 
  84 const uint64_t sample_rate = 48000;
  85 const double dt = 1.0 / sample_rate;
  86 
  87 // is_help_option simplifies control-flow for func main
  88 bool is_help_option(const char* s) {
  89     return (s[0] == '-') && (
  90         strcmp(s, "-h") == 0 ||
  91         strcmp(s, "-help") == 0 ||
  92         strcmp(s, "--h") == 0 ||
  93         strcmp(s, "--help") == 0
  94     );
  95 }
  96 
  97 // is_output_option simplifies control-flow for func main
  98 bool is_output_option(const char* s) {
  99     return (s[0] == '-') && (
 100         strcmp(s, "-o") == 0 ||
 101         strcmp(s, "-out") == 0 ||
 102         strcmp(s, "-output") == 0 ||
 103         strcmp(s, "--o") == 0 ||
 104         strcmp(s, "--out") == 0 ||
 105         strcmp(s, "--output") == 0
 106     );
 107 }
 108 
 109 static inline void set_int16_le(unsigned char* buf, int16_t v) {
 110     buf[0] = (v >> 0);
 111     buf[1] = (v >> 8);
 112 }
 113 
 114 static inline void write_int16_le(FILE* w, int16_t v) {
 115     fputc((v >> 0), w);
 116     fputc((v >> 8), w);
 117 }
 118 
 119 static inline void write_uint16_le(FILE* w, uint16_t v) {
 120     fputc((v >> 0), w);
 121     fputc((v >> 8), w);
 122 }
 123 
 124 void write_uint32_le(FILE* w, uint32_t v) {
 125     fputc((v >> 0), w);
 126     fputc((v >> 8), w);
 127     fputc((v >> 16), w);
 128     fputc((v >> 24), w);
 129 }
 130 
 131 // write_wav_intro starts a wave-format data-stream declaring a mono 16-bit
 132 // integer PCM sound for the number of samples given
 133 void write_wav_intro(FILE* w, uint64_t samples) {
 134     const uint16_t integer_pcm = 1;
 135     const uint32_t fmt_chunk_size = 16;
 136 
 137     const uint32_t channels = 1;
 138     const uint32_t bytes_per_sample = 2;
 139     const uint32_t bits_per_sample = 8 * bytes_per_sample;
 140     const uint32_t byte_rate = sample_rate * bytes_per_sample * channels;
 141 
 142     uint64_t data_size = byte_rate * samples;
 143     uint64_t total_size = data_size + 44;
 144     if (data_size > UINT32_MAX) {
 145         data_size = UINT32_MAX;
 146         total_size = UINT32_MAX;
 147     }
 148 
 149     fprintf(w, "RIFF");
 150     write_uint32_le(w, total_size);
 151     fprintf(w, "WAVEfmt ");
 152 
 153     write_uint32_le(w, fmt_chunk_size);
 154     write_uint16_le(w, integer_pcm);
 155     write_uint16_le(w, channels);
 156     write_uint32_le(w, sample_rate);
 157     write_uint32_le(w, byte_rate);
 158     write_uint16_le(w, bytes_per_sample * channels);
 159     write_uint16_le(w, bits_per_sample);
 160 
 161     fprintf(w, "data");
 162     write_uint32_le(w, data_size);
 163 }
 164 
 165 int emit_wav_ringtone(double seconds, double volume) {
 166     if (seconds <= 0) {
 167         write_wav_intro(stdout, 0);
 168         return 0;
 169     }
 170 
 171     if (volume < 0 || volume > 1 || isnan(seconds) || isinf(seconds)) {
 172         volume = 1;
 173     }
 174 
 175     const uint64_t samples = (uint64_t)(seconds * sample_rate);
 176     if (samples >= UINT32_MAX) {
 177         fprintf(stderr, ERROR_LINE("duration given exceeds WAV-format max"));
 178         return 1;
 179     }
 180 
 181     write_wav_intro(stdout, samples);
 182 
 183     const double tau = 2 * M_PI;
 184 
 185     for (uint64_t i = 0; i < samples; i++) {
 186         const double t = (double)i * dt;
 187         const double u = fmod(t, 0.1);
 188         const double v = volume * sin(2048 * tau * t) * exp(-50 * u);
 189         write_int16_le(stdout, (int16_t)(32767 * v));
 190 
 191         // check if the standard output was closed only occasionally
 192         if ((i % (32 * 1024) == 0) && feof(stdout)) {
 193             return 0;
 194         }
 195     }
 196 
 197     return 0;
 198 }
 199 
 200 // is_big_endian checks the platform's endianness; kept for reference; not
 201 // used anymore since playback use little-endian samples on all platforms,
 202 // via function set_int16_le
 203 bool is_big_endian() {
 204     const uint8_t pair[2] = {255, 0};
 205     return *((uint16_t*)pair) >= 256;
 206 }
 207 
 208 int play_ringtone(double seconds, double volume) {
 209     if (seconds <= 0) {
 210         return 0;
 211     }
 212     if (volume < 0 || volume > 1 || isnan(seconds) || isinf(seconds)) {
 213         volume = 1;
 214     }
 215 
 216     const uint64_t rate = 48 * 1000;
 217     unsigned char buf[32 * 1024];
 218     pa_simple* pa;
 219     pa_sample_spec spec;
 220     memset(&spec, 0, sizeof(spec));
 221     // spec.format = is_big_endian() ? PA_SAMPLE_S16BE : PA_SAMPLE_S16LE;
 222     spec.format = PA_SAMPLE_S16LE;
 223     spec.rate = rate;
 224     spec.channels = 1;
 225 
 226     pa = pa_simple_new(
 227         NULL, "ringtone", PA_STREAM_PLAYBACK, NULL, "<<ringtone>>", &spec,
 228         NULL, NULL, &errno
 229     );
 230     if (pa == NULL) {
 231         fprintf(stderr, ERROR_LINE("can't use pulse-audio for playback"));
 232         return 1;
 233     }
 234 
 235     uint64_t pos = 0;
 236     const double tau = 2 * M_PI;
 237     const uint64_t samples = (uint64_t)ceil(seconds * rate);
 238 
 239     for (uint64_t i = 0; i < samples; i++) {
 240         const double t = (double)i * dt;
 241         const double u = fmod(t, 0.1);
 242         const double v = volume * sin(2048 * tau * t) * exp(-50 * u);
 243         set_int16_le(&buf[pos], (int16_t)(32767 * v));
 244 
 245         pos += 2;
 246         if (pos == sizeof(buf)) {
 247             if (pa_simple_write(pa, buf, pos, NULL) < 0) {
 248                 pa_simple_drain(pa, NULL);
 249                 pa_simple_free(pa);
 250                 return 1;
 251             }
 252             pos = 0;
 253         }
 254     }
 255 
 256     if (pos > 0) {
 257         if (pa_simple_write(pa, buf, pos, NULL) < 0) {
 258             pa_simple_drain(pa, NULL);
 259             pa_simple_free(pa);
 260             return 1;
 261         }
 262     }
 263 
 264     pa_simple_drain(pa, NULL);
 265     pa_simple_free(pa);
 266     return 0;
 267 }
 268 
 269 int main(int argc, char** argv) {
 270 #ifdef _WIN32
 271     setmode(fileno(stdin), O_BINARY);
 272     // ensure output lines end in LF instead of CRLF on windows
 273     setmode(fileno(stdout), O_BINARY);
 274     setmode(fileno(stderr), O_BINARY);
 275 #endif
 276 
 277     if (argc > 1 && is_help_option(argv[1])) {
 278         printf("%s", info);
 279         return 0;
 280     }
 281 
 282     size_t start_args = 1;
 283     bool output = false;
 284     if (argc > start_args && is_output_option(argv[start_args])) {
 285         start_args++;
 286         output = true;
 287     }
 288 
 289     double seconds = 1.0;
 290     if (argc > start_args) {
 291         char* end;
 292         const double n = strtod(argv[start_args], &end);
 293         if (*end == 0 && argv[start_args] != end) {
 294             seconds = n >= 0 ? n : 1.0;
 295             start_args++;
 296         }
 297     }
 298 
 299     if (argc > start_args && is_output_option(argv[start_args])) {
 300         start_args++;
 301         output = true;
 302     }
 303 
 304     double volume = 1.0;
 305     if (argc > start_args) {
 306         char* end;
 307         const double n = strtod(argv[start_args], &end);
 308         if (*end == 0 && argv[start_args] != end) {
 309             volume = n >= 0 ? n : 1.0;
 310         }
 311     }
 312 
 313     if (argc > start_args && is_output_option(argv[start_args])) {
 314         output = true;
 315     }
 316 
 317     if (output) {
 318         // enable full/block-buffering for standard output
 319         setvbuf(stdout, NULL, _IOFBF, 0);
 320     }
 321 
 322     const double s = seconds;
 323     const double v = volume;
 324     return output ? emit_wav_ringtone(s, v) : play_ringtone(s, v);
 325 }