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 }