first commit
This commit is contained in:
commit
65430188aa
11 changed files with 2819 additions and 0 deletions
81
lua/src/.installer.lua
Normal file
81
lua/src/.installer.lua
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
local base_url = "https://files.sad.ovh/public/livestream-cc"
|
||||
local api_url = base_url .. "?ls"
|
||||
local download_root = "livestream-cc"
|
||||
|
||||
local function fetch_folder_list()
|
||||
local response = http.get(api_url)
|
||||
if not response then
|
||||
error("Failed to get folder list from Copyparty API")
|
||||
end
|
||||
local body = response.readAll()
|
||||
response.close()
|
||||
local data = textutils.unserializeJSON(body)
|
||||
return data
|
||||
end
|
||||
|
||||
local function download_file(path)
|
||||
local file_url = base_url .. "/" .. path
|
||||
local local_path = download_root .. "/" .. path
|
||||
local response = http.get(file_url)
|
||||
if response then
|
||||
local file = fs.open(local_path, "wb")
|
||||
if file then
|
||||
file.write(response.readAll())
|
||||
file.close()
|
||||
end
|
||||
response.close()
|
||||
else
|
||||
print("Failed to download: " .. file_url)
|
||||
end
|
||||
end
|
||||
|
||||
local function traverse_and_download(folder_data, prefix)
|
||||
prefix = prefix or ""
|
||||
|
||||
for _, file in ipairs(folder_data.files or {}) do
|
||||
print("Downloading: " .. prefix .. file.href)
|
||||
download_file(prefix .. file.href)
|
||||
end
|
||||
|
||||
for _, dir in ipairs(folder_data.dirs or {}) do
|
||||
fs.makeDir(download_root .. "/" .. prefix .. dir.href)
|
||||
local subdir_url = base_url .. "/" .. dir.href .. "?ls"
|
||||
local response = http.get(subdir_url)
|
||||
if response then
|
||||
local body = response.readAll()
|
||||
response.close()
|
||||
local subdir_data = textutils.unserializeJSON(body)
|
||||
traverse_and_download(subdir_data, prefix .. dir.href)
|
||||
else
|
||||
print("Failed to get subdirectory: " .. subdir_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if fs.exists(download_root) then
|
||||
if fs.exists(download_root .. "/version") then
|
||||
local previousVersion = fs.open(download_root .. "/version", "r").readAll()
|
||||
local currentVersion = http.get(base_url .. "/version").readAll();
|
||||
|
||||
if previousVersion == currentVersion then
|
||||
print("Previous version " .. previousVersion .. " is already installed, we're on " .. currentVersion .. " aswell, so skipping installation.")
|
||||
shell.run("livestream-cc/main.lua")
|
||||
return
|
||||
else
|
||||
print("Version " .. previousVersion .. " was already installed. Uninstalling.")
|
||||
fs.delete(download_root)
|
||||
end
|
||||
else
|
||||
print("Version marker does not exist. Cannot install.")
|
||||
shell.run("livestream-cc/main.lua")
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
fs.makeDir(download_root)
|
||||
|
||||
local folder_list = fetch_folder_list()
|
||||
traverse_and_download(folder_list, "")
|
||||
|
||||
print("Done :), installed livestream-cc version " .. (http.get(base_url .. "/version").readAll()))
|
||||
shell.run("livestream-cc/main.lua")
|
||||
87
lua/src/dcode.lua
Normal file
87
lua/src/dcode.lua
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
local char, byte, floor, band, rshift = string.char, string.byte, math.floor, bit32.band, bit32.arshift
|
||||
|
||||
|
||||
local PREC = 8
|
||||
local PREC_POW = 2 ^ PREC
|
||||
local PREC_POW_HALF = 2 ^ (PREC - 1)
|
||||
local STRENGTH_MIN = 2 ^ (PREC - 8 + 1)
|
||||
|
||||
local function make_predictor()
|
||||
local charge, strength, previous_bit = 0, 0, false
|
||||
|
||||
return function(current_bit)
|
||||
local target = current_bit and 127 or -128
|
||||
|
||||
local next_charge = charge + floor((strength * (target - charge) + PREC_POW_HALF) / PREC_POW)
|
||||
if next_charge == charge and next_charge ~= target then
|
||||
next_charge = next_charge + (current_bit and 1 or -1)
|
||||
end
|
||||
|
||||
local z = current_bit == previous_bit and PREC_POW - 1 or 0
|
||||
local next_strength = strength
|
||||
if next_strength ~= z then next_strength = next_strength + (current_bit == previous_bit and 1 or -1) end
|
||||
if next_strength < STRENGTH_MIN then next_strength = STRENGTH_MIN end
|
||||
|
||||
charge, strength, previous_bit = next_charge, next_strength, current_bit
|
||||
return charge
|
||||
end
|
||||
end
|
||||
|
||||
local function make_dec()
|
||||
local predictor = make_predictor()
|
||||
local low_pass_charge = 0
|
||||
local previous_charge, previous_bit = 0, false
|
||||
|
||||
return function (input)
|
||||
|
||||
local output, output_n = {}, 0
|
||||
for i = 1, #input do
|
||||
local input_byte = byte(input, i)
|
||||
for _ = 1, 8 do
|
||||
local current_bit = band(input_byte, 1) ~= 0
|
||||
local charge = predictor(current_bit)
|
||||
|
||||
local antijerk = charge
|
||||
if current_bit ~= previous_bit then
|
||||
antijerk = floor((charge + previous_charge + 1) / 2)
|
||||
end
|
||||
|
||||
previous_charge, previous_bit = charge, current_bit
|
||||
|
||||
low_pass_charge = low_pass_charge + floor(((antijerk - low_pass_charge) * 140 + 0x80) / 256)
|
||||
|
||||
output_n = output_n + 1
|
||||
output[output_n] = low_pass_charge
|
||||
|
||||
input_byte = rshift(input_byte, 1)
|
||||
end
|
||||
end
|
||||
|
||||
return output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function make_truebit_dec(max_amp)
|
||||
max_amp = max_amp or 127
|
||||
|
||||
return function (input)
|
||||
local output, output_n = {}, 0
|
||||
for i = 1, #input do
|
||||
local input_byte = byte(input, i)
|
||||
for _ = 1, 8 do
|
||||
local bit_is_1 = band(input_byte, 1) ~= 0
|
||||
output_n = output_n + 1
|
||||
output[output_n] = bit_is_1 and max_amp or -max_amp
|
||||
input_byte = rshift(input_byte, 1)
|
||||
end
|
||||
end
|
||||
return output
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
make_dec = make_dec,
|
||||
make_truebit_dec = make_truebit_dec
|
||||
}
|
||||
261
lua/src/main.lua
Normal file
261
lua/src/main.lua
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
local monitor = peripheral.find("monitor")
|
||||
local w, h = monitor.getSize()
|
||||
|
||||
local recent_logs = {
|
||||
["misc"] = {}
|
||||
}
|
||||
|
||||
function ui()
|
||||
monitor.clear()
|
||||
|
||||
local x = 1
|
||||
local y = 3
|
||||
|
||||
monitor.setTextScale(0.5)
|
||||
monitor.setCursorPos(x,1)
|
||||
monitor.write("Log")
|
||||
|
||||
|
||||
for k, v in pairs(recent_logs) do
|
||||
if k ~= "misc" then
|
||||
monitor.setCursorPos(x, y)
|
||||
monitor.write(k .. " -> " .. v.time)
|
||||
monitor.setCursorPos(x, y+1)
|
||||
monitor.write(v.log:sub(1, (w/2)-1))
|
||||
|
||||
y = y+3;
|
||||
|
||||
if y >= h then
|
||||
y = 3
|
||||
x = (w/2)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
monitor.setCursorPos(x,y)
|
||||
monitor.write("misc")
|
||||
y = y +1
|
||||
|
||||
for _, v in pairs(recent_logs.misc) do
|
||||
monitor.setCursorPos(x,y)
|
||||
monitor.write(v.log .. " " .. v.time)
|
||||
y = y + 1
|
||||
if y >= h then
|
||||
y = 3
|
||||
x = (w/2)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local dcode = require "dcode"
|
||||
-- Use the normal decoder for real audio; TrueBit is just for testing
|
||||
local make_decoder = dcode.make_truebit_dec -- or dcode.make_truebit_dec
|
||||
|
||||
local ws = http.websocket("ws://vps.sad.ovh:5821/ws")
|
||||
|
||||
-------------------------------------------------------
|
||||
-- metadata
|
||||
-------------------------------------------------------
|
||||
local raw = ws.receive()
|
||||
local meta = textutils.unserializeJSON(raw)
|
||||
print(("codec=%s rate=%dHz frame_ms=%d ch_enc=%d frame_bytes/chan=%d")
|
||||
:format(meta.codec, meta.sample_rate, meta.frame_ms, meta.channels_encoded, meta.frame_bytes))
|
||||
|
||||
---@return string[]
|
||||
--[[local function find_speakers()
|
||||
local names = peripheral.getNames()
|
||||
local found = {}
|
||||
for _, n in ipairs(names) do
|
||||
if peripheral.getType(n) == "speaker" then
|
||||
table.insert(found, n)
|
||||
if #found == 2 then break end
|
||||
end
|
||||
end
|
||||
return found
|
||||
end]]
|
||||
|
||||
--local speakers = find_speakers()
|
||||
--if #speakers == 0 then error("no speaker found") end
|
||||
local namesSpkrsL = {
|
||||
"speaker_756",
|
||||
"speaker_755",
|
||||
"speaker_754",
|
||||
}
|
||||
local namesSpkrsR = {
|
||||
"speaker_751",
|
||||
"speaker_752",
|
||||
"speaker_753",
|
||||
}
|
||||
|
||||
local spkrsL = {}
|
||||
local spkrsR = {}
|
||||
|
||||
for i=1,#namesSpkrsL do
|
||||
spkrsL[i] = peripheral.wrap(namesSpkrsL[i])
|
||||
spkrsR[i] = peripheral.wrap(namesSpkrsR[i])
|
||||
end
|
||||
|
||||
--print("SpeakerL: " .. (speakers[1]).. ", SpeakerR: " .. (speakers[2] or speakers[1] ))
|
||||
-------------------------------------------------------
|
||||
-- deque queue implementation (fast push/pop)
|
||||
-------------------------------------------------------
|
||||
local function make_queue() return { buf = {}, head = 1, tail = 0 } end
|
||||
local function q_len(q) return q.tail - q.head + 1 end
|
||||
local function q_push(q, v) q.tail = q.tail + 1; q.buf[q.tail] = v end
|
||||
local function q_pop(q) if q.head > q.tail then return nil end local v=q.buf[q.head]; q.buf[q.head]=nil; q.head=q.head+1; return v end
|
||||
local function q_trim_to(q, n) while q_len(q) > n do q_pop(q) end end
|
||||
|
||||
-------------------------------------------------------
|
||||
-- buffering policy
|
||||
-------------------------------------------------------
|
||||
local MIN_BUFFER, TARGET_BUFFER, MAX_BUFFER = 5, 10, 30
|
||||
|
||||
local frames = make_queue()
|
||||
|
||||
-------------------------------------------------------
|
||||
-- helper logging
|
||||
-------------------------------------------------------
|
||||
|
||||
local function log(event, extra)
|
||||
local t = textutils.formatTime(os.time(), true)
|
||||
|
||||
if extra == nil then
|
||||
table.insert(recent_logs.misc, {
|
||||
log = event,
|
||||
time = os.time()
|
||||
})
|
||||
if #recent_logs.misc > 5 then
|
||||
table.remove(recent_logs.misc, 1)
|
||||
end
|
||||
else
|
||||
recent_logs[event] = {
|
||||
log = extra,
|
||||
time = os.time()
|
||||
}
|
||||
end
|
||||
--ui()
|
||||
print(("[%s] %-12s | %s"):format(t, event, extra or ""))
|
||||
end
|
||||
-------------------------------------------------------
|
||||
-- receiver
|
||||
-------------------------------------------------------
|
||||
local function receiver()
|
||||
while true do
|
||||
local msg = ws.receive((meta.frame_ms / 1000) * 1.5)
|
||||
if msg then
|
||||
q_push(frames, msg)
|
||||
local n = q_len(frames)
|
||||
log("recv", "frames=" .. n)
|
||||
if n > MAX_BUFFER then
|
||||
log("drop", ("queue %d>%d trimming to %d"):format(n, MAX_BUFFER, TARGET_BUFFER))
|
||||
q_trim_to(frames, TARGET_BUFFER)
|
||||
end
|
||||
else
|
||||
log("ws underrun")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------
|
||||
-- playback
|
||||
-------------------------------------------------------
|
||||
local function player()
|
||||
local buffering = true
|
||||
local last_log = 0
|
||||
local decL, decR = make_decoder(), make_decoder()
|
||||
|
||||
while true do
|
||||
local queued = q_len(frames)
|
||||
|
||||
if buffering then
|
||||
if queued < MIN_BUFFER then
|
||||
if os.clock() - last_log > 0.5 then
|
||||
log("buffering", ("waiting %d/%d"):format(queued, MIN_BUFFER))
|
||||
last_log = os.clock()
|
||||
end
|
||||
os.sleep(meta.frame_ms / 1000 / 4)
|
||||
goto continue
|
||||
else
|
||||
log("start", ("starting playback with %d frames buffered"):format(queued))
|
||||
buffering = false
|
||||
end
|
||||
end
|
||||
local frame = q_pop(frames)
|
||||
if not frame then
|
||||
log("underrun", "no frame available — rebuffering")
|
||||
buffering = true
|
||||
else
|
||||
if meta.channels_encoded == 2 and #frame >= meta.frame_bytes * 6 then
|
||||
local B = meta.frame_bytes
|
||||
|
||||
-- Split frame into per-speaker byte chunks (L1,L2,L3,R1,R2,R3)
|
||||
local lBytes = {
|
||||
frame:sub(1, B),
|
||||
frame:sub(B + 1, 2 * B),
|
||||
frame:sub(2 * B + 1, 3 * B),
|
||||
}
|
||||
local rBytes = {
|
||||
frame:sub(3 * B + 1, 4 * B),
|
||||
frame:sub(4 * B + 1, 5 * B),
|
||||
frame:sub(5 * B + 1, 6 * B),
|
||||
}
|
||||
|
||||
-- Decode per speaker
|
||||
local lPcm = { decL(lBytes[1]), decL(lBytes[2]), decL(lBytes[3]) }
|
||||
local rPcm = { decR(rBytes[1]), decR(rBytes[2]), decR(rBytes[3]) }
|
||||
|
||||
for i = 1, 3 do spkrsL[i].playAudio(lPcm[i]) end
|
||||
for i = 1, 3 do spkrsR[i].playAudio(rPcm[i]) end
|
||||
|
||||
local done = {}
|
||||
for i = 1, 3 do done[namesSpkrsL[i]] = false; done[namesSpkrsR[i]] = false end
|
||||
|
||||
while true do
|
||||
local allDone = true
|
||||
for _, v in pairs(done) do if not v then allDone = false break end end
|
||||
if allDone then
|
||||
print("all speakers are done")
|
||||
break end
|
||||
|
||||
local _, speaker = os.pullEvent("speaker_audio_empty")
|
||||
print("speaker: ".. speaker .. " is done")
|
||||
done[speaker] = true
|
||||
end
|
||||
else
|
||||
-- mono stream or short packet: decode and play sequentially
|
||||
local pcm = decL(frame)
|
||||
if pcm then
|
||||
local lDone = (not spkL)
|
||||
local rDone = (not spkR)
|
||||
while not (lDone and rDone) do
|
||||
if not lDone then
|
||||
lDone = spkL.playAudio(pcm)
|
||||
end
|
||||
if not rDone then
|
||||
rDone = spkR.playAudio(pcm)
|
||||
end
|
||||
if not (lDone and rDone) then
|
||||
os.pullEvent("speaker_audio_empty")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Pacing is governed by speaker_audio_empty waits above.
|
||||
local n = q_len(frames)
|
||||
if n < 1 then
|
||||
log("underrun", "ran out after frame — rebuffering")
|
||||
buffering = true
|
||||
elseif n < MIN_BUFFER and os.clock() - last_log > 0.5 then
|
||||
log("lowbuf", ("frames=%d < %d"):format(n, MIN_BUFFER))
|
||||
last_log = os.clock()
|
||||
end
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
log("init", "waiting for frames…")
|
||||
parallel.waitForAny(receiver, player)
|
||||
1
lua/src/version
Normal file
1
lua/src/version
Normal file
|
|
@ -0,0 +1 @@
|
|||
800
|
||||
Loading…
Add table
Add a link
Reference in a new issue