diff --git a/Clip-Chime/clip-notify.lua b/Clip-Chime/clip-notify.lua new file mode 100644 index 0000000..5fe1662 --- /dev/null +++ b/Clip-Chime/clip-notify.lua @@ -0,0 +1,189 @@ +--[[ + clip-notify.lua v3 + ─────────────────────────────────────────────────────── + "Clip Saved" chime for OBS Studio. + Plays a sound + logs to OBS when the replay buffer saves. + + Install: + 1. Place this file + your .wav in the same folder. + 2. OBS → Tools → Scripts → "+" → select clip-notify.lua + 3. Set your chime file & volume, hit Test. + ─────────────────────────────────────────────────────── +--]] + +obs = obslua + +local cfg = { + play_sound = true, + sound_path = "", + chime_volume = 80, -- 0–100 + notify_on_record = false, +} + +-- ── Platform ────────────────────────────────────────── +local is_windows = package.config:sub(1,1) == "\\" +local is_mac, is_linux = false, false +if not is_windows then + local f = io.popen("uname -s 2>/dev/null") + if f then + local u = f:read("*l"); f:close() + is_mac = (u == "Darwin") + is_linux = not is_mac + end +end + +-- ── FFI (Windows only) ──────────────────────────────── +local ffi, winmm +local ffi_ok = false + +if is_windows then + local ok; ok, ffi = pcall(require, "ffi") + if ok then + ffi.cdef[[ + /* winmm */ + int __stdcall PlaySoundA(const char* pszSound, void* hmod, unsigned long fdwSound); + unsigned long __stdcall waveOutSetVolume(void* hwo, unsigned long dwVolume); + unsigned long __stdcall waveOutGetVolume(void* hwo, unsigned long* pdwVolume); + ]] + local ok2; ok2, winmm = pcall(ffi.load, "winmm") + ffi_ok = ok2 + if ffi_ok then + obs.blog(obs.LOG_INFO, "[clip-notify] winmm FFI ready") + else + obs.blog(obs.LOG_WARNING, "[clip-notify] winmm failed to load — sound disabled") + end + end +end + +-- ── Volume helpers ──────────────────────────────────── +-- waveOutSetVolume: low word = left, high word = right (each 0x0000–0xFFFF) +local function pct_to_dword(pct) + local v = math.floor((math.max(0, math.min(100, pct)) / 100) * 0xFFFF) + return v + v * 0x10000 +end + +-- ── Sound playback ──────────────────────────────────── +local SND_FILENAME = 0x00020000 +local SND_ASYNC = 0x00000001 +local SND_NODEFAULT = 0x00000002 + +local function play_chime(path) + if not cfg.play_sound or path == "" then return end + + if is_windows then + if not ffi_ok then return end + + -- Save current wave-out volume + local prev = ffi.new("unsigned long[1]", 0) + winmm.waveOutGetVolume(nil, prev) + local saved = prev[0] + + -- Apply our volume + winmm.waveOutSetVolume(nil, pct_to_dword(cfg.chime_volume)) + + -- Fire async — returns immediately + winmm.PlaySoundA(path, nil, SND_FILENAME + SND_ASYNC + SND_NODEFAULT) + + -- Restore original volume after the chime has finished (1.5s headroom) + obs.timer_add(function() + winmm.waveOutSetVolume(nil, saved) + obs.remove_current_callback() + end, 1500) + + elseif is_mac then + -- afplay -v accepts 0.0–1.0 + local vol = string.format("%.2f", cfg.chime_volume / 100) + os.execute(string.format('afplay -v %s "%s" &', vol, path)) + + elseif is_linux then + -- paplay --volume: 0–65536 (100% = 65536) + local vol = math.floor((cfg.chime_volume / 100) * 65536) + os.execute(string.format( + 'paplay --volume=%d "%s" 2>/dev/null || aplay "%s" 2>/dev/null &', + vol, path, path)) + end +end + +-- ── Handlers ───────────────────────────────────────── +local function on_clip_saved() + play_chime(cfg.sound_path) + local filename = "" + if obs.obs_frontend_get_last_replay ~= nil then + local raw = obs.obs_frontend_get_last_replay() + if raw and raw ~= "" then + filename = raw:match("([^/\\]+)$") or raw + end + end + obs.blog(obs.LOG_INFO, "[clip-notify] Clip saved → " .. (filename ~= "" and filename or "done")) +end + +local function on_recording_saved() + if not cfg.notify_on_record then return end + play_chime(cfg.sound_path) + obs.blog(obs.LOG_INFO, "[clip-notify] Recording saved.") +end + +local function on_event(event) + if event == obs.OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED then on_clip_saved() + elseif event == obs.OBS_FRONTEND_EVENT_RECORDING_STOPPED then on_recording_saved() + end +end + +-- ── Script UI ───────────────────────────────────────── +function script_properties() + local p = obs.obs_properties_create() + + obs.obs_properties_add_bool(p, "play_sound", "Enable chime sound") + + obs.obs_properties_add_path(p, "sound_path", + "Chime sound file (.wav)", + obs.OBS_PATH_FILE, + "WAV Files (*.wav);;All Files (*.*)", + script_path()) + + obs.obs_properties_add_int_slider(p, "chime_volume", "Volume", 0, 100, 1) + + obs.obs_properties_add_bool(p, "notify_on_record", + "Also chime when a recording is saved") + + obs.obs_properties_add_button(p, "test_btn", "▶ Test Chime Now", + function() on_clip_saved(); return true end) + + return p +end + +function script_defaults(s) + obs.obs_data_set_default_bool (s, "play_sound", true) + obs.obs_data_set_default_string(s, "sound_path", "") + obs.obs_data_set_default_int (s, "chime_volume", 80) + obs.obs_data_set_default_bool (s, "notify_on_record", false) +end + +function script_update(s) + cfg.play_sound = obs.obs_data_get_bool (s, "play_sound") + cfg.sound_path = obs.obs_data_get_string(s, "sound_path") + cfg.chime_volume = obs.obs_data_get_int (s, "chime_volume") + cfg.notify_on_record = obs.obs_data_get_bool (s, "notify_on_record") + if cfg.sound_path == "" then + cfg.sound_path = script_path() .. "clip-saved.wav" + end +end + +function script_load(s) + script_update(s) + obs.obs_frontend_add_event_callback(on_event) + obs.blog(obs.LOG_INFO, "[clip-notify] v3 loaded.") +end + +function script_unload() + obs.blog(obs.LOG_INFO, "[clip-notify] Unloaded.") +end + +function script_description() + return [[
Plays a chime the moment your replay buffer clip is saved.
+Windows: native winmm — no subprocess, instant.
+macOS: afplay | Linux: paplay / aplay