#!/usr/bin/python3 # The MIT License (MIT) # # Copyright © 2024 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. info = ''' waveout [seconds...] [python expressions...] WAVE OUTput emits wave-format audio using the expression given to calculate samples. The audio output format is RIFF WAVE (wav) sampled at 48khz. The duration is an optional floating-point number of seconds: if not given, the default duration is 1 second. The formulas/expressions can use floating-point value `t`, which has the current time, integer `i`, which is the 0-based index of the current sample being calculated, as well as all names (functions and values) from the built-in python modules `math` and `random`. A few extra convenience funcs are also defined and are available to the expressions. The formulas are expected to result in floating-point values between -1.0 and +1.0; other types of results will end in an error. The number of expressions determines the number of sound-channels, such as mono (1 formula/channel), or stereo (2 formulas/channels): you can use as many formulas/channels as you want. Using a single dot (`.`) as a formula reuses the previous formula, as a shortcut. While many of the examples below save to files, you can just pipe to a media player such as mpv, to play results directly instead. Examples # thuds commonly used in club music as beats waveout 2 'sin(10 * tau * exp(-20 * u)) * exp(-2 * u)' > club-beats.wav # a heartbeat-like sound lasting 2 seconds # waveout 2 'sum(sin(10*tau*exp(-20*v))*exp(-2*v) for v in (u, (u-0.25)%1))/2' # 1 minute and 5 seconds of static-like random noise waveout 65 'random()' > random-noise.wav # many bell-like clicks in quick succession; can be a cellphone's ringtone waveout 'sin(2048 * tau * t) * exp(-50 * (t%0.1))' > ringtone.wav # (vaguely) similar to the door-opening sound from a home alarm waveout 'sin(4096 * tau * t) * exp(-10 * (t%0.1))' > home-alarm.wav # watch your ears: quickly increases frequency up to 2khz waveout 'sin(2_000 * t * tau * t)' > frequency-sweep.wav # 1-second 400hz test tone waveout 'sin(400 * tau * t)' > test-tone-400.wav # 2s of a 440hz test tone, also called an A440 sound waveout 2 'sin(440 * tau * t)' > a440.wav # 1s 400hz test tone with sudden volume drop at the end, to avoid clip waveout 'sin(400 * tau * t) * min(1, exp(-100*(t-0.9)))' > nice-tone.wav # old ringtone used in north america waveout '0.5*sin(350 * tau * t) + 0.5*sin(450 * tau * t)' > na-ringtone.wav # 20 seconds of periodic pings waveout 20 'sin(800 * tau * u) * exp(-20 * u)' > pings.wav # 2 seconds of a european-style dial-tone waveout 2 '(sin(350 * tau * t) + sin(450 * tau * t)) / 2' > euro-dial-tone.wav # 4 seconds of a north-american-style busy-phone signal # 'min(1, exp(-90*(u-0.5))) * (sin(480*tau*t) + sin(620*tau*t)) / 2' waveout 4 '(u < 0.5) * (sin(480*tau * t) + sin(620*tau * t)) / 2' > na-busy.wav # hit the 51st key on a synthetic piano-like instrument waveout 'sin(tau * 440 * 2**((51 - 49)/12) * t) * exp(-10*u)' > piano-key.wav # hit of a synthetic snare-like sound waveout 'random() * exp(-10 * t)' > synth-snare.wav # a stereotypical `laser` sound waveout 'sin(100 * tau * exp(-40 * t))' > laser.wav # a heartbeat-like sound, lasting 4 seconds waveout 4 'sum( sin(a*tau*exp(-20*b)) * exp(-2*b) for (a, b) in ((12, u), (8, (u-0.25)%1)) ) / 2' > heartbeat.wav # a funny/wobbly sound, lasting 1.3 seconds waveout 1.3 'sin(tau * (260 * sin(tau * t)) * t)' > woo-woah-wow.wav ''' import math from math import \ acos, acosh, asin, asinh, atan, atan2, atanh, cbrt, ceil, comb, \ copysign, cos, cosh, degrees, dist, e, erf, erfc, exp, exp2, expm1, \ fabs, factorial, floor, fmod, frexp, fsum, gamma, gcd, hypot, inf, \ isclose, isfinite, isinf, isnan, isqrt, lcm, ldexp, lgamma, log, \ log10, log1p, log2, modf, nan, nextafter, perm, pi, pow, prod, \ radians, remainder, sin, sinh, sqrt, tan, tanh, tau, trunc, ulp Math = math power = pow from random import \ betavariate, choice, choices, expovariate, gammavariate, gauss, \ getrandbits, getstate, lognormvariate, normalvariate, paretovariate, \ randbytes, randint, random, randrange, sample, seed, setstate, \ shuffle, triangular, uniform, vonmisesvariate, weibullvariate from struct import Struct from sys import argv, exit, stderr, stdout from wave import open as open_wav def clamp(x, lowest, highest): return max(lowest, min(x, highest)) def deinf(x, default): return default if isinf(x) else x def denan(x, default): return default if isnan(x) else x def ln(x): return log(x) if x >= 0 else nan def osc(x, freq = 1, phase = 0): return sin(freq * tau * (x + phase)) def revalue(x, default): return default if isinf(x) or isnan(x) else x def safe(f, x): try: return f(x) except Exception: return nan recover = safe rescue = safe trycall = safe def scale(x, x0, x1, y0, y1): return (y1 - y0) * (x - x0) / (x1 - x0) + y0 rescale = scale rescaled = scale scaled = scale def sinc(x): return sin(x) / x if x != 0.0 else 1.0 def until(t, until, k = 100): 'Keep a volume multiplier at 1, until an exponential drop-off.' return min(1, exp(-abs(k) * (t - until))) def unwrap(x, y0, y1): return (y1 - y0) * x + y0 def wrap(x, y0, y1): return (x - y0) / (y1 - y0) # handle standard help cmd-line options, quitting right away in that case if len(argv) == 2 and argv[1] in ('-h', '--h', '-help', '--help'): print(info.strip(), file=stderr) exit(0) args = argv[1:] # default duration is 1 second, but a leading float argument can override it duration = 1.0 if len(args) > 0: try: duration = float(args[0]) if duration < 0 or isnan(duration) or isinf(duration): duration = 0.0 args = args[1:] except Exception: pass # use all remaining args as formulas for the audio channel(s): single # dots just reuse the previous formula, as a shortcut sources = [] prev_src = '0.0' for s in args: if s == '.': s = prev_src sources.append(s) prev_src = s if len(sources) == 0: print(info.strip(), file=stderr) exit(0) try: # giving func eval compiled strings considerably speeds things up comp = lambda i, s: compile(s, f'', 'eval') expressions = [comp(i, s) for i, s in enumerate(sources)] with open_wav(stdout.buffer, 'wb') as w: rate = 48_000 samples = int(duration * rate) open = open_wav = comp = compile = None w.setparams((len(sources), 2, rate, samples, 'NONE', 'none')) _ = f = 0.0 dt = 1.0 / rate packer = Struct('