diff options
Diffstat (limited to 'apps/plugins/lua_scripts')
-rw-r--r-- | apps/plugins/lua_scripts/playback.lua | 459 |
1 files changed, 459 insertions, 0 deletions
diff --git a/apps/plugins/lua_scripts/playback.lua b/apps/plugins/lua_scripts/playback.lua new file mode 100644 index 0000000000..bd2d0513f9 --- /dev/null +++ b/apps/plugins/lua_scripts/playback.lua | |||
@@ -0,0 +1,459 @@ | |||
1 | --[[ Lua RB Playback | ||
2 | /*************************************************************************** | ||
3 | * __________ __ ___. | ||
4 | * Open \______ \ ____ ____ | | _\_ |__ _______ ___ | ||
5 | * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / | ||
6 | * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < | ||
7 | * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ | ||
8 | * \/ \/ \/ \/ \/ | ||
9 | * $Id$ | ||
10 | * | ||
11 | * Copyright (C) 2020 William Wilgus | ||
12 | * | ||
13 | * This program is free software; you can redistribute it and/or | ||
14 | * modify it under the terms of the GNU General Public License | ||
15 | * as published by the Free Software Foundation; either version 2 | ||
16 | * of the License, or (at your option) any later version. | ||
17 | * | ||
18 | * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY | ||
19 | * KIND, either express or implied. | ||
20 | * | ||
21 | ****************************************************************************/ | ||
22 | ]] | ||
23 | |||
24 | local print = require("print") | ||
25 | |||
26 | local _draw = require("draw") | ||
27 | local _poly = require("draw_poly") | ||
28 | local _clr = require("color") | ||
29 | |||
30 | require("actions") | ||
31 | require("rbsettings") | ||
32 | require("settings") | ||
33 | |||
34 | local scrpath = rb.current_path() | ||
35 | |||
36 | local metadata = rb.settings.read | ||
37 | local cur_trk = "audio_current_track" | ||
38 | local track_data = {} | ||
39 | -- grab only settings we are interested in | ||
40 | track_data.title = rb.metadata.mp3_entry.title | ||
41 | track_data.path = rb.metadata.mp3_entry.path | ||
42 | |||
43 | do -- free up some ram by removing items we don't need | ||
44 | local function strip_functions(t, ...) | ||
45 | local t_keep = {...} | ||
46 | local keep | ||
47 | for key, val in pairs(t) do | ||
48 | keep = false | ||
49 | for _, v in ipairs(t_keep) do | ||
50 | if string.find (key, v) then | ||
51 | keep = true; break | ||
52 | end | ||
53 | end | ||
54 | if keep ~= true then | ||
55 | t[key] = nil | ||
56 | end | ||
57 | end | ||
58 | end | ||
59 | |||
60 | strip_functions(rb.actions, "PLA_", "TOUCHSCREEN", "_NONE") | ||
61 | rb.contexts = nil | ||
62 | |||
63 | strip_functions(_draw, "^rect$", "^rect_filled$") | ||
64 | strip_functions(_poly, "^polyline$") | ||
65 | strip_functions(print.opt, "line", "get") | ||
66 | |||
67 | _clr.inc = nil | ||
68 | rb.metadata = nil -- remove metadata settings | ||
69 | rb.system = nil -- remove system settings | ||
70 | rb.settings.dump = nil | ||
71 | collectgarbage("collect") | ||
72 | end | ||
73 | |||
74 | local t_icn = {} | ||
75 | t_icn[1] = {16,16,16,4,10,10,10,4,4,10,10,16,10,10} -- rewind | ||
76 | t_icn[2] = {4,5,4,15,10,9,10,15,12,15,12,5,10,5,10,11,4,5} -- play/pause | ||
77 | t_icn[3] = {4,4,4,16,10,10,10,16,16,10,10,4,10,10} -- fast forward | ||
78 | |||
79 | local pb = {} | ||
80 | local track_length | ||
81 | |||
82 | local track_name = metadata(cur_trk, track_data.title) or | ||
83 | metadata(cur_trk, track_data.path) | ||
84 | |||
85 | local clr_active = _clr.set(1, 0, 255, 0) | ||
86 | local clr_inactive = _clr.set(0, 255, 255, 255) | ||
87 | local t_clr_icn = {clr_inactive, clr_inactive, clr_inactive} | ||
88 | |||
89 | |||
90 | local function set_active_icon(idx) | ||
91 | local tClr = t_clr_icn | ||
92 | |||
93 | if idx == 4 and t_clr_icn[4] == clr_active then | ||
94 | idx = 2 | ||
95 | end | ||
96 | for i = 1, 4 do | ||
97 | t_clr_icn[i] = clr_inactive | ||
98 | end | ||
99 | if idx >= 1 and idx <= 4 then | ||
100 | t_clr_icn[idx] = clr_active | ||
101 | end | ||
102 | end | ||
103 | |||
104 | local function clear_actions() | ||
105 | track_length = (rb.audio("length") or 0) | ||
106 | local playback = rb.audio("status") | ||
107 | if playback == 1 then | ||
108 | set_active_icon(2) | ||
109 | elseif playback == 3 then | ||
110 | set_active_icon(4) | ||
111 | return | ||
112 | end | ||
113 | rockev.trigger("timer", false, rb.current_tick() + rb.HZ * 60) | ||
114 | end | ||
115 | |||
116 | -------------------------------------------------------------------------------- | ||
117 | --[[ returns a sorted tables of directories and (another) of files | ||
118 | -- path is the starting path; norecurse == true.. only that path will be searched | ||
119 | -- findfile & finddir are definable search functions | ||
120 | -- if not defined all files/dirs are returned if false is passed.. none | ||
121 | -- or you can provide your own function see below.. | ||
122 | -- f_t and d_t allow you to pass your own tables for re-use but isn't necessary | ||
123 | ]] | ||
124 | local function get_files(path, norecurse, finddir, findfile, sort_by, max_depth, f_t, d_t) | ||
125 | local quit = false | ||
126 | --local sort_by_function -- forward declaration | ||
127 | local filepath_function -- forward declaration | ||
128 | local files = f_t or {} | ||
129 | local dirs = d_t or {} | ||
130 | |||
131 | local function f_filedir(name) | ||
132 | --default find function | ||
133 | -- example: return name:find(".mp3", 1, true) ~= nil | ||
134 | if name:len() <= 2 and (name == "." or name == "..") then | ||
135 | return false | ||
136 | end | ||
137 | return true | ||
138 | end | ||
139 | local function d_filedir(name) | ||
140 | --default discard function | ||
141 | return false | ||
142 | end | ||
143 | |||
144 | if finddir == nil then | ||
145 | finddir = f_filedir | ||
146 | elseif type(finddir) ~= "function" then | ||
147 | finddir = d_filedir | ||
148 | end | ||
149 | |||
150 | if findfile == nil then | ||
151 | findfile = f_filedir | ||
152 | elseif type(findfile) ~= "function" then | ||
153 | findfile = d_filedir | ||
154 | end | ||
155 | |||
156 | if not max_depth then max_depth = 3 end | ||
157 | max_depth = max_depth + 1 -- determined by counting '/' 3 deep = /d1/d2/d3/ | ||
158 | |||
159 | local function _get_files(path) | ||
160 | local sep = "" | ||
161 | local filepath | ||
162 | local finfo_t | ||
163 | local count | ||
164 | if string.sub(path, - 1) ~= "/" then sep = "/" end | ||
165 | for fname, isdir, finfo_t in luadir.dir(path, true) do | ||
166 | if isdir and finddir(fname) then | ||
167 | path, count = string.gsub(path, "/", "/") | ||
168 | if count <= max_depth then | ||
169 | table.insert(dirs, path .. sep ..fname) | ||
170 | end | ||
171 | elseif not isdir and findfile(fname) then | ||
172 | filepath = filepath_function(path, sep, fname) | ||
173 | table.insert(files, filepath) | ||
174 | end | ||
175 | |||
176 | if action_quit() then | ||
177 | return true | ||
178 | end | ||
179 | end | ||
180 | end | ||
181 | |||
182 | rb.splash(10, "Searching for Files") | ||
183 | |||
184 | filepath_function = function(path, sep, fname) | ||
185 | return string.format("%s%s%s;", path, sep, fname) | ||
186 | end | ||
187 | table.insert(dirs, path) -- root | ||
188 | |||
189 | for key,value in pairs(dirs) do | ||
190 | --luadir.dir may error out so we need to do the call protected | ||
191 | -- _get_files(value, CANCEL_BUTTON) | ||
192 | _, quit = pcall(_get_files, value) | ||
193 | |||
194 | if quit == true or norecurse then | ||
195 | break; | ||
196 | end | ||
197 | end | ||
198 | |||
199 | return dirs, files | ||
200 | end -- get_files | ||
201 | |||
202 | local audio_elapsed, audio_ff_rew | ||
203 | do | ||
204 | local elapsed = 0 | ||
205 | local ff_rew = 0 | ||
206 | audio_elapsed = function() | ||
207 | if ff_rew == 0 then elapsed = (rb.audio("elapsed") or 0) end | ||
208 | return elapsed | ||
209 | end | ||
210 | |||
211 | audio_ff_rew = function(time_ms) | ||
212 | if ff_rew ~= 0 and time_ms == 0 then | ||
213 | rb.audio("stop") | ||
214 | rb.audio("play", elapsed, 0) | ||
215 | rb.sleep(100) | ||
216 | elseif time_ms ~= 0 then | ||
217 | elapsed = elapsed + time_ms | ||
218 | if elapsed < 0 then elapsed = 0 end | ||
219 | if elapsed > track_length then elapsed = track_length end | ||
220 | end | ||
221 | ff_rew = time_ms | ||
222 | end | ||
223 | end | ||
224 | |||
225 | do | ||
226 | local act = rb.actions | ||
227 | local quit = false | ||
228 | local last_action = 0 | ||
229 | local magnitude = 1 | ||
230 | local skip_ms = 1000 | ||
231 | local playback | ||
232 | |||
233 | function action_event(action) | ||
234 | local event | ||
235 | if action == act.PLA_EXIT or action == act.PLA_CANCEL then | ||
236 | quit = true | ||
237 | elseif action == act.PLA_RIGHT_REPEAT then | ||
238 | event = pb.TRACK_FF | ||
239 | audio_ff_rew(skip_ms * magnitude) | ||
240 | magnitude = magnitude + 1 | ||
241 | elseif action == act.PLA_LEFT_REPEAT then | ||
242 | event = pb.TRACK_REW | ||
243 | audio_ff_rew(-skip_ms * magnitude) | ||
244 | magnitude = magnitude + 1 | ||
245 | elseif action == act.PLA_SELECT then | ||
246 | playback = rb.audio("status") | ||
247 | if playback == 1 then | ||
248 | rb.audio("pause") | ||
249 | collectgarbage("collect") | ||
250 | elseif playback == 3 then | ||
251 | rb.audio("resume") | ||
252 | end | ||
253 | event = rb.audio("status") + 1 | ||
254 | elseif action == act.ACTION_NONE then | ||
255 | magnitude = 1 | ||
256 | audio_ff_rew(0) | ||
257 | if last_action == act.PLA_RIGHT then | ||
258 | rb.audio("next") | ||
259 | elseif last_action == act.PLA_LEFT then | ||
260 | rb.audio("prev") | ||
261 | end | ||
262 | end | ||
263 | |||
264 | if event then -- pass event id to playback_event | ||
265 | rockev.trigger(pb.EV, true, event) | ||
266 | end | ||
267 | |||
268 | last_action = action | ||
269 | end | ||
270 | |||
271 | function action_set_quit(bQuit) | ||
272 | quit = bQuit | ||
273 | end | ||
274 | |||
275 | function action_quit() | ||
276 | return quit | ||
277 | end | ||
278 | end | ||
279 | |||
280 | do | ||
281 | pb.EV = "playback" | ||
282 | -- custom event ids | ||
283 | pb.STOPPED = 1 | ||
284 | pb.PLAY = 2 | ||
285 | pb.PAUSE = 3 | ||
286 | pb.PAUSED = 4 | ||
287 | pb.TRACK_REW = 5 | ||
288 | pb.PLAY_ = 6 | ||
289 | pb.TRACK_FF = 7 | ||
290 | |||
291 | function playback_event(id, event_data) | ||
292 | if id == pb.PLAY then | ||
293 | id = pb.PLAY_ | ||
294 | elseif id == pb.PAUSE then | ||
295 | id = 8 | ||
296 | elseif id == rb.PLAYBACK_EVENT_TRACK_BUFFER or | ||
297 | id == rb.PLAYBACK_EVENT_TRACK_CHANGE then | ||
298 | rb.lcd_scroll_stop() | ||
299 | track_name = metadata(cur_trk, track_data.title) or | ||
300 | metadata(cur_trk, track_data.path) | ||
301 | else | ||
302 | -- rb.splash(0, id) | ||
303 | end | ||
304 | |||
305 | set_active_icon(id - 4) | ||
306 | rockev.trigger("timer", false, rb.current_tick() + rb.HZ) | ||
307 | end | ||
308 | end | ||
309 | |||
310 | local function pbar_init(img, x, y, w, h, fgclr, bgclr, barclr) | ||
311 | local t | ||
312 | -- when initialized table is returned that points back to this function | ||
313 | if type(img) == "table" then | ||
314 | t = img | ||
315 | t.pct = x | ||
316 | if not t.set then error("not initialized", 2) end | ||
317 | else | ||
318 | t = {} | ||
319 | t.img = img or rb.lcd_framebuffer() | ||
320 | t.x = x or 1 | ||
321 | t.y = y or 1 | ||
322 | t.w = w or 10 | ||
323 | t.h = h or 10 | ||
324 | t.pct = 0 | ||
325 | t.fgclr = fgclr or _clr.set(-1, 255, 255, 255) | ||
326 | t.bgclr = bgclr or _clr.set(0, 0, 50, 200) | ||
327 | t.barclr = barclr or t.fgclr | ||
328 | |||
329 | t.set = pbar_init | ||
330 | setmetatable(t,{__call = t.set}) | ||
331 | return t | ||
332 | end -- initalization | ||
333 | --============================================================================-- | ||
334 | if t.pct < 0 then | ||
335 | t.pct = 0 | ||
336 | elseif t.pct > 100 then | ||
337 | t.pct = 100 | ||
338 | end | ||
339 | |||
340 | local wp = t.pct * (t.w - 4) / 100 | ||
341 | |||
342 | if wp < 1 and t.pct > 0 then wp = 1 end | ||
343 | |||
344 | _draw.rect_filled(t.img, t.x, t.y, t.w, t.h, t.fgclr, t.bgclr, true) | ||
345 | _draw.rect_filled(t.img, t.x + 2, t.y + 2, wp, t.h - 4, t.barclr, nil, true) | ||
346 | end -- pbar_init | ||
347 | |||
348 | local function create_playlist(startdir, file_search, maxfiles) | ||
349 | |||
350 | function f_filedir_(name) | ||
351 | if name:len() <= 2 and (name == "." or name == "..") then | ||
352 | return false | ||
353 | end | ||
354 | if name:find(file_search) ~= nil then | ||
355 | maxfiles = maxfiles -1 | ||
356 | if maxfiles < 1 then | ||
357 | action_set_quit(true) | ||
358 | return true | ||
359 | end | ||
360 | return true | ||
361 | else | ||
362 | return false | ||
363 | end | ||
364 | end | ||
365 | |||
366 | local norecurse = false | ||
367 | local f_finddir = nil | ||
368 | local f_findfile = f_filedir_ | ||
369 | local files = {} | ||
370 | local dirs = {} | ||
371 | local max_depth = 3 | ||
372 | |||
373 | dirs, files = get_files(startdir, norecurse, f_finddir, f_findfile, nil, max_depth, dirs, files) | ||
374 | |||
375 | if #files > 0 then | ||
376 | rb.audio("stop") | ||
377 | rb.playlist("create", scrpath .. "/", "playback.m3u8") | ||
378 | end | ||
379 | |||
380 | for i = 1, #files do | ||
381 | rb.playlist("insert_track", string.match(files[i], "[^;]+") or "?") | ||
382 | end | ||
383 | if #files > 0 then | ||
384 | rb.playlist("start", 0, 0, 0) | ||
385 | end | ||
386 | |||
387 | for i=1, #dirs do dirs[i] = nil end -- empty table | ||
388 | for i=1, #files do files[i] = nil end -- empty table | ||
389 | action_set_quit(false) | ||
390 | end -- create_playlist | ||
391 | |||
392 | local function main() | ||
393 | clear_actions() | ||
394 | local lcd = rb.lcd_framebuffer() | ||
395 | |||
396 | local eva = rockev.register("action", action_event) | ||
397 | |||
398 | local evp = rockev.register("playback", playback_event) | ||
399 | |||
400 | if not track_name then -- Nothing loaded lets search for some mp3's | ||
401 | create_playlist("/", "%.mp3$", 10) | ||
402 | collectgarbage("collect") | ||
403 | end | ||
404 | |||
405 | local evt = rockev.register("timer", clear_actions, rb.HZ) | ||
406 | |||
407 | rb.lcd_clear_display() | ||
408 | rb.lcd_update() | ||
409 | do -- configure print function | ||
410 | local t_print = print.opt.get(true) | ||
411 | t_print.autoupdate = false | ||
412 | t_print.justify = "center" | ||
413 | t_print.col = 2 | ||
414 | end | ||
415 | |||
416 | local progress = pbar_init(nil, 1, rb.LCD_HEIGHT - 5, rb.LCD_WIDTH - 1, 5) | ||
417 | |||
418 | local i = 0 | ||
419 | |||
420 | local colw = (rb.LCD_WIDTH - 16) / 4 | ||
421 | local scr_col = {colw, colw * 2, colw * 3} | ||
422 | --local mem = collectgarbage("count") | ||
423 | --local mem_min = mem | ||
424 | --local mem_max = mem | ||
425 | while not action_quit() do | ||
426 | elapsed = audio_elapsed() | ||
427 | |||
428 | -- control initialized with pbar_init now we set the value | ||
429 | progress(track_length > 0 and elapsed / (track_length / 100) or 0) | ||
430 | |||
431 | print.opt.line(1) | ||
432 | print.f() -- clear the line | ||
433 | print.f(track_name) | ||
434 | print.f() -- clear the line | ||
435 | _,_,_,h = print.f("%ds / %ds", elapsed / 1000, track_length / 1000) | ||
436 | |||
437 | for i = 1, 3 do -- draw the rew/play/fwd icons | ||
438 | _poly.polyline(lcd, scr_col[i], h * 2 + 1, t_icn[i], t_clr_icn[i], true) | ||
439 | end | ||
440 | --[[ | ||
441 | mem = collectgarbage("count") | ||
442 | if mem < mem_min then mem_min = mem end | ||
443 | if mem > mem_max then mem_max = mem end | ||
444 | |||
445 | if i >= 10 then | ||
446 | rb.splash(0, mem_min .. " : " .. mem .. " : " ..mem_max) | ||
447 | i = 0 | ||
448 | else | ||
449 | i = i + 1 | ||
450 | end | ||
451 | --]] | ||
452 | rb.lcd_update() | ||
453 | rb.sleep(rb.HZ / 2) | ||
454 | end | ||
455 | |||
456 | rb.splash(100, "Goodbye") | ||
457 | end | ||
458 | |||
459 | main() -- BILGUS | ||