Skip to content

Instantly share code, notes, and snippets.

@michielp1807
Last active September 29, 2024 15:34
Show Gist options
  • Save michielp1807/9536445bd5773915a58594869049d2ed to your computer and use it in GitHub Desktop.
Save michielp1807/9536445bd5773915a58594869049d2ed to your computer and use it in GitHub Desktop.
WaveFlow audio visualizer
-- WaveFlow audio visualizer
--
-- Usage:
-- - run in background with `bg waveflow`
-- - or run in background on monitor with `bg monitor left waveflow`
-- - then use another program to play audio (e.g. with a DFPWM audio player or MeowSynth)
-- - click to change the colors!
--
-- This program will detect any audio played on speakers via playAudio
-- Unfortunately, in-game it lags everytime a new audio buffer is queued...
-- But it works fine on CraftOS-PC :D
local only_use_speaker = nil -- set to speaker name if you only want to use audio data from a specific speaker
local function downloadFile(filename, url)
local res = http.get(url)
if not res then error("Error downloading file from " .. url) end
local data = res.readAll()
res.close()
local file = fs.open(filename, "w")
if not file then error("Can't write to file " .. filename .. "...") end
file.write(data)
file.close()
end
-- BetterBetterBlittle (takes color spaces into account when picking colors)
if not fs.exists("betterbetterblittle.lua") then
downloadFile("betterbetterblittle.lua",
"https://raw.githubusercontent.com/Xella37/Pine3D/bf70ce8dfd293c5cfc795183cf1ba8a6d230be73/betterblittle.lua")
end
local betterblittle = require("betterbetterblittle")
local LAST_COLOR = 2 ^ 15
local gradients = {
{0xf3ee5e, 0xe6ca58, 0xd9a653, 0xcc824e, 0xbf5d48, 0xb14242, 0x9f3b3b, 0x8e3535, 0x7d2f2f, 0x6c2828, 0x5a2222, 0x491c1c, 0x371515, 0x260e0e, 0x140808, 0x040101},
{0x54d2f3, 0x5eb3e8, 0x6995de, 0x7275d3, 0x7d56c8, 0x813fbc, 0x7439a9, 0x673296, 0x5a2c84, 0x4e2772, 0x41205f, 0x351a4d, 0x28143b, 0x1b0d28, 0x0f0716, 0x020103},
{0xf45256, 0xec4f6c, 0xe34c82, 0xdc4a98, 0xd448ae, 0xc844b8, 0xb43da5, 0xa13693, 0x8d3082, 0x79296f, 0x66235e, 0x521c4c, 0x3e1539, 0x2b0f28, 0x180816, 0x040103},
{0x79f256, 0x6de66c, 0x61d983, 0x55cd98, 0x4ac0af, 0x40b2b9, 0x39a0a7, 0x338f95, 0x2c7d82, 0x276c70, 0x205a5e, 0x1a4a4c, 0x14383a, 0x0e2628, 0x071415, 0x010304}
}
local gradientI = 1
-- each sample is an integer between -128 and 127
local samplerate = 48 -- 48 samples per ms (48kHz)
local buffersPlayed = 0
local buffers = {}
local MAX_BUFFERS = 3 -- maximum number of buffers to keep in memory
local bufferI = 0
local newestBufferEndTime = 0 -- in ms, aka nextBufferStartTime
local oldPeripheralCall = peripheral.call
function peripheral.call(name, method, ...)
if method == "playAudio" then
if not oldPeripheralCall(name, method, ...) then return false end
if not only_use_speaker or only_use_speaker == name then
local newBuffer = ({...})[1]
local volume = ({...})[2]
local bufferDuration = #newBuffer / samplerate
local time = os.epoch("utc")
if time - newestBufferEndTime > 10 then
newestBufferEndTime = time + bufferDuration
buffersPlayed = 0
buffers = {}
else
newestBufferEndTime = newestBufferEndTime + bufferDuration
end
buffersPlayed = buffersPlayed + 1
bufferI = bufferI % MAX_BUFFERS + 1
buffers[bufferI] = newBuffer
end
return true -- success
else
return oldPeripheralCall(name, method, ...)
end
end
local SCREEN_WIDTH, SCREEN_HEIGHT = term.getSize()
local FB_WIDTH, FB_HEIGHT = SCREEN_WIDTH * 2, SCREEN_HEIGHT * 3
local window = window.create(term.current(), 1, 1, SCREEN_WIDTH, SCREEN_HEIGHT)
local frameBuffer = {}
local function setColors()
local gradient = gradients[gradientI]
for i = 1, #gradient do
window.setPaletteColor(2 ^ (i - 1), gradient[i])
end
end
setColors()
local function userInput()
while true do
local event, which, x, y = os.pullEventRaw()
if event == "mouse_click" then
gradientI = gradientI % #gradients + 1
setColors()
elseif event == "terminate" then
peripheral.call = oldPeripheralCall
return
end
end
end
local function gameLoop()
local lastTime = os.epoch("utc")
local lastI = -1
local max, min, floor, random = math.max, math.min, math.floor, math.random
while true do
local time = os.epoch("utc")
local dt = time - lastTime
if dt >= 10 then -- limit to 100 fps
lastTime = time
local buffer, sampleIndex = {}, 1
local sampleOffset = floor((newestBufferEndTime - time) * samplerate)
if sampleOffset >= 0 then
buffer = buffers[bufferI] or {}
sampleIndex = #buffer - sampleOffset
if sampleIndex < 0 then
buffer = buffers[(bufferI - 2) % MAX_BUFFERS + 1] or {}
sampleIndex = sampleIndex + #buffer
end
end
local fadeFactor = random() < dt / 20 and 2 or 1
local fadeFactor2 = random() < dt / 50 and 2 or 1
for y = 1, FB_HEIGHT do
local row = frameBuffer[y] or {}
for x = 1, FB_WIDTH do
local color = row[x] or LAST_COLOR
row[x] = max(min((color) * (color > 128 and fadeFactor2 or fadeFactor), LAST_COLOR), 2)
end
frameBuffer[y] = row
end
local scale = 4
for x = 1, FB_WIDTH do
local sample = 0
if sampleOffset >= 0 then
sample = buffer[sampleIndex - scale * (x - 1)]
if not sample then
buffer = buffers[bufferI % MAX_BUFFERS + 1] or {}
sample = buffer[sampleIndex + x] or 0
end
end
local y = floor((sample + 128) / 256 * (FB_HEIGHT - 2) + 1)
if y > 0 and y < FB_HEIGHT then
local row = frameBuffer[y]
row[FB_WIDTH - x + 1] = 1
frameBuffer[y] = row
end
end
betterblittle.drawBuffer(frameBuffer, window)
-- window.setTextColor(2)
-- window.setCursorPos(1, 1)
-- window.write("" .. sampleOffset .. " " .. sampleIndex)
window.setVisible(true)
window.setVisible(false)
end
os.queueEvent("gameLoop")
---@diagnostic disable-next-line: param-type-mismatch
os.pullEventRaw("gameLoop")
end
end
parallel.waitForAny(userInput, gameLoop)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment