#!/usr/bin/python # The MIT License (MIT) # # Copyright (c) 2026 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 = ''' sboard [options...] [sound...] [duration...] [volume...] Sound BOARD plays the sound given by name, lasting the number of seconds given, or 1 second by default. The audio output is 16-bit samples at 48khz. There's also an optional volume argument, which is 1 by default. Options, all of which can start with either 1 or 2 dashes: -h, -help show this help message -o, -out, -output emit WAV-format bytes to standard output ''' from math import ceil, cos, e, exp, floor, fmod, isinf, isnan, log, sin, tau from random import random from struct import Struct from sys import argv, exit, stderr, stdout from wave import open as open_wav def beeper(i, t): return sin(2000 * tau * t) * (fmod(t, 0.5) < 0.0625) def bell(i, t): u = fmod(t, 1) return sin(880 * tau * u) * exp(-10 * u) # busy-phone tone def busy(i, t): u = fmod(t, 1) k = min(1, exp(-90 * (u - 0.5))) return k * (sin(480 * tau * t) + sin(620 * tau * t)) / 2 def door_ajar(i, t): u = fmod(t, 1) return sin(660 * tau * u) * exp(-10 * u) def heartbeat(i, t): beat = 0.0 u = fmod(t, 1) beat += sin(12 * tau * exp(-20 * u)) * exp(-2 * u) u = max(0, u - 0.25) beat += sin(8 * tau * exp(-20 * u)) * exp(-2 * u) return 0.5 * beat # a once-a-seconds stereotypical laser sound def laser(i, t): u = fmod(t, 1) return sin(100 * tau * exp(-40 * u)) # flat/uniform random noise def noise(i, t): return 2 * random() - 1 # ready-phone tone def ready(i, t): return 0.5 * sin(350 * tau * t) + 0.5 * sin(450 * tau * t) # annoying ringtone def ringtone(i, t): u = fmod(t, 0.1) return sin(2048 * tau * t) * exp(-50 * u) def thud(i, t): u = fmod(t, 1) return sin(12 * tau * exp(-20 * u)) * exp(-2 * u) # 440hz tuning tone def tone(i, t): return sin(440 * tau * t) def woo_woah_wow(i, t): # period = 1.3 period = 1.25 u = fmod(t, period) return sin(tau * (260 * sin(tau * u)) * u) # functions used by fancier tunes def kick(t, f, k, p): return sin(tau * f * pow(p, t)) * exp(-k * t) def default_kick(t, f, k): return kick(t, f, k, 0.085) # flat/uniform random noise def random_uniform(): return 2 * random() - 1 def hit_hi_hat(t, k): return random_uniform() * exp(-k * t) def schedule(t, delay, period): return fmod(t + (1 - delay) * period, period) def arp(x, y, z, k, t): u = fmod(t / 2, k) return sin(x * (exp(-y * u))) * exp(-z * u) def linterp(x, y, k): return k * x + (1 - k) * y def power_synth(t, freq): # function linspace(a, b, n) { # const y = new Array(n); # const incr = (b - a) / (n + 1); # for (let i = 0; i < n; i++) # y[i] = a + incr * i; # return y; # } # bass_powers = linspace(1e-5, 1, 10).map(x => -0.05 * Math.log(x)); powers = [ 0.5756462732485115, 0.11988976388990187, 0.08523515466254475, 0.06496281589095718, 0.05057917059158016, 0.03942226802181348, 0.030306373513585217, 0.022598970473683477, 0.015922499056278294, 0.010033423662119907, ] res = 0.0 for i, p in enumerate(powers): res += p * cos(tau * (i + 1) * freq * t) return res def bass_envelope(t): u = fmod(t, 1) return 15 * u if (u < 0.05) else exp(-7 * u) # fancier tunes def bust_a_move(i, t): period = 4.09 freqs = [ 50, 75, 50, 50, 75, 50, 50, 50, 75, 50, 50, 50, 50, 50, 75, 50 ] # const delays = [ # 0, 0.52, 0.77, 1.28, 1.54, 1.79, # 2.04, 2.3, 2.56, 2.67, # 3.05, 3.07, 3.2, 3.45, 3.57, 3.82 # ].map(x => x / period); delays = [ 0.0000000000000000, 0.1271393643031785, 0.1882640586797066, 0.31295843520782396, 0.3765281173594132, 0.43765281173594134, 0.49877750611246946, 0.5623471882640586, 0.6259168704156479, 0.6528117359413202, 0.745721271393643, 0.7506112469437652, 0.78239608801956, 0.8435207823960881, 0.8728606356968215, 0.9339853300733496, ] u = fmod(t, period) half = int(len(freqs) / 2) start = 0 if u < 2.5 else half kicks = 0.0 for i in range(half): d = delays[start + i] f = freqs[start + i] kicks += default_kick(schedule(t, d, period), f, 50) hi_hats = 0.0 for i in range(half): hi_hats += 0*hit_hi_hat(schedule(t, i / half, period / half), 25) return 0.9 * kicks + 1.0 / 32 * hi_hats def crazy(i, t): snares = [ 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, ] kicks = [ 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, ] bass_speed = [ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, ] def seq(d, s, t): return d[floor(t / s / 2) % len(kicks)] t *= 148.0 / 120 # period = 6.5 # u = fmod(t, period) # v = fmod(t, 2*period) rand = linterp(random(), 1, 1.0 / 2) anticlip = max(-log(.75 * fmod(8 * t, 1) + 1.0 / e), 0) k = anticlip * arp(60, 40, 20, 1.0 / 16, t) * seq(kicks, 1.0 / 16, t) s = 0.3 * arp(60, 80, 3, 1.0 / 16, t) * rand * seq(snares, 1.0 / 16, t) su = seq(bass_speed, 1.0 / 16, t) b1 = bass_envelope(su * t) * power_synth(su * t, 50.0 / su) v = power_synth(su * (t + 0.5 * 6.5), 60.0 / su) b2 = bass_envelope(su * (t + 0.5 * 6.5)) * v b = b1 + b2 return 0.9 * k + 0.7 * s + 0.7 * b def piano(t, n): p = (n - 49) / 12 f = 440 * pow(2, p) return sin(tau * f * t) def piano_loop(i, t): period = 1.025 cutoff = 12 p = period y = 0 y += piano(t, 49) * exp(-cutoff * fmod(t, period)) y += piano(t + 0.25 * p, 50) * exp(-cutoff * fmod(t + 0.25 * p, p)) y += piano(t + 0.50 * p, 54) * exp(-cutoff * fmod(t + 0.50 * p, p)) y += piano(t + 0.75 * p, 51) * exp(-cutoff * fmod(t + 0.75 * p, p)) return 0.75 * y def walk_this_way(i, t): period = 2.2 freqs = [50, 70, 50, 50, 50, 70] delays = [0.000, 0.250, 0.450, 0.500, 0.625, 0.750] n = len(delays) kicks = 0 for i in range(n): kicks += default_kick(schedule(t, delays[i], period), freqs[i], 50) hi_hats = 1.2 * hit_hi_hat(schedule(t, 0, period), 7) for i in range(n): hi_hats += hit_hi_hat(schedule(t, i, period / 8), 30) return 1 * kicks + 0.1 * hi_hats aliases = { 'beeper': 'beeper', 'beeps': 'beeper', 'bell': 'bells', 'bells': 'bells', 'busy': 'busy', 'door-ajar': 'door-ajar', 'doorajar': 'door-ajar', 'heart': 'heart', 'heart-beat': 'heart', 'heart-beats': 'heart', 'heartbeat': 'heart', 'heartbeats': 'heart', 'laser': 'laser', 'noise': 'noise', 'ready': 'ready', 'ring-tone': 'ringtone', 'ringtone': 'ringtone', 'thud': 'thud', '440': 'tone', '440hz': 'tone', 'tone': 'tone', 'woo-woah-wow': 'woo-woah-wow', 'woo': 'woo-woah-wow', 'woah': 'woo-woah-wow', 'wow': 'woo-woah-wow', 'bust-a-move': 'bust-a-move', 'bustamove': 'bust-a-move', 'crazy': 'crazy', 'piano': 'piano-loop', 'piano-loop': 'piano-loop', 'pianoloop': 'piano-loop', 'walk-this-way': 'walk-this-way', 'walkthisway': 'walk-this-way', } entries = { 'beeper': (beeper, 'beep some TV-related devices used to make'), 'bells': (bell, 'a synthetic bell'), 'busy': (busy, 'a busy phone'), 'door-ajar': (door_ajar, 'door-ajar warning sound'), 'heart': (heartbeat, 'pairs of heart-pulses'), 'laser': (laser, 'a stereotypical laser sound, once a second'), 'noise': (noise, 'uniform random noise (annoying)'), 'ready': (ready, 'a ready phone'), 'ringtone': (ringtone, 'a slightly annoying ringtone'), 'thud': (thud, 'a low beat, similar to those in nightclubs'), 'tone': (tone, 'a 440hz tuning tone'), 'woo-woah-wow': (woo_woah_wow, 'a crazy sound (loop: 1.25s)'), 'bust-a-move': (bust_a_move, 'middle part of that song (loop: 4.09s)'), 'crazy': (crazy, 'a repeating tune (NOT FULLY WORKING) (loop: 6.5s)'), 'piano-loop': (piano_loop, 'a few repeating piano notes (loop: 1.025s)'), 'walk-this-way': (walk_this_way, 'drums from that song (loop: 2.2s)'), } def show_help(w): print(info.strip(), file=w) print('', file=w) print('Sound-effects available', file=w) print('', file=w) for name, (_, note) in entries.items(): print(f' {name:16} {note}', file=w) print('', file=w) print('Aliases available', file=w) print('', file=w) for k in aliases.keys(): print(f' {k}', file=w) # handle standard help cmd-line options, quitting right away in that case if len(argv) > 1 and argv[1] in ('-h', '--h', '-help', '--help'): show_help(stdout) exit(0) output_opts = ( '-o', '--o', '-out', '--out', '-output', '--output', ) args = argv[1:] name = '' output = False if len(args) > 0: name = args[0] args = args[1:] else: show_help(stderr) exit(1) if name in aliases: name = aliases[name] if len(args) > 0 and args[0] in output_opts: args = args[1:] output = True # default duration is 1 second, but a float argument can override it duration = 1.0 if len(args) > 0: try: duration = float(args[0]) args = args[1:] if duration < 0 or isnan(duration) or isinf(duration): duration = 0.0 except Exception: pass if len(args) > 0 and args[0] in output_opts: args = args[1:] output = True volume = 1.0 if len(args) > 0: try: volume = float(args[0]) args = args[1:] if volume < 0 or volume > 1 or isnan(volume) or isinf(volume): volume = 1.0 except Exception: pass if len(args) > 0 and args[0] in output_opts: args = args[1:] output = True # demand the output option, to behave more like ringtone.c if not output: print('live-sound mode not supported: use option -o instead', file=stderr) exit(1) if not name in entries: print(f'no sound-effect named "{name}" is available', file=stderr) exit(1) rate = 48000.0 dt = 1.0 / rate samples = int(ceil(duration * rate)) packer = Struct('