File: waveout.py 1 #!/usr/bin/python3 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2024 pacman64 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the “Software”), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be included in 15 # all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 25 26 info = ''' 27 waveout [seconds...] [python expressions...] 28 29 30 WAVE OUTput emits wave-format audio using the expression given to calculate 31 samples. The audio output format is RIFF WAVE (wav) sampled at 48khz. 32 33 The duration is an optional floating-point number of seconds: if not given, 34 the default duration is 1 second. 35 36 The formulas/expressions can use floating-point value `t`, which has the 37 current time, integer `i`, which is the 0-based index of the current sample 38 being calculated, as well as all names (functions and values) from the 39 built-in python modules `math` and `random`. A few extra convenience funcs 40 are also defined and are available to the expressions. 41 42 The formulas are expected to result in floating-point values between -1.0 43 and +1.0; other types of results will end in an error. 44 45 The number of expressions determines the number of sound-channels, such as 46 mono (1 formula/channel), or stereo (2 formulas/channels): you can use as 47 many formulas/channels as you want. 48 49 Using a single dot (`.`) as a formula reuses the previous formula, as a 50 shortcut. 51 52 While many of the examples below save to files, you can just pipe to a media 53 player such as mpv, to play results directly instead. 54 55 56 Examples 57 58 # thuds commonly used in club music as beats 59 waveout 2 'sin(10 * tau * exp(-20 * u)) * exp(-2 * u)' > club-beats.wav 60 61 # a heartbeat-like sound lasting 2 seconds 62 # waveout 2 'sum(sin(10*tau*exp(-20*v))*exp(-2*v) for v in (u, (u-0.25)%1))/2' 63 64 # 1 minute and 5 seconds of static-like random noise 65 waveout 65 'random()' > random-noise.wav 66 67 # many bell-like clicks in quick succession; can be a cellphone's ringtone 68 waveout 'sin(2048 * tau * t) * exp(-50 * (t%0.1))' > ringtone.wav 69 70 # (vaguely) similar to the door-opening sound from a home alarm 71 waveout 'sin(4096 * tau * t) * exp(-10 * (t%0.1))' > home-alarm.wav 72 73 # watch your ears: quickly increases frequency up to 2khz 74 waveout 'sin(2_000 * t * tau * t)' > frequency-sweep.wav 75 76 # 1-second 400hz test tone 77 waveout 'sin(400 * tau * t)' > test-tone-400.wav 78 79 # 2s of a 440hz test tone, also called an A440 sound 80 waveout 2 'sin(440 * tau * t)' > a440.wav 81 82 # 1s 400hz test tone with sudden volume drop at the end, to avoid clip 83 waveout 'sin(400 * tau * t) * min(1, exp(-100*(t-0.9)))' > nice-tone.wav 84 85 # old ringtone used in north america 86 waveout '0.5*sin(350 * tau * t) + 0.5*sin(450 * tau * t)' > na-ringtone.wav 87 88 # 20 seconds of periodic pings 89 waveout 20 'sin(800 * tau * u) * exp(-20 * u)' > pings.wav 90 91 # 2 seconds of a european-style dial-tone 92 waveout 2 '(sin(350 * tau * t) + sin(450 * tau * t)) / 2' > euro-dial-tone.wav 93 94 # 4 seconds of a north-american-style busy-phone signal 95 # 'min(1, exp(-90*(u-0.5))) * (sin(480*tau*t) + sin(620*tau*t)) / 2' 96 waveout 4 '(u < 0.5) * (sin(480*tau * t) + sin(620*tau * t)) / 2' > na-busy.wav 97 98 # hit the 51st key on a synthetic piano-like instrument 99 waveout 'sin(tau * 440 * 2**((51 - 49)/12) * t) * exp(-10*u)' > piano-key.wav 100 101 # hit of a synthetic snare-like sound 102 waveout 'random() * exp(-10 * t)' > synth-snare.wav 103 104 # a stereotypical `laser` sound 105 waveout 'sin(100 * tau * exp(-40 * t))' > laser.wav 106 107 # a heartbeat-like sound, lasting 4 seconds 108 waveout 4 'sum( 109 sin(a*tau*exp(-20*b)) * exp(-2*b) for (a, b) in ((12, u), (8, (u-0.25)%1)) 110 ) / 2' > heartbeat.wav 111 112 # a funny/wobbly sound, lasting 1.3 seconds 113 waveout 1.3 'sin(tau * (260 * sin(tau * t)) * t)' > woo-woah-wow.wav 114 ''' 115 116 117 import math 118 from math import \ 119 acos, acosh, asin, asinh, atan, atan2, atanh, cbrt, ceil, comb, \ 120 copysign, cos, cosh, degrees, dist, e, erf, erfc, exp, exp2, expm1, \ 121 fabs, factorial, floor, fmod, frexp, fsum, gamma, gcd, hypot, inf, \ 122 isclose, isfinite, isinf, isnan, isqrt, lcm, ldexp, lgamma, log, \ 123 log10, log1p, log2, modf, nan, nextafter, perm, pi, pow, prod, \ 124 radians, remainder, sin, sinh, sqrt, tan, tanh, tau, trunc, ulp 125 126 Math = math 127 power = pow 128 129 from random import \ 130 betavariate, choice, choices, expovariate, gammavariate, gauss, \ 131 getrandbits, getstate, lognormvariate, normalvariate, paretovariate, \ 132 randbytes, randint, random, randrange, sample, seed, setstate, \ 133 shuffle, triangular, uniform, vonmisesvariate, weibullvariate 134 135 from struct import Struct 136 from sys import argv, exit, stderr, stdout 137 from wave import open as open_wav 138 139 140 def clamp(x, lowest, highest): 141 return max(lowest, min(x, highest)) 142 143 def deinf(x, default): 144 return default if isinf(x) else x 145 146 def denan(x, default): 147 return default if isnan(x) else x 148 149 def ln(x): 150 return log(x) if x >= 0 else nan 151 152 def osc(x, freq = 1, phase = 0): 153 return sin(freq * tau * (x + phase)) 154 155 def revalue(x, default): 156 return default if isinf(x) or isnan(x) else x 157 158 def safe(f, x): 159 try: 160 return f(x) 161 except Exception: 162 return nan 163 164 recover = safe 165 rescue = safe 166 trycall = safe 167 168 def scale(x, x0, x1, y0, y1): 169 return (y1 - y0) * (x - x0) / (x1 - x0) + y0 170 171 rescale = scale 172 rescaled = scale 173 scaled = scale 174 175 def sinc(x): 176 return sin(x) / x if x != 0.0 else 1.0 177 178 def until(t, until, k = 100): 179 'Keep a volume multiplier at 1, until an exponential drop-off.' 180 return min(1, exp(-abs(k) * (t - until))) 181 182 def unwrap(x, y0, y1): 183 return (y1 - y0) * x + y0 184 185 def wrap(x, y0, y1): 186 return (x - y0) / (y1 - y0) 187 188 189 # handle standard help cmd-line options, quitting right away in that case 190 if len(argv) == 2 and argv[1] in ('-h', '--h', '-help', '--help'): 191 print(info.strip(), file=stderr) 192 exit(0) 193 194 args = argv[1:] 195 196 # default duration is 1 second, but a leading float argument can override it 197 duration = 1.0 198 if len(args) > 0: 199 try: 200 duration = float(args[0]) 201 if duration < 0 or isnan(duration) or isinf(duration): 202 duration = 0.0 203 args = args[1:] 204 except Exception: 205 pass 206 207 # use all remaining args as formulas for the audio channel(s): single 208 # dots just reuse the previous formula, as a shortcut 209 sources = [] 210 prev_src = '0.0' 211 for s in args: 212 if s == '.': 213 s = prev_src 214 sources.append(s) 215 prev_src = s 216 217 if len(sources) == 0: 218 print(info.strip(), file=stderr) 219 exit(0) 220 221 try: 222 # giving func eval compiled strings considerably speeds things up 223 comp = lambda i, s: compile(s, f'<channel {i+1}>', 'eval') 224 expressions = [comp(i, s) for i, s in enumerate(sources)] 225 226 with open_wav(stdout.buffer, 'wb') as w: 227 rate = 48_000 228 samples = int(duration * rate) 229 230 open = open_wav = comp = compile = None 231 w.setparams((len(sources), 2, rate, samples, 'NONE', 'none')) 232 233 _ = f = 0.0 234 dt = 1.0 / rate 235 packer = Struct('<h') 236 237 for i in range(samples): 238 t = i * dt 239 u = fmod(t, 1.0) 240 241 for e in expressions: 242 _ = f = min(max(eval(e), -1.0), +1.0) 243 w.writeframesraw(packer.pack(int(32_767 * f))) 244 except BrokenPipeError: 245 # quit quietly, instead of showing a confusing error message 246 stderr.close() 247 except KeyboardInterrupt: 248 exit(2) 249 except Exception as e: 250 print(f'\x1b[31m{e}\x1b[0m', file=stderr) 251 exit(1)