250 lines
9.3 KiB
Lua
250 lines
9.3 KiB
Lua
-- Piano speaker manager for ComputerCraft / CC:Tweaked
|
||
-- 功能:
|
||
-- - 自动发现所有 speaker 周边设备并形成池
|
||
-- - 将播放请求分配给未锁定的 speaker 并锁定,播放结束后解锁(使用 timer)
|
||
-- - 检测 peripheral attach/detach,动态更新池
|
||
-- - 当需要播放的音数 > 未锁定 speaker 数量时,在屏幕上显示警告
|
||
-- - 使用 peripheral.call(speakerName, "playNote", instrument, note) 播放原版音
|
||
|
||
local keys = keys or require("keys") -- 在 CC:Tweaked 中 keys 库存在;若不存在请直接移除这一行
|
||
local term = term
|
||
local peripheral = peripheral
|
||
local os = os
|
||
local print = print
|
||
|
||
-- 配置区 -----------------------------------------------------------------
|
||
local DEFAULT_INSTRUMENT = "harp" -- 你可以改为 "bassdrum", "snare", 等支持的 instrument
|
||
local DEFAULT_NOTE_DURATION = 0.5 -- 单位:秒,播放结束后解锁 speaker 所用的估计时长(可按需调整或为每个 note 指定)
|
||
-- 键位到音符的映射(示例):键名 -> {instrument, note, duration}
|
||
-- note 采用整数(0..24 等,根据你使用的 playNote 接口),duration 可选覆盖默认时长
|
||
local KEY_NOTE_MAP = {
|
||
eight = {instrument = DEFAULT_INSTRUMENT, note = 0, duration = 0.5}, -- F#3
|
||
nine = {instrument = DEFAULT_INSTRUMENT, note = 1, duration = 0.5}, -- G3
|
||
zero = {instrument = DEFAULT_INSTRUMENT, note = 2, duration = 0.5}, -- G#3
|
||
minus = {instrument = DEFAULT_INSTRUMENT, note = 3, duration = 0.5}, -- A3
|
||
equals = {instrument = DEFAULT_INSTRUMENT, note = 4, duration = 0.5}, -- A#3
|
||
q = {instrument = DEFAULT_INSTRUMENT, note = 5, duration = 0.5}, -- B3
|
||
w = {instrument = DEFAULT_INSTRUMENT, note = 6, duration = 0.5}, -- C4
|
||
e = {instrument = DEFAULT_INSTRUMENT, note = 7, duration = 0.5}, -- C#4
|
||
r = {instrument = DEFAULT_INSTRUMENT, note = 8, duration = 0.5}, -- D4
|
||
t = {instrument = DEFAULT_INSTRUMENT, note = 9, duration = 0.5}, -- D#4
|
||
y = {instrument = DEFAULT_INSTRUMENT, note = 10, duration = 0.5}, -- E4
|
||
u = {instrument = DEFAULT_INSTRUMENT, note = 11, duration = 0.5}, -- F4
|
||
i = {instrument = DEFAULT_INSTRUMENT, note = 12, duration = 0.5}, -- F#4
|
||
o = {instrument = DEFAULT_INSTRUMENT, note = 13, duration = 0.5}, -- G4
|
||
p = {instrument = DEFAULT_INSTRUMENT, note = 14, duration = 0.5}, -- G#4
|
||
leftBracket = {instrument = DEFAULT_INSTRUMENT, note = 15, duration = 0.5}, -- A4
|
||
rightBracket = {instrument = DEFAULT_INSTRUMENT, note = 16, duration = 0.5}, -- A#4
|
||
a = {instrument = DEFAULT_INSTRUMENT, note = 17, duration = 0.5}, -- B4
|
||
s = {instrument = DEFAULT_INSTRUMENT, note = 18, duration = 0.5}, -- C5
|
||
d = {instrument = DEFAULT_INSTRUMENT, note = 19, duration = 0.5}, -- C#5
|
||
f = {instrument = DEFAULT_INSTRUMENT, note = 20, duration = 0.5}, -- D5
|
||
g = {instrument = DEFAULT_INSTRUMENT, note = 21, duration = 0.5}, -- D#5
|
||
h = {instrument = DEFAULT_INSTRUMENT, note = 22, duration = 0.5}, -- E5
|
||
j = {instrument = DEFAULT_INSTRUMENT, note = 23, duration = 0.5}, -- F5
|
||
k = {instrument = DEFAULT_INSTRUMENT, note = 24, duration = 0.5} -- F#5
|
||
}
|
||
|
||
-- 内部状态 ----------------------------------------------------------------
|
||
local speakers = {} -- speaker 池: key = peripheralName -> {name=name, locked=false}
|
||
local timers = {} -- timers[id] = {speakerName=name}
|
||
local timerToSpeaker = {} -- 反向映射 timerId -> speakerName
|
||
|
||
-- helper: 刷新 speaker 列表(扫描当前连接的 peripheral)
|
||
local function refreshSpeakers()
|
||
local names = peripheral.getNames()
|
||
local newPool = {}
|
||
for _, name in ipairs(names) do
|
||
local t = peripheral.getType(name)
|
||
if t == "speaker" then
|
||
newPool[name] = { name = name, locked = false }
|
||
end
|
||
end
|
||
speakers = newPool
|
||
end
|
||
|
||
-- helper: 添加单个 speaker(attach 事件)
|
||
local function addSpeaker(name)
|
||
if peripheral.getType(name) == "speaker" then
|
||
speakers[name] = { name = name, locked = false }
|
||
print("[speaker] attached:", name)
|
||
end
|
||
end
|
||
|
||
-- helper: 移除单个 speaker(detach 事件)
|
||
local function removeSpeaker(name)
|
||
if speakers[name] then
|
||
speakers[name] = nil
|
||
print("[speaker] detached:", name)
|
||
end
|
||
end
|
||
|
||
local function countUnlocked()
|
||
local c = 0
|
||
for _, s in pairs(speakers) do
|
||
if not s.locked then c = c + 1 end
|
||
end
|
||
return c
|
||
end
|
||
|
||
-- 找到一个未锁定的 speaker 名称(返回 peripheralName 或 nil)
|
||
local function findFreeSpeaker()
|
||
for name, s in pairs(speakers) do
|
||
if not s.locked then
|
||
return name
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
-- 锁定 speaker 并发起播放(使用 peripheral.call)
|
||
local function lockAndPlay(speakerName, instrument, note, duration)
|
||
local s = speakers[speakerName]
|
||
if not s then
|
||
return false, "speaker not found"
|
||
end
|
||
if s.locked then
|
||
return false, "speaker already locked"
|
||
end
|
||
|
||
s.locked = true
|
||
-- 设置解锁计时器
|
||
local t = os.startTimer(duration or DEFAULT_NOTE_DURATION)
|
||
timers[t] = { speaker = speakerName }
|
||
timerToSpeaker[t] = speakerName
|
||
|
||
-- 异常保护:如果 playNote 调用失败则立即解锁并返回错误
|
||
local ok, err = pcall(peripheral.call, speakerName, "playNote", instrument, 3, note)
|
||
if not ok then
|
||
-- 立刻解锁并取消 timer 映射(timer 仍会到期,但我们删除映射以避免重复解锁)
|
||
s.locked = false
|
||
timers[t] = nil
|
||
timerToSpeaker[t] = nil
|
||
return false, err
|
||
end
|
||
|
||
return true
|
||
end
|
||
|
||
-- 解锁 speaker(timer 到期或其他情况)
|
||
local function unlockSpeaker(speakerName)
|
||
local s = speakers[speakerName]
|
||
if s then
|
||
s.locked = false
|
||
end
|
||
end
|
||
|
||
-- 在屏幕显示警告(短时间显示)
|
||
local function showWarning(msg, seconds)
|
||
seconds = seconds or 2
|
||
local w, h = term.getSize()
|
||
term.setCursorPos(1, h)
|
||
term.clearLine()
|
||
term.setTextColor(colors.red)
|
||
term.write("[WARNING] " .. msg)
|
||
term.setTextColor(colors.white)
|
||
-- 设一个计时器把警告清掉
|
||
os.startTimer(seconds)
|
||
-- 注意:这里不阻塞主循环,主循环会处理 timer 并清理最后一行(见主循环中 timer 处理)
|
||
end
|
||
|
||
-- 播放多个音(notes 是一组 {instrument, note, duration})
|
||
-- 返回 true/false, message
|
||
local function playNotes(notes)
|
||
if type(notes) ~= "table" then
|
||
return false, "notes must be a table"
|
||
end
|
||
local need = #notes
|
||
local unlocked = countUnlocked()
|
||
if need > unlocked then
|
||
showWarning(string.format("需要播放 %d 个音,但只有 %d 个可用扬声器,播放被拒绝。", need, unlocked))
|
||
return false, "not enough unlocked speakers"
|
||
end
|
||
|
||
-- 为每个音分配 speaker 并播放
|
||
local assigned = {}
|
||
for i, n in ipairs(notes) do
|
||
local freeName = findFreeSpeaker()
|
||
if not freeName then
|
||
-- 失败时尝试回滚已分配的(解锁)
|
||
for _, nm in ipairs(assigned) do unlockSpeaker(nm) end
|
||
return false, "no free speaker during assignment"
|
||
end
|
||
local duration = n.duration or DEFAULT_NOTE_DURATION
|
||
local ok, err = lockAndPlay(freeName, n.instrument or DEFAULT_INSTRUMENT, n.note, duration)
|
||
if not ok then
|
||
-- 回滚
|
||
for _, nm in ipairs(assigned) do unlockSpeaker(nm) end
|
||
return false, err
|
||
end
|
||
table.insert(assigned, freeName)
|
||
end
|
||
|
||
return true
|
||
end
|
||
|
||
-- 初始化:扫描 speakers
|
||
refreshSpeakers()
|
||
print("[info] found speakers:")
|
||
for name,_ in pairs(speakers) do print(" - " .. name) end
|
||
print("[info] ready. Press mapped keys to play notes. Ctrl+T to terminate (or stop the program).")
|
||
|
||
-- 主事件循环
|
||
while true do
|
||
local ev = { os.pullEventRaw() } -- 使用 pullEventRaw 防止 ctrl+t 自动关闭时丢失信息(按需修改)
|
||
local et = ev[1]
|
||
|
||
if et == "key" then
|
||
local keycode = ev[2]
|
||
local keyname = keys.getName(keycode)
|
||
if keyname then
|
||
-- 支持同时按多个键:这里简单做单键响应;若要和弦需要监听 "char"/其他并维护按下集合
|
||
local mapping = KEY_NOTE_MAP[keyname]
|
||
if mapping then
|
||
local ok, err = playNotes({ mapping })
|
||
if not ok then
|
||
print("[error] playNotes: " .. tostring(err))
|
||
end
|
||
end
|
||
end
|
||
|
||
elseif et == "timer" then
|
||
local timerId = ev[2]
|
||
-- 如果是警告计时器(没有在 timers 映射中)则清掉最后一行
|
||
if timers[timerId] then
|
||
local rec = timers[timerId]
|
||
local sname = rec.speaker
|
||
unlockSpeaker(sname)
|
||
timers[timerId] = nil
|
||
timerToSpeaker[timerId] = nil
|
||
-- 可选日志
|
||
-- print(string.format("[info] speaker %s unlocked (timer %d)", sname, timerId))
|
||
else
|
||
-- 可能是警告清除定时器:清掉最后一行
|
||
-- 检查屏幕高度并清除最后一行
|
||
local w, h = term.getSize()
|
||
term.setCursorPos(1, h)
|
||
term.clearLine()
|
||
end
|
||
|
||
elseif et == "peripheral" then
|
||
-- attach: ev = {"peripheral", sideName, peripheralType}
|
||
local sideName = ev[2]
|
||
local pType = ev[3]
|
||
if pType == "speaker" then
|
||
addSpeaker(sideName)
|
||
end
|
||
|
||
elseif et == "peripheral_detach" then
|
||
local sideName = ev[2]
|
||
removeSpeaker(sideName)
|
||
|
||
elseif et == "terminate" then
|
||
-- Ctrl+T
|
||
print("[info] terminated by user.")
|
||
break
|
||
else
|
||
-- 其它事件忽略或用于扩展(如鼠标、多键和弦检测等)
|
||
-- print("event", et)
|
||
end
|
||
end |