initialize

This commit is contained in:
Andrew-71 2024-03-10 23:45:44 +03:00
commit 5b23ea99fd
43 changed files with 2336 additions and 0 deletions

View file

@ -0,0 +1,95 @@
local graphics = require("graphics")
local modem = peripheral.find('modem')
if not modem then error('No modem found') end
modem.open(7101)
local dfpwm = require("cc.audio.dfpwm")
local speaker = peripheral.find("speaker")
if not speaker then error('No speaker found') end
local decoder = dfpwm.make_decoder()
local status_vars = {
is_playing = true,
is_paused = false,
current_second = 0,
track_index = 1,
start_index = 1,
}
local cfg_vars = {
forward_skip = 5,
backward_skip = 5,
port = 7101,
chunk_size = 0.5
}
local function get_catalogue()
modem.transmit(7101, 7101, {type = 'query'})
local event, sides, channel, replyChannel, message, distance = os.pullEvent('modem_message')
return message.audio_catalogue
end
local audio_catalogue = get_catalogue()
local function music_loop()
local play_music = require("playback")
while true do
status_vars.current_second = play_music(status_vars, cfg_vars, audio_catalogue, modem)
sleep()
end
end
local function draw_ui_loop()
while true do
if not status_vars.is_playing then
graphics.draw_track_list(1, 1, audio_catalogue, status_vars.start_index, select(2, term.getSize()) - 1, term)
else
graphics.draw_track_list(1, 1, audio_catalogue, status_vars.start_index, select(2, term.getSize()) - 4, term)
end
sleep(1)
end
end
local function control_loop()
while true do
local event, button, x, y = os.pullEvent("mouse_click")
local btn_actions = {
pause = {
x = x + math.floor((select(1, term.getSize()) - 11) / 2) + 5,
y = select(2, term.getSize()) - 1,
func = function ()
status_vars.is_paused = not status_vars.is_paused
end
},
forward = {
x = math.floor((select(1, term.getSize()) - 11) / 2) + 9,
y = select(2, term.getSize()) - 1,
func = function ()
status_vars.current_second = status_vars.current_second + cfg_vars.forward_skip
end
},
backward = {
x = math.floor((select(1, term.getSize()) - 11) / 2) + 1,
y = select(2, term.getSize()) - 1,
func = function ()
status_vars.current_second = status_vars.current_second - cfg_vars.backward_skip
end
}
}
if x == 3 and y == 1 then return end
term.setCursorPos(btn_actions.pause.x, btn_actions.pause.y)
term.write('%')
for i, v in pairs(btn_actions) do
if x == v.x and y == v.y then
v.func()
break
end
end
end
end
term.clear() -- Clear terminal
parallel.waitForAny(music_loop, draw_ui_loop, control_loop) -- Start loops

View file

