-- 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