Files
OBS-Plugins/Clip-Chime/clip-notify.lua

190 lines
6.5 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--[[
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, -- 0100
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 0x00000xFFFF)
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.01.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: 065536 (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 [[<h3>🎬 Clip Notify &nbsp;<small>v3</small></h3>
<p>Plays a chime the moment your replay buffer clip is saved.</p>
<p><b>Windows:</b> native winmm — no subprocess, instant.<br>
<b>macOS:</b> afplay &nbsp;|&nbsp; <b>Linux:</b> paplay / aplay</p>
<br>
<b>Setup:</b> pick your .wav, set volume, hit <b>Test Chime Now</b>.]]
end