@ -0,0 +1,167 @@
local api = {}
api.symbols = {
audio_controls = {
play = "\16",
pause = "\19",
forward = "\16",
backward = "\17",
full_back = "\171"
},
app_controls = {
up = "\30",
down = "\31",
download = "\25",
play = "\16"
},
misc = {
music = "\15",
}
}
-- Draw a spinner loop at a specified position. Useful for waitForAny.
function api.spinner_loop(x, y, spin_monitor)
local spin_chars = {
"\139",
"\138",
"\142",
"\140",
"\141",
"\133",
"\135",
"\131"
}
local index = 1
while true do
spin_monitor.setCursorPos(x, y)
spin_monitor.write(spin_chars[index])
index = index + 1
if index > #spin_chars then index = index - #spin_chars end
sleep(0.1)
end
end
--[[
Function to create simple progress bars of any length.
size - length of the bar in pixels, taken, max - ratio of the bar that should be full.
There is an ability to set custom symbols for full and empty pixels,
however it is not necessary and by default the program uses '#' and '.' for these
]]
function api.make_progressbar(size, taken, max, char_full, char_empty)
--[[
Maths behind the function are quite simple:
taken/max = x/size
taken * size = x * max
x = taken * size / max
]]
local full_symbol, empty_symbol = char_full or '#', char_empty or '.'
size = size - 2 -- make space for "[" and "]"
local progress = math.floor(size * taken / max)
local hash = (full_symbol):rep(progress)
local dots = (empty_symbol):rep(size - progress)
return ("[" .. hash .. dots .. "]")
end
local function seconds_to_string(seconds)
-- int to mm:ss
local minutes = math.floor(seconds / 60)
local seconds = seconds - minutes * 60
return string.format("%02d:%02d", minutes, seconds)
end
function api.draw_audio_bar(x, y, progress_seconds, track_info, is_paused, monitor)
--[[
Example:
| Among us soundtrack lol |
| [<] [####........[<]............] [>] |
| 12:43/14:31 |
]]
local x_max, y_max = monitor.getSize()
-- Title
monitor.setCursorPos(x, y)
local title = track_info.title
if #title > x_max - 2 then
title = title:sub(1, x_max - 4) .. ".."
end
monitor.write((' '):rep(math.floor((x_max - #title) / 2)) .. title .. (' '):rep(math.ceil((x_max - #title) / 2)))
-- Progress bar
monitor.setCursorPos(x, y + 1)
local progress_bar = api.make_progressbar(x_max - 16, progress_seconds, track_info.length)
--monitor.blit(' ' .. seconds_to_string(progress_seconds) .. ' ' .. progress_bar .. ' ' .. seconds_to_string(track_info.length), 'f00000f0' .. ('3'):rep(x_max - 16) .. '0f00000f', ('f'):rep(x_max))
monitor.write(' ' .. seconds_to_string(progress_seconds) .. ' ' .. progress_bar .. ' ' .. seconds_to_string(track_info.length))
-- Controls
local controls = {
api.symbols.audio_controls.backward,
is_paused and api.symbols.audio_controls.play or api.symbols.audio_controls.pause,
api.symbols.audio_controls.forward
}
for i, control in ipairs(controls) do
monitor.setCursorPos(x + math.floor((x_max - 11) / 2) + (i - 1) * 4, y + 1)
monitor.write('[' .. control .. ']')
end
end
-- Copy of draw_audio_bar, but for when no track is played - just a blank bar.
function api.draw_track_list(x, y, audio_catalogue, start_index, height, monitor)
height = height or select(2, monitor.getSize())
local x_max, y_max = monitor.getSize()
--[[
top bar:
"[X] 21:00:01 Tue 12"
-button to close the app
-time and date
bottom bar:
"[up_btn] [down_btn] Showing [current_max_index - list_size]-[current_max_index] of [#audio_catalogue] | [REFRESH]"
-buttons to go up and down the list
-current index and total number of tracks
-button to refresh the list
list items:
"[index] [title] [length] [play_btn] [download_btn]"
-index - number of the track in the list
-title - title of the track
-length - length of the track
-play_btn - button to play the track
-download_btn - button to download the track (not implemented yet)
]]
-- top bar
monitor.setCursorPos(x, y)
local current_time = os.date("%H:%M:%S %b %d")
local exit_btn = "[X]"
monitor.blit(' ' .. exit_btn .. ' ' ..current_time, 'f0e0f00000000f000f00', 'ffffffffffffffffffff')
-- bottom bar
monitor.setCursorPos(x, y + height - 1)
local bottom_btns = '[' .. api.symbols.app_controls.up .. '] [' .. api.symbols.app_controls.down .. ']'
local showing = 'Showing ' .. tostring(start_index) .. '-' .. tostring(start_index + height - 2) .. ' of ' .. tostring(#audio_catalogue)
local refresh_btn = '[REFRESH]'
monitor.blit(' ' .. bottom_btns .. ' ' .. showing .. ' | ' .. refresh_btn, 'f0e0f0b0f' .. ('0'):rep(#showing) .. "f0f033333330", ('f'):rep(#bottom_btns + #showing + #refresh_btn + 5))
-- monitor.write(' ' .. bottom_btns .. ' ' .. showing .. ' | ' .. refresh_btn)
-- list items
-- i should be current_max_index - height - 2 or 1 if current_max_index - height - 2 < 1
for i = start_index, math.min(start_index + height - 2, #audio_catalogue) do
if (y + 1 + i >= y + height - 1) or i > #audio_catalogue then break end
local track = audio_catalogue[i]
local index = tostring(i)
local title = track.title
local length = seconds_to_string(track.length)
local play_btn = api.symbols.audio_controls.play
local download_btn = api.symbols.app_controls.download
local list_item = ' ' .. index .. ' ' .. title .. ' ' .. length .. ' ' .. play_btn .. ' ' .. download_btn
monitor.setCursorPos(x, y + 1 + i)
monitor.write(list_item)
end
end
return api

View file

@ -0,0 +1,27 @@
local dfpwm = require("cc.audio.dfpwm")
local speaker = peripheral.find("speaker")
local decoder = dfpwm.make_decoder()
local graphics = require("graphics")
local function play_music(status_vars, cfg_vars, audio_catalogue, modem)
if (not status_vars.is_playing) or status_vars.is_paused then return 0 end
modem.transmit(7101, 7101, {type = 'track_chunk',
index = status_vars.track_index,
starting_second = status_vars.current_second,
length = cfg_vars.chunk_size})
local event, sides, channel, replyChannel, message, distance = os.pullEvent('modem_message')
local buffer = decoder(message.track)
while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty")
graphics.draw_audio_bar(1, select(2, term.getSize()) - 2, status_vars.current_second + cfg_vars.chunk_size, audio_catalogue[status_vars.track_index], status_vars.is_paused, term)
end
return status_vars.current_second + cfg_vars.chunk_size
end
return play_music

View file

@ -0,0 +1,59 @@
local touch_processer = {}
touch_processer.init = function ()
local self = {}
function self.process_minimised_controls()
local x, y = e[3], e[4]
local btn_actions = {
pause = {
x = pause_btn_coords.x,
y = pause_btn_coords.y,
func = function()
status_vars.is_paused = not status_vars.is_paused
end
},
forward = {
x = forward_btn_coords.x,
y = forward_btn_coords.y,
func = function()
status_vars.current_second = math.min(status_vars.current_second + cfg_vars.forward_skip, status_vars.max_seconds)
end
},
backward = {
x = backward_btn_coords.x,
y = backward_btn_coords.y,
func = function()
status_vars.current_second = math.max(status_vars.current_second - cfg_vars.backward_skip, 0)
end
},
exit = {
x = exit_btn_coords.x,
y = exit_btn_coords.y,
func = function()
status_vars.is_playing = false
return
end
},
minimise = {
x = minimise_btn_coords.x,
y = minimise_btn_coords.y,
func = function()
status_vars.is_minimised = not status_vars.is_minimised
end
},
}
for k, v in pairs(btn_actions) do
if x == v.x and y == v.y then
v.func()
end
end
end
return self
end
local function key_to_func()
end

View file

@ -0,0 +1,98 @@
--[[
Class to manage ComputerCraft (CC) audio tracks on different disks
Written by Andrew_7_1
]]
local audio_manager = {}
audio_manager.init = function (dirs_file)
local self = {}
-- Load audio directories from file where directories are seperated by newlines"
local function load_audio_dirs(file_location)
local audio_dirs = {}
local file = io.open(file_location, 'r')
if file then
for line in file:lines() do
table.insert(audio_dirs, line)
end
file:close()
end
return audio_dirs
end
local audio_dirs = load_audio_dirs(dirs_file or 'audio_dirs.txt')
--[[
Iterate over all audio directories and load all audio files into a catalogue.
Audio files have extensions of .dfpwm and rarely .wav
Save them to a table of tables like this:
{
["filepath"] = absolute file path
["length"] = file size / 6000
["title"] = file name without extension and path, e.g. "title.dfpwm" -> "title"
["extension"] = file extension, e.g. "dfpwm"
}
]]
local function load_audio_catalogue()
local audio_catalogue = {}
for _, dir in pairs(audio_dirs) do
for _, file in pairs(fs.list(dir)) do
if file:find('.dfpwm') or file:find('.wav') then
local file_path = dir .. '/' .. file
local file_size = fs.getSize(file_path)
local file_title = file:sub(1, #file - #file:match('.*%.(.*)$'))
local file_extension = file:match('.*%.(.*)$')
table.insert(audio_catalogue, {
["filepath"] = file_path,
["length"] = file_size / 6000,
["title"] = file_title,
["extension"] = file_extension
})
end
end
end
return audio_catalogue
end
self.audio_catalogue = load_audio_catalogue()
-- Update audio catalogue
function self.update_catalogue()
self.audio_catalogue = load_audio_catalogue()
end
function self.get_catalogue()
return self.audio_catalogue
end
-- Get part of an audio file in the catalogue
function self.get_track_chunk(index, starting_second, length)
if index > #self.audio_catalogue then
return false
end
local track = self.audio_catalogue[index]
local file = io.open(track.filepath, 'rb')
if not file then
return false
end
file:seek('set', starting_second * 6000)
local chunk = file:read(length * 6000)
file:close()
return chunk
end
-- More convenient way to get track info than using entire catalogue
function self.get_track_details(index)
if index > #self.audio_catalogue then
return false
end
return self.audio_catalogue[index]
end
return self
end
return audio_manager

View file

@ -0,0 +1,60 @@
local logger = {}
logger.init = function(monitor_wrap, scale)
local self = {}
local monitor = monitor_wrap or peripheral.find("monitor")
if not monitor then
monitor = term
else monitor.setTextScale(scale or 0.5)
end
local log_colours = {
['log'] = colours.white,
['info'] = colours.lightBlue,
['warn'] = colours.yellow,
['err'] = colours.red,
['success'] = colours.lime
}
local function move_line()
monitor.setCursorPos(1, select(2, monitor.getCursorPos()) + 1)
if select(2, monitor.getCursorPos()) >= select(2, monitor.getSize()) then
monitor.scroll(1)
monitor.setCursorPos(1, select(2, monitor.getCursorPos()) - 1)
end
end
local function write_msg(message, msg_colour)
local time_str = os.date('%Y-%b-%d %H:%M:%S')
local msg_formatted = "[" .. time_str .. "] " .. message
monitor.setTextColour(msg_colour)
monitor.write(msg_formatted)
move_line()
end
function self.log(msg)
write_msg(msg, log_colours['log'])
end
function self.info(msg)
write_msg('INFO: ' .. msg, log_colours['info'])
end
function self.warning(msg)
write_msg('WARNING: ' .. msg, log_colours['warn'])
end
function self.error(msg)
write_msg('ERROR: ' .. msg, log_colours['err'])
end
function self.success(msg)
write_msg('SUCCESS: ' .. msg, log_colours['success'])
end
return self
end
return logger

View file

@ -0,0 +1,60 @@
--[[
Audio server for ComputerCraft (CC)
Written by Andrew_7_1
]]
local server = {}
server.init = function (port, audio_dir)
local self = {}
local logger = require('logger').init()
local audio_manager = require('audio_manager').init(audio_dir or 'audio_dirs.txt')
local port = port or 7101
local modem = peripheral.find('modem')
if not modem then error('No modem found') end
modem.open(port)
logger.info('Audio server started on port ' .. port .. ' with audio directories from ' .. audio_dir)
local message_types = {
query = function (replyChannel, msg)
modem.transmit(replyChannel, port, {code = 200, audio_catalogue = audio_manager.get_catalogue()})
logger.success('Query sent')
end,
refresh = function (replyChannel, msg)
audio_manager.update_catalogue()
modem.transmit(replyChannel, port, {code = 200})
logger.success('Catalogue updated')
end,
track_info = function (replyChannel, msg)
local track = audio_manager.get_track_details(msg.index)
if not track then
modem.transmit(replyChannel, port, {code = 404})
logger.error('Track not found (id: ' .. msg.index .. ')')
else
modem.transmit(replyChannel, port, {code = 200, track = track})
logger.success('Track details sent (id: ' .. msg.index .. ')')
end
end,
track_chunk = function (replyChannel, msg)
local track = audio_manager.get_track_chunk(msg.index, msg.starting_second, msg.length)
if not track then
modem.transmit(replyChannel, port, {code = 404})
logger.error('Track not found (id: ' .. msg.index .. ')')
else
modem.transmit(replyChannel, port, {code = 200, track = track})
logger.success('Part of a track sent (id: ' .. msg.index .. ', part:' .. msg.starting_second .. '-' .. msg.starting_second + msg.length .. ')')
end
end
}
function self.process_transmission(event, side, channel, replyChannel, message, distance)
message_types[message.type](replyChannel, message)
end
return self
end
return server

View file

@ -0,0 +1,14 @@
local config = {
port = 7101,
audio_dir = 'audio_dirs.txt'
}
local server = require("server").init(config.port, config.audio_dir)
local modem = peripheral.find("modem")
if not modem then error("No modem found") end
modem.open(config.port)
while true do
local event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message")
server.process_transmission(event, side, channel, replyChannel, message, distance)
end