From 65430188aa8970e526b4293119156634ec86c43a Mon Sep 17 00:00:00 2001 From: yourfriendoss Date: Tue, 11 Nov 2025 11:29:38 +0200 Subject: [PATCH] first commit --- .gitignore | 3 + .luarc.json | 6 + README.md | 7 + lua/src/.installer.lua | 81 ++ lua/src/dcode.lua | 87 ++ lua/src/main.lua | 261 ++++++ lua/src/version | 1 + rust/.gitignore | 1 + rust/Cargo.lock | 1799 ++++++++++++++++++++++++++++++++++++++++ rust/Cargo.toml | 11 + rust/src/main.rs | 562 +++++++++++++ 11 files changed, 2819 insertions(+) create mode 100644 .gitignore create mode 100644 .luarc.json create mode 100644 README.md create mode 100644 lua/src/.installer.lua create mode 100644 lua/src/dcode.lua create mode 100644 lua/src/main.lua create mode 100644 lua/src/version create mode 100644 rust/.gitignore create mode 100644 rust/Cargo.lock create mode 100644 rust/Cargo.toml create mode 100644 rust/src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e23e4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +push.sh +u2c.py diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..296d35a --- /dev/null +++ b/.luarc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", + "runtime.version": "Lua 5.2", + "format.enable": true, + "workspace.library": ["~/lua-ls-cc-tweaked/library"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..816f99e --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# livestream-cc + +## in development audio player from pipewire to cc + +pfowrd -> ssh -R 0.0.0.0:5821:localhost:8080 vps +lua -> wget run https://files.sad.ovh/public/livestream-cc/.installer.lua +rust -> cargo run -- --device pipewire --bind '127.0.0.1:8080' --frame-ms 300 --sample-rate 48000 --stereo diff --git a/lua/src/.installer.lua b/lua/src/.installer.lua new file mode 100644 index 0000000..31444f3 --- /dev/null +++ b/lua/src/.installer.lua @@ -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") diff --git a/lua/src/dcode.lua b/lua/src/dcode.lua new file mode 100644 index 0000000..6e19a09 --- /dev/null +++ b/lua/src/dcode.lua @@ -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 +} diff --git a/lua/src/main.lua b/lua/src/main.lua new file mode 100644 index 0000000..796bbc6 --- /dev/null +++ b/lua/src/main.lua @@ -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) diff --git a/lua/src/version b/lua/src/version new file mode 100644 index 0000000..5ae5aef --- /dev/null +++ b/lua/src/version @@ -0,0 +1 @@ +800 diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..26bc38e --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,1799 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.10.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "base64", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rust" +version = "0.1.0" +dependencies = [ + "axum", + "clap", + "cpal", + "serde", + "tokio", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..ad483e4 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rust" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = { version = "0.7", features = ["ws"] } +tokio = { version = "1", features = ["full"] } +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +cpal = "0.15" diff --git a/rust/src/main.rs b/rust/src/main.rs new file mode 100644 index 0000000..234d5c9 --- /dev/null +++ b/rust/src/main.rs @@ -0,0 +1,562 @@ +use std::{ + fmt::Write as _, + sync::Arc, +}; + +use axum::{ + extract::{ + ws::{Message, WebSocket, WebSocketUpgrade}, + State, + }, + response::IntoResponse, + routing::get, + Router, +}; +use clap::Parser; +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + Device, Sample, SampleFormat, SampleRate, Stream, StreamConfig, +}; +use tokio::sync::broadcast; + +#[derive(Parser, Debug)] +#[command(name = "livestream-cc", version, about = "Capture audio (PipeWire/CPAL) -> DFPWM -> WebSocket broadcast")] +struct Args { + // Substring to match an input device by name. If omitted, uses the default input device. + #[arg(long)] + device: Option, + // Bind address for the WebSocket server. + #[arg(long, default_value = "127.0.0.1:8080")] + bind: String, + // Frame duration in milliseconds (must result in samples divisible by 8). + #[arg(long, default_value_t = 20)] + frame_ms: u32, + // Attempt to use this sample rate. Falls back to the device default if not supported. + #[arg(long)] + sample_rate: Option, + // List input devices and exit. + #[arg(long)] + list_devices: bool, + // NEW: request stereo (if device has ≥2 channels; mono dup if 1 ch) + #[arg(long)] stereo: bool, +} + +struct AppState { + tx: broadcast::Sender>, + meta: StreamMeta, +} + + +#[derive(Clone)] +struct StreamMeta { + codec: &'static str, + const_prec: i32, + frame_ms: u32, + frame_samples: usize, + frame_bytes: usize, // DFPWM bytes per channel + sample_rate: u32, + channels_source: u16, // input channels from device + channels_encoded: u16, // 1 or 2 +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + let host = cpal::default_host(); + + if args.list_devices { + list_input_devices(&host); + return; + } + + // Pick device + let device = match pick_input_device(&host, args.device.as_deref()) { + Ok(dev) => dev, + Err(e) => { + eprintln!("Failed to get input device: {e}"); + return; + } + }; + + println!("Using input device: {}", device.name().unwrap_or_else(|_| "".into())); + + // Pick config + let (mut config, sample_format) = match pick_stream_config(&device, args.sample_rate) { + Ok(cfg) => cfg, + Err(e) => { + eprintln!("Failed to get stream config: {e}"); + return; + } + }; + + if args.stereo { + config.channels = 2; + } + + println!( + "Device reports: {} Hz, {} channels, format {:?}", + config.sample_rate.0, config.channels, sample_format + ); + + let sr = config.sample_rate.0; + let ch = config.channels; + let enc_ch = if args.stereo { 2 } else { 1 }; + + + // Determine per-frame sample count (ensure multiple of 8) + let mut frame_samples = + ((sr as u64) * (args.frame_ms as u64) / 1000) as usize; + if frame_samples < 8 { + frame_samples = 8; + } + frame_samples -= frame_samples % 8; + let frame_bytes = frame_samples / 8; + + let meta = StreamMeta { + codec: "dfpwm-1a", + const_prec: DfpwmEncoder::CONST_PREC, + frame_ms: args.frame_ms, + frame_samples, + frame_bytes, // per channel! + sample_rate: sr, + channels_source: ch, + channels_encoded: enc_ch, + }; + + + println!( + "Configured: {} Hz, {}ch (downmix->mono), frame {} ms -> {} samples -> {} dfpwm bytes, CONST_PREC={}", + meta.sample_rate, + meta.channels_source, + meta.frame_ms, + meta.frame_samples, + meta.frame_bytes, + meta.const_prec + ); + + // Broadcast channel for DFPWM frames + let (tx, _rx) = broadcast::channel::>(128); + let state = Arc::new(AppState { tx: tx.clone(), meta: meta.clone() }); + + // Build and start CPAL stream + let stream = match build_cpal_stream( + &device, + &config, + sample_format, + frame_samples, + tx.clone(), + enc_ch + ) { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to build input stream: {e}"); + return; + } + }; + stream.play().expect("failed to start input stream"); + + // Axum WebSocket server + let app = Router::new() + .route("/ws", get(ws_handler)) + .with_state(state); + + println!("WebSocket server listening on ws://{}/ws", args.bind); + + let listener = match tokio::net::TcpListener::bind(&args.bind).await { + Ok(l) => l, + Err(e) => { + eprintln!("Failed to bind {}: {e}", args.bind); + return; + } + }; + + if let Err(e) = axum::serve(listener, app).await { + eprintln!("Server error: {e}"); + } + + // Keep the stream alive + drop(stream); +} + +async fn ws_handler(ws: WebSocketUpgrade, State(state): State>) -> impl IntoResponse { + ws.on_upgrade(move |socket| ws_on_upgraded(socket, state)) +} + +async fn ws_on_upgraded(mut socket: WebSocket, state: Arc) { + // Send a small textual metadata message first. + if let Err(e) = socket + .send(Message::Text(build_meta_text(&state.meta))) + .await + { + eprintln!("WS send meta failed: {e}"); + return; + } + + let mut rx = state.tx.subscribe(); + loop { + match rx.recv().await { + Ok(block) => { + if socket.send(Message::Binary(block)).await.is_err() { + break; + } + } + Err(broadcast::error::RecvError::Lagged(n)) => { + eprintln!("WS receiver lagged by {n} frames, dropping old frames"); + continue; + } + Err(_) => break, + } + } +} + +fn build_meta_text(m: &StreamMeta) -> String { + let mut s = String::new(); + let _ = write!( + s, + "{{\"type\":\"meta\",\"codec\":\"{}\",\"const_prec\":{},\"frame_ms\":{},\"frame_samples\":{},\"frame_bytes\":{},\"sample_rate\":{},\"channels_source\":{},\"channels_encoded\":{}}}", + m.codec, m.const_prec, m.frame_ms, m.frame_samples, m.frame_bytes, + m.sample_rate, m.channels_source, m.channels_encoded + ); + s +} + +fn list_input_devices(host: &cpal::Host) { + println!("Input devices:"); + match host.input_devices() { + Ok(devs) => { + for (i, d) in devs.enumerate() { + let name = d.name().unwrap_or_else(|_| "".into()); + println!(" [{i}] {name}"); + } + } + Err(e) => eprintln!(" Failed to enumerate input devices: {e}"), + } +} + +/// Pick an input device, matching by substring if provided; else default input device. +fn pick_input_device(host: &cpal::Host, name_substr: Option<&str>) -> Result { + if let Some(sub) = name_substr { + let sub_l = sub.to_lowercase(); + let mut found: Option = None; + for d in host.input_devices().map_err(|e| e.to_string())? { + let name = d.name().unwrap_or_else(|_| "".into()); + if name.to_lowercase().contains(&sub_l) { + found = Some(d); + break; + } + } + if let Some(d) = found { + Ok(d) + } else { + Err(format!("No input device matching substring: {sub}")) + } + } else { + host.default_input_device().ok_or_else(|| "No default input device".into()) + } +} + +/// Pick a usable stream config, preferring the requested sample rate if possible. +fn pick_stream_config( + device: &Device, + target_sample_rate: Option, +) -> Result<(StreamConfig, SampleFormat), String> { + // Try supported configs first so we can request a specific sample rate. + if let Ok(mut supported) = device.supported_input_configs() { + let mut candidates = Vec::new(); + for cfg in supported { + candidates.push(cfg); + } + // Try to find exact sample rate match + if let Some(sr) = target_sample_rate { + // Most devices expose ranges; pick one that contains the sample rate. + for supp in &candidates { + if supp.min_sample_rate().0 <= sr && sr <= supp.max_sample_rate().0 { + let sf = supp.sample_format(); + let cfg = StreamConfig { + channels: supp.channels(), + sample_rate: SampleRate(sr), + buffer_size: cpal::BufferSize::Default, + }; + return Ok((cfg, sf)); + } + } + eprintln!("Target sample rate {sr} not in any supported range; falling back to default input config"); + } + } + + // Fall back to device default config + let default_cfg = device.default_input_config().map_err(|e| e.to_string())?; + let sf = default_cfg.sample_format(); + Ok((default_cfg.config(), sf)) +} + +/// Build and return a CPAL input stream that performs: +/// - downmix to mono +/// - convert to i8 PCM [-128,127] +/// - DFPWM encode in frames +/// - broadcast frames over a channel +fn build_cpal_stream( + device: &Device, + cfg: &StreamConfig, + sample_format: SampleFormat, + frame_samples: usize, + tx: broadcast::Sender>, + enc_ch: u16, +) -> Result { + match sample_format { + SampleFormat::F32 => build_stream_f32(device, cfg, frame_samples, tx), + SampleFormat::I16 => build_stream_i16(device, cfg, frame_samples, tx), + SampleFormat::U16 => build_stream_u16(device, cfg, frame_samples, tx), + SampleFormat::U8 => build_stream_u8(device, cfg, frame_samples, tx, enc_ch), + other => Err(format!("Unsupported sample format: {other:?}")), + } +} + +fn build_stream_f32( + device: &Device, + cfg: &StreamConfig, + frame_samples: usize, + tx: broadcast::Sender>, +) -> Result { + let channels = cfg.channels as usize; + + let mut encoder = DfpwmEncoder::new(); + let mut acc: Vec = Vec::with_capacity(frame_samples * 2); + + let mut process_buffer = move |data: &[f32]| { + // Downmix to mono and convert to i8 + for frame in data.chunks(channels) { + let mut sum = 0.0f32; + for &s in frame { + sum += s; + } + let f = sum / channels as f32; + let i = f32_to_i8(f); + acc.push(i); + } + + // Encode full frames + while acc.len() >= frame_samples { + let frame: Vec = acc.drain(..frame_samples).collect(); + let encoded = encoder.encode(&frame); + let _ = tx.send(encoded); + } + }; + + let err_fn = |err: cpal::StreamError| { + eprintln!("Stream error: {err}"); + }; + + device + .build_input_stream(cfg, move |data: &[f32], _| process_buffer(data), err_fn, None) + .map_err(|e| e.to_string()) +} + +fn build_stream_i16( + device: &Device, + cfg: &StreamConfig, + frame_samples: usize, + tx: broadcast::Sender>, +) -> Result { + let channels = cfg.channels as usize; + + let mut encoder = DfpwmEncoder::new(); + let mut acc: Vec = Vec::with_capacity(frame_samples * 2); + + let mut process_buffer = move |data: &[i16]| { + for frame in data.chunks(channels) { + let mut sum = 0.0f32; + for &s in frame { + // Map i16 [-32768, 32767] -> f32 [-1.0, 1.0) + sum += (s as f32) / 32768.0; + } + let f = sum / channels as f32; + let i = f32_to_i8(f); + acc.push(i); + } + + while acc.len() >= frame_samples { + let frame: Vec = acc.drain(..frame_samples).collect(); + let encoded = encoder.encode(&frame); + let _ = tx.send(encoded); + } + }; + + let err_fn = |err: cpal::StreamError| { + eprintln!("Stream error: {err}"); + }; + + device + .build_input_stream(cfg, move |data: &[i16], _| process_buffer(data), err_fn, None) + .map_err(|e| e.to_string()) +} +fn build_stream_u8( + device: &Device, + cfg: &StreamConfig, + frame_samples: usize, + tx: broadcast::Sender>, + enc_ch: u16, // 1 or 2 +) -> Result { + let channels = cfg.channels as usize; + let mut accL: Vec = Vec::with_capacity(frame_samples * 2); + let mut accR: Vec = Vec::with_capacity(frame_samples * 2); + let mut enc = DfpwmEncoder::new(); + + let mut process_buffer = move |data: &[u8]| { + for frame in data.chunks(channels.max(1)) { + // Convert to f32 [-1,1) + let s0 = ((frame.get(0).copied().unwrap_or(128) as f32) - 128.0) / 128.0; + let left_i8 = f32_to_i8(s0); + + let right_i8 = if enc_ch == 2 { + let s1 = if channels >= 2 { + ((frame[1] as f32) - 128.0) / 128.0 + } else { + s0 // mono dup to right + }; + f32_to_i8(s1) + } else { + left_i8 // mono encoder + }; + accL.push(left_i8); + if enc_ch == 2 { accR.push(right_i8); } + } + + while accL.len() >= frame_samples && (enc_ch == 1 || accR.len() >= frame_samples) { + let frameL: Vec = accL.drain(..frame_samples).collect(); + + if enc_ch == 2 { + let frameR: Vec = accR.drain(..frame_samples).collect(); + + let L = enc.encode(&frameL); + let R = enc.encode(&frameR); + + // LR concatenation: [left][right] + let mut packet = Vec::with_capacity(L.len() * 3 + R.len() * 3); + packet.extend_from_slice(&L); + packet.extend_from_slice(&L); + packet.extend_from_slice(&L); + + packet.extend_from_slice(&R); + packet.extend_from_slice(&R); + packet.extend_from_slice(&R); + + let _ = tx.send(packet); + } else { + let outL = enc.encode(&frameL); + + let _ = tx.send(outL); + } + } + }; + + let err_fn = |err: cpal::StreamError| eprintln!("Stream error: {err}"); + + device + .build_input_stream(cfg, move |data: &[u8], _| process_buffer(data), err_fn, None) + .map_err(|e| e.to_string()) +} + +fn build_stream_u16( + device: &Device, + cfg: &StreamConfig, + frame_samples: usize, + tx: broadcast::Sender>, +) -> Result { + let channels = cfg.channels as usize; + + let mut encoder = DfpwmEncoder::new(); + let mut acc: Vec = Vec::with_capacity(frame_samples * 2); + + let mut process_buffer = move |data: &[u16]| { + for frame in data.chunks(channels) { + let mut sum = 0.0f32; + for &s in frame { + // Map u16 [0, 65535] -> f32 [-1.0, 1.0) + sum += ((s as f32) - 32768.0) / 32768.0; + } + let f = sum / channels as f32; + let i = f32_to_i8(f); + acc.push(i); + } + + while acc.len() >= frame_samples { + let frame: Vec = acc.drain(..frame_samples).collect(); + let encoded = encoder.encode(&frame); + let _ = tx.send(encoded); + } + }; + + let err_fn = |err: cpal::StreamError| { + eprintln!("Stream error: {err}"); + }; + + device + .build_input_stream(cfg, move |data: &[u16], _| process_buffer(data), err_fn, None) + .map_err(|e| e.to_string()) +} + +/// Convert f32 [-1.0, 1.0] to i8 [-128, 127] with clamping and rounding. +fn f32_to_i8(x: f32) -> i8 { + // Scale so -1.0 -> -128, 1.0 -> 127, then clamp. + let v = (x * 128.0).round() as i32; + v.clamp(-128, 127) as i8 +} + +struct DfpwmEncoder { + q: i32, + s: i32, + lt: i32, +} + +impl DfpwmEncoder { + const CONST_PREC: i32 = 10; + + fn new() -> Self { + Self { q: 0, s: 0, lt: -128 } + } + + fn encode(&mut self, input: &[i8]) -> Vec { + assert!( + input.len() % 8 == 0, + "DFPWM encode expects input length multiple of 8" + ); + + let mut out = Vec::with_capacity(input.len() / 8); + + for chunk in input.chunks(8) { + let mut d: u8 = 0; + for (bit, &v_i8) in chunk.iter().enumerate() { + let v = v_i8 as i32; + let t = if v < self.q || v == -128 { -128 } else { 127 }; + + if t > 0 { + d |= 1 << bit; + } + + let mut nq = self.q + ((self.s * (t - self.q) + (1 << (Self::CONST_PREC - 1))) >> Self::CONST_PREC); + if nq == self.q && nq != t { + nq += if t == 127 { 1 } else { -1 }; + } + self.q = nq; + + let st = if t != self.lt { 0 } else { (1 << Self::CONST_PREC) - 1 }; + let mut ns = self.s; + if ns != st { + ns += if st != 0 { 1 } else { -1 }; + } + if Self::CONST_PREC > 8 { + let min = 1 + (1 << (Self::CONST_PREC - 8)); + if ns < min { ns = min; } + } + self.s = ns; + self.lt = t; + } + out.push(d); + } + + + out + } +}