Files
CCT_piano_speaker_manager/piano_speaker_manager.lua
2026-01-28 03:48:19 +08:00

250 lines
9.3 KiB
Lua
Raw Permalink 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.

-- 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: 添加单个 speakerattach 事件)
local function addSpeaker(name)
if peripheral.getType(name) == "speaker" then
speakers[name] = { name = name, locked = false }
print("[speaker] attached:", name)
end
end
-- helper: 移除单个 speakerdetach 事件)
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
-- 解锁 speakertimer 到期或其他情况)
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