Created
December 14, 2025 01:18
-
-
Save OptoCloud/292c855b229b70945bef3dfe01f815ef to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --============================================================== | |
| -- McDonald's Machine Alarm Simulator (CC:Tweaked Speaker PCM) | |
| --============================================================== | |
| -- This program emulates the overlapping alarm sounds commonly | |
| -- heard from McDonald's kitchen machines (fryers, grills, UHCs). | |
| -- | |
| -- It synthesizes raw PCM audio at 48 kHz and streams it directly | |
| -- to a CC:Tweaked speaker peripheral using playAudio(). | |
| -- | |
| -- Key characteristics: | |
| -- - Two independent alarm patterns (A and B) | |
| -- - Distinct high / mid / low pitched beeps | |
| -- - Slight detuning, vibrato, and soft saturation for realism | |
| -- - Triangle-based waveform with smoothing and envelopes | |
| -- - PCM merging logic so alarm B mutes alarm A when overlapping | |
| -- - Scheduling logic to allow alarms to drift and collide | |
| -- | |
| -- Alarm behavior: | |
| -- Alarm A: Slower, lower-pitched repeating pattern | |
| -- Alarm B: Faster, high-pitched urgent beeps | |
| -- | |
| -- When both alarms trigger at the same time, a single merged | |
| -- sound is played, just like real kitchen equipment where one | |
| -- alarm cuts through another. | |
| -- | |
| -- Requirements: | |
| -- - CC:Tweaked | |
| -- - Speaker peripheral attached on the RIGHT side | |
| -- | |
| -- Notes: | |
| -- - Uses signed 8-bit PCM samples (-128 .. 127) | |
| -- - Sample rate is fixed at 48,000 Hz | |
| -- - Designed for continuous looping operation | |
| -- | |
| -- Authenticity warning: | |
| -- Running this near other people may cause involuntary | |
| -- stress responses, flashbacks, or sudden urges to check | |
| -- fries. | |
| --============================================================== | |
| local speaker = peripheral.wrap("right") | |
| if not speaker then error("No speaker on right") end | |
| local SAMPLE_RATE = 48000 | |
| ------------------------------------------------- | |
| -- Speaker helper | |
| ------------------------------------------------- | |
| local function push_and_get_len(samples) | |
| while not speaker.playAudio(samples) do | |
| os.pullEvent("speaker_audio_empty") | |
| end | |
| return #samples / SAMPLE_RATE | |
| end | |
| ------------------------------------------------- | |
| -- PCM helpers (signed -128..127) | |
| ------------------------------------------------- | |
| local function clamp(v) | |
| if v < -128 then return -128 end | |
| if v > 127 then return 127 end | |
| return v | |
| end | |
| local function silence(sec) | |
| local n = math.floor(sec * SAMPLE_RATE) | |
| local t = {} | |
| for i = 1, n do t[i] = 0 end | |
| return t | |
| end | |
| ------------------------------------------------- | |
| -- Alarm waveform generator (smoothed triangle) | |
| ------------------------------------------------- | |
| local function triangle(freq, sec, amp) | |
| amp = amp or 90 | |
| local n = math.floor(sec * SAMPLE_RATE) | |
| local t = {} | |
| local phase = math.random() | |
| local detune_cents = (math.random() * 8 - 4) | |
| local detune_ratio = 2 ^ (detune_cents / 1200) | |
| local base_freq = freq * detune_ratio | |
| local vib_rate = 5.0 + math.random() * 2.0 | |
| local vib_depth = 0.0018 + math.random() * 0.0012 | |
| local alpha = 0.34 | |
| local last = 0 | |
| local attack = 0.003 + math.random() * 0.003 | |
| local release = 0.008 + math.random() * 0.008 | |
| local aN = math.max(1, math.floor(attack * SAMPLE_RATE)) | |
| local rN = math.max(1, math.floor(release * SAMPLE_RATE)) | |
| local drive = 1.15 + math.random() * 0.10 | |
| local function softclip(x) | |
| local ax = math.abs(x) | |
| return x / (1 + 0.8 * ax) | |
| end | |
| local q_err = 0.0 | |
| local function tpdf_dither() | |
| return (math.random() - math.random()) * 0.5 | |
| end | |
| for i = 1, n do | |
| local time = (i - 1) / SAMPLE_RATE | |
| local f = base_freq * (1 + math.sin(2 * math.pi * vib_rate * time) * vib_depth) | |
| local step = f / SAMPLE_RATE | |
| phase = phase + step | |
| if phase >= 1 then phase = phase - 1 end | |
| local v | |
| if phase < 0.25 then | |
| v = phase * 4 | |
| elseif phase < 0.75 then | |
| v = 2 - phase * 4 | |
| else | |
| v = phase * 4 - 4 | |
| end | |
| last = last + alpha * (v - last) | |
| local env = 1.0 | |
| if i <= aN then | |
| local x = i / aN | |
| env = x * x | |
| elseif i > (n - rN) then | |
| local x = (n - i) / rN | |
| if x < 0 then x = 0 end | |
| env = x * x | |
| end | |
| local s = softclip(last * drive) * amp * env | |
| local y = s + q_err + tpdf_dither() | |
| local q = math.floor(y + 0.5) | |
| q_err = y - q | |
| t[i] = clamp(q) | |
| end | |
| return t | |
| end | |
| ------------------------------------------------- | |
| -- Table helpers | |
| ------------------------------------------------- | |
| local function append(dst, src) | |
| local n = #dst | |
| for i = 1, #src do dst[n + i] = src[i] end | |
| end | |
| local function build(parts) | |
| local out = {} | |
| for i = 1, #parts do append(out, parts[i]) end | |
| return out | |
| end | |
| ------------------------------------------------- | |
| -- Merge utility (B mutes A wherever B exists) | |
| -- a_start / b_start are 1-based timeline indices. | |
| -- Returns: | |
| -- out : contiguous Lua array of samples | |
| -- out_len : number of samples in out | |
| -- out_start : timeline sample index corresponding to out[1] | |
| ------------------------------------------------- | |
| local function merge_pcm_mute_a_under_b(a, b, a_start, b_start) | |
| a_start = a_start or 1 | |
| b_start = b_start or 1 | |
| local a_len, b_len = #a, #b | |
| local a_end = a_start + a_len - 1 | |
| local b_end = b_start + b_len - 1 | |
| local out_start = math.min(a_start, b_start) | |
| local out_end = math.max(a_end, b_end) | |
| local out_len = out_end - out_start + 1 | |
| local out = {} | |
| for ti = out_start, out_end do | |
| local out_i = ti - out_start + 1 | |
| local a_i = ti - a_start + 1 | |
| if a_i >= 1 and a_i <= a_len then | |
| out[out_i] = a[a_i] | |
| else | |
| out[out_i] = 0 | |
| end | |
| local b_i = ti - b_start + 1 | |
| if b_i >= 1 and b_i <= b_len then | |
| out[out_i] = b[b_i] | |
| end | |
| end | |
| return out, out_len, out_start | |
| end | |
| ------------------------------------------------- | |
| -- Tuning / precompute notes | |
| ------------------------------------------------- | |
| math.randomseed(os.epoch("utc") % 2147483647) | |
| local F_HIGH = 2000 | |
| local F_MID = 1700 | |
| local F_LOW = 1000 | |
| local DUR = 0.140 | |
| local DUR_SHORT = 0.090 | |
| local GAP = 0.010 | |
| local PAUSE_B = 0.060 | |
| local AMP = 90 | |
| local SIL_GAP = silence(GAP) | |
| local SIL_PAUSE = silence(PAUSE_B) | |
| local NOTE_HIGH = triangle(F_HIGH, DUR_SHORT, AMP) | |
| local NOTE_MID = triangle(F_MID, DUR, AMP) | |
| local NOTE_LOW = triangle(F_LOW, DUR, AMP) | |
| ------------------------------------------------- | |
| -- Sound A | |
| ------------------------------------------------- | |
| local WAIT_A = 0.85 | |
| local SIL_WAIT_A = silence(WAIT_A) | |
| local A = build({ | |
| -- First part (-_-) | |
| NOTE_MID, SIL_GAP, | |
| NOTE_LOW, SIL_GAP, | |
| NOTE_MID, | |
| -- wait | |
| SIL_WAIT_A, | |
| -- Second part (-_-_-) | |
| NOTE_MID, SIL_GAP, | |
| NOTE_LOW, SIL_GAP, | |
| NOTE_MID, SIL_GAP, | |
| NOTE_LOW, SIL_GAP, | |
| NOTE_MID | |
| }) | |
| ------------------------------------------------- | |
| -- Sound B | |
| ------------------------------------------------- | |
| local B = build({ | |
| NOTE_HIGH, SIL_GAP, | |
| SIL_PAUSE, | |
| NOTE_HIGH, SIL_GAP, | |
| NOTE_HIGH, SIL_GAP, | |
| NOTE_HIGH | |
| }) | |
| local ALARM1_INTERVAL = 2.2 | |
| local ALARM2_INTERVAL = 1.35 | |
| local t1 = 0 | |
| local t2 = 0 | |
| local function play(samples) | |
| local played = push_and_get_len(samples) | |
| t1 = t1 - played | |
| t2 = t2 - played | |
| end | |
| while true do | |
| -- sleep until next event | |
| local dt = math.min(t1, t2) | |
| if dt > 0 then | |
| sleep(dt) | |
| t1 = t1 - dt | |
| t2 = t2 - dt | |
| end | |
| -- If both are due at the same time, play ONE combined sound and reschedule both. | |
| if t1 <= 0 and t2 <= 0 then | |
| play(merge_pcm_mute_a_under_b(A, B, 1, 1)) -- B on top of A | |
| t1 = t1 + ALARM1_INTERVAL | |
| t2 = t2 + ALARM2_INTERVAL | |
| else | |
| if t1 <= 0 then | |
| if t2 < (#A / SAMPLE_RATE) then | |
| play(merge_pcm_mute_a_under_b(A, B, 1, 1 + math.floor(t2 * SAMPLE_RATE))) | |
| t2 = t2 + ALARM2_INTERVAL | |
| else | |
| play(A) | |
| end | |
| t1 = t1 + ALARM1_INTERVAL | |
| end | |
| if t2 <= 0 then | |
| if t1 < (#B / SAMPLE_RATE) then | |
| play(merge_pcm_mute_a_under_b(B, A, 1, 1 + math.floor(t1 * SAMPLE_RATE))) | |
| t1 = t1 + ALARM1_INTERVAL | |
| else | |
| play(B) | |
| end | |
| t2 = t2 + ALARM2_INTERVAL | |
| end | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment