Skip to content

Instantly share code, notes, and snippets.

@OptoCloud
Created December 14, 2025 01:18
Show Gist options
  • Select an option

  • Save OptoCloud/292c855b229b70945bef3dfe01f815ef to your computer and use it in GitHub Desktop.

Select an option

Save OptoCloud/292c855b229b70945bef3dfe01f815ef to your computer and use it in GitHub Desktop.
--==============================================================
-- 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