summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWilliam Wilgus <wilgus.william@gmail.com>2021-11-28 22:23:48 -0500
committerWilliam Wilgus <me.theuser@yahoo.com>2021-12-03 16:56:12 -0500
commit7200b738a5426d384e70fe814b7b84ab86326f01 (patch)
tree5d4af1c5aea7017cce7888b4af41f2f4b71c4d09
parentc94acc771d24fde560f52db3affd0172fdb6d20e (diff)
downloadrockbox-7200b738a5426d384e70fe814b7b84ab86326f01.tar.gz
rockbox-7200b738a5426d384e70fe814b7b84ab86326f01.zip
lua random_playlist generator
uses the database to create random playlists of specified size with the advanced GUI you can.. set the number of songs (up to max playlist buffer) set the number of tracks before a repeat is allowed play the tracks when finished -- fix a parsing bug where first and last entry in database were corrupted -- Incremental loading for very large databases, allows decent speed and lower ram usage Change-Id: Ia95469b6a04625b621129ec61e5db4b2b00adc5e
-rw-r--r--apps/plugins/lua_scripts/dbgettags.lua15
-rw-r--r--apps/plugins/lua_scripts/random_playlist.lua464
2 files changed, 472 insertions, 7 deletions
diff --git a/apps/plugins/lua_scripts/dbgettags.lua b/apps/plugins/lua_scripts/dbgettags.lua
index ec6e29a330..8e9f26393d 100644
--- a/apps/plugins/lua_scripts/dbgettags.lua
+++ b/apps/plugins/lua_scripts/dbgettags.lua
@@ -28,13 +28,14 @@ local CANCEL_BUTTON = rb.actions.PLA_CANCEL
28local sINVALIDDATABASE = "Invalid Database" 28local sINVALIDDATABASE = "Invalid Database"
29local sERROROPENING = "Error opening" 29local sERROROPENING = "Error opening"
30 30
31-- tag cache header 31-- tag cache header
32local sTCVERSION = string.char(0x0F) 32sTCVERSION = string.char(0x10)
33local sTCHEADER = string.reverse("TCH" .. sTCVERSION) 33sTCHEADER = string.reverse("TCH" .. sTCVERSION)
34local DATASZ = 4 -- int32_t 34DATASZ = 4 -- int32_t
35local TCHSIZE = 3 * DATASZ -- 3 x int32_t 35TCHSIZE = 3 * DATASZ -- 3 x int32_t
36 36
37local function bytesLE_n(str) 37-- Converts array of bytes to proper endian
38function bytesLE_n(str)
38 str = str or "" 39 str = str or ""
39 local tbyte={str:byte(1, -1)} 40 local tbyte={str:byte(1, -1)}
40 local bpos = 1 41 local bpos = 1
diff --git a/apps/plugins/lua_scripts/random_playlist.lua b/apps/plugins/lua_scripts/random_playlist.lua
new file mode 100644
index 0000000000..6e4dbe25e2
--- /dev/null
+++ b/apps/plugins/lua_scripts/random_playlist.lua
@@ -0,0 +1,464 @@
1--[[ Lua RB Random Playlist -- random_playlist.lua V 1.0
2/***************************************************************************
3 * __________ __ ___.
4 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
5 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
6 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
7 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
8 * \/ \/ \/ \/ \/
9 * $Id$
10 *
11 * Copyright (C) 2021 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]]
23require ("actions")
24require("dbgettags")
25get_tags = nil -- unneeded
26
27-- User defaults
28local playlistpath = "/Playlists"
29local max_tracks = 500; -- size of playlist to create
30local min_repeat = 500; -- this many songs before a repeat
31local play_on_success = true;
32
33-- Random integer function
34local random = math.random; -- ref random(min, max)
35math.randomseed(rb.current_tick()); -- some kind of randomness
36
37-- Button definitions
38local CANCEL_BUTTON = rb.actions.PLA_CANCEL
39local OK_BUTTON = rb.actions.PLA_SELECT
40local ADD_BUTTON = rb.actions.PLA_UP
41local ADD_BUTTON_RPT = rb.actions.PLA_UP_REPEAT or ADD_BUTTON
42local SUB_BUTTON = rb.actions.PLA_DOWN
43local SUB_BUTTON_RPT = rb.actions.PLA_DOWN_REPEAT or SUB_BUTTON
44-- remove action and context tables to free some ram
45rb.actions = nil
46rb.contexts = nil
47-- Program strings
48local sINITDATABASE = "Initialize Database"
49local sHEADERTEXT = "Random Playlist"
50local sPLAYLISTERROR = "Playlist Error!"
51local sREMOVEPLAYLIST = "Removing Dynamic Playlist"
52local sSEARCHINGFILES = "Searching for Files.."
53local sERROROPENFMT = "Error Opening %s"
54local sINVALIDDBFMT = "Invalid Database %s"
55local sPROGRESSHDRFMT = "%d \\ %d Tracks"
56local sGOODBYE = "Goodbye"
57
58-- Gets size of text
59local function text_extent(msg, font)
60 font = font or rb.FONT_UI
61 return rb.font_getstringsize(msg, font)
62end
63
64local function _setup_random_playlist(tag_entries, play, min_repeat, trackcount)
65 -- Setup string tables
66 local tPLAYTEXT = {"Play? [ %s ] (up/dn)", "true = play tracks on success"}
67 local tREPEATTEXT = {"Repeat hist? [ %d ] (up/dn)","higher = less repeated songs"}
68 local tPLSIZETEXT = {"Find [ %d ] tracks? (up/dn)",
69 "Warning overwrites dynamic playlist",
70 "Press back to cancel"};
71 -- how many lines can we fit on the screen?
72 local res, w, h = text_extent("I")
73 h = h + 5 -- increase spacing in the setup menu
74 local max_w = rb.LCD_WIDTH / w
75 local max_h = rb.LCD_HEIGHT - h
76 local y = 0
77
78 -- User Setup Menu
79 local action, ask, increment
80 local t_desc = {scroll = true} -- scroll the setup items
81
82 -- Clears screen and adds title and icon, called first..
83 function show_setup_header()
84 local desc = {icon = 2, show_icons = true, scroll = true} -- 2 == Icon_Playlist
85 rb.lcd_clear_display()
86 rb.lcd_put_line(1, 0, sHEADERTEXT, desc)
87 end
88
89 -- Display up to 3 items and waits for user action -- returns action
90 function ask_user_action(desc, ln1, ln2, ln3)
91 if ln1 then rb.lcd_put_line(1, h, ln1, desc) end
92 if ln2 then rb.lcd_put_line(1, h + h, ln2, desc) end
93 if ln3 then rb.lcd_put_line(1, h + h + h, ln3, desc) end
94 rb.lcd_hline(1,rb.LCD_WIDTH - 1, h - 5);
95 rb.lcd_update()
96
97 local act = rb.get_plugin_action(-1); -- Blocking wait for action
98 -- handle magnitude of the increment here so consumer fn doesn't need to
99 if act == ADD_BUTTON_RPT and act ~= ADD_BUTTON then
100 increment = increment + 1
101 if increment > 1000 then increment = 1000 end
102 act = ADD_BUTTON
103 elseif act == SUB_BUTTON_RPT and act ~= SUB_BUTTON then
104 increment = increment + 1
105 if increment > 1000 then increment = 1000 end
106 act = SUB_BUTTON
107 else
108 increment = 1;
109 end
110
111 return act
112 end
113
114 -- Play the playlist on successful completion true/false?
115 function setup_get_play()
116 action = ask_user_action(tdesc,
117 string.format(tPLAYTEXT[1], tostring(play)),
118 tPLAYTEXT[2]);
119 if action == ADD_BUTTON then
120 play = true
121 elseif action == SUB_BUTTON then
122 play = false
123 end
124 end
125
126 -- Repeat song buffer list of previously added tracks 0-??
127 function setup_get_repeat()
128 if min_repeat >= trackcount then min_repeat = trackcount - 1 end
129 if min_repeat >= tag_entries then min_repeat = tag_entries - 1 end
130 action = ask_user_action(t_desc,
131 string.format(tREPEATTEXT[1],min_repeat),
132 tREPEATTEXT[2]);
133 if action == ADD_BUTTON then
134 min_repeat = min_repeat + increment
135 elseif action == SUB_BUTTON then -- MORE REPEATS LESS RAM USED
136 if min_repeat < increment then increment = 1 end
137 min_repeat = min_repeat - increment
138 if min_repeat < 0 then min_repeat = 0 end
139 elseif action == OK_BUTTON then
140 ask = setup_get_play;
141 setup_get_repeat = nil
142 action = 0
143 end
144 end
145
146 -- How many tracks to find
147 function setup_get_playlist_size()
148 action = ask_user_action(t_desc,
149 string.format(tPLSIZETEXT[1], trackcount),
150 tPLSIZETEXT[2],
151 tPLSIZETEXT[3]);
152 if action == ADD_BUTTON then
153 trackcount = trackcount + increment
154 elseif action == SUB_BUTTON then
155 if trackcount < increment then increment = 1 end
156 trackcount = trackcount - increment
157 if trackcount < 1 then trackcount = 1 end
158 elseif action == OK_BUTTON then
159 ask = setup_get_repeat;
160 setup_get_playlist_size = nil
161 action = 0
162 end
163 end
164 ask = setup_get_playlist_size; -- \!FIRSTRUN!/
165
166 repeat -- SETUP MENU LOOP
167 show_setup_header()
168 ask()
169 rb.lcd_scroll_stop() -- I'm still wary of not doing this..
170 collectgarbage("collect")
171 if action == CANCEL_BUTTON then rb.lcd_scroll_stop(); return nil end
172 until (action == OK_BUTTON)
173
174 return play, min_repeat, trackcount;
175end
176
177--[[ Given the filenameDB file [database]
178 creates a random dynamic playlist with a default savename of [playlist]
179 containing [trackcount] tracks, played on completion if [play] is true]]
180function create_random_playlist(database, playlist, trackcount, play)
181 if not database or not playlist or not trackcount then return end
182 if not play then play = false end
183
184 local file = io.open('/' .. database or "", "r") --read
185 if not file then rb.splash(100, string.format(sERROROPENFMT, database)) return end
186
187 local fsz = file:seek("end")
188 local fbegin
189 local posln = 0
190 local tag_len = TCHSIZE
191
192 local anchor_index
193 local ANCHOR_INTV
194 local track_index = setmetatable({},{__mode = "v"}) --[[ weak table values
195 this allows them to be garbage collected as space is needed / rebuilt as needed ]]
196
197 -- Read character function sets posln as file position
198 function readchrs(count)
199 if posln >= fsz then return nil end
200 file:seek("set", posln)
201 posln = posln + count
202 return file:read(count)
203 end
204
205 -- Check the header and get size + #entries
206 local tagcache_header = readchrs(DATASZ) or ""
207 local tagcache_sz = readchrs(DATASZ) or ""
208 local tagcache_entries = readchrs(DATASZ) or ""
209
210 if tagcache_header ~= sTCHEADER or
211 bytesLE_n(tagcache_sz) ~= (fsz - TCHSIZE) then
212 rb.splash(100, string.format(sINVALIDDBFMT, database))
213 return
214 end
215
216 local tag_entries = bytesLE_n(tagcache_entries)
217
218 play, min_repeat, trackcount = _setup_random_playlist(
219 tag_entries, play, min_repeat, trackcount);
220 _setup_random_playlist = nil
221 collectgarbage("collect")
222
223 -- how many lines can we fit on the screen?
224 local res, w, h = text_extent("I")
225 local max_w = rb.LCD_WIDTH / w
226 local max_h = rb.LCD_HEIGHT - h
227 local y = 0
228 rb.lcd_clear_display()
229
230 function get_tracks_random()
231 local tries, idxp
232
233 local tracks = 0
234 local str = ""
235 local t_lru = {}
236 local lru_widx = 1
237 local lru_max = min_repeat
238 if lru_max >= tag_entries then lru_max = tag_entries / 2 + 1 end
239
240 function do_progress_header()
241 rb.lcd_put_line(1, 0, string.format(sPROGRESSHDRFMT,tracks, trackcount))
242 rb.lcd_update()
243 --rb.sleep(300)
244 end
245
246 function show_progress()
247 local sdisp = str:match("([^/]+)$") or "?" --just the track name
248 rb.lcd_put_line(1, y, sdisp:sub(1, max_w));-- limit string length
249 y = y + h
250 if y >= max_h then
251 do_progress_header()
252 rb.lcd_clear_display()
253 y = h
254 end
255 end
256
257 -- check for repeated tracks
258 function check_lru(val)
259 if lru_max <= 0 or val == nil then return 0 end --user wants all repeats
260 local rv
261 local i = 1
262 repeat
263 rv = t_lru[i]
264 if rv == nil then
265 break;
266 elseif rv == val then
267 return i
268 end
269 i = i + 1
270 until (i == lru_max)
271 return 0
272 end
273
274 -- add a track to the repeat list (overwrites oldest if full)
275 function push_lru(val)
276 t_lru[lru_widx] = val
277 lru_widx = lru_widx + 1
278 if lru_widx > lru_max then lru_widx = 1 end
279 end
280
281 function get_index()
282 if ANCHOR_INTV > 1 then
283 get_index =
284 function(plidx)
285 local p = track_index[plidx]
286 if p == nil then
287 parse_database_offsets(plidx)
288 end
289 return track_index[plidx][1]
290 end
291 else -- all tracks are indexed
292 get_index =
293 function(plidx)
294 return track_index[plidx]
295 end
296 end
297 end
298
299 get_index() --init get_index fn
300 -- Playlist insert loop
301 while true do
302 str = nil
303 tries = 0
304 repeat
305 idxp = random(1, tag_entries)
306 tries = tries + 1 -- prevent endless loops
307 until check_lru(idxp) == 0 or tries > fsz -- check for recent repeats
308
309 posln = get_index(idxp)
310
311 tag_len = bytesLE_n(readchrs(DATASZ))
312 posln = posln + DATASZ -- idx = bytesLE_n(readchrs(DATASZ))
313 str = readchrs(tag_len) or "\0" -- Read the database string
314 str = str:match("^(%Z+)%z$") -- \0 terminated string
315
316 -- Insert track into playlist
317 if str ~= nil then
318 tracks = tracks + 1
319 show_progress()
320 push_lru(idxp) -- add to repeat list
321 if rb.playlist("insert_track", str) < 0 then
322 rb.splash(rb.HZ, sPLAYLISTERROR)
323 break; -- ERROR, PLAYLIST FULL?
324 end
325
326 end
327
328 if tracks >= trackcount then
329 do_progress_header()
330 break
331 end
332
333 -- check for cancel non-blocking
334 if rb.get_plugin_action(0) == CANCEL_BUTTON then
335 break
336 end
337 end
338 end -- get_files
339
340 function build_anchor_index()
341 -- index every n files
342 ANCHOR_INTV = 1 -- for small db we can put all the entries in ram
343 local ent = tag_entries / 1000 -- more than 10,000 will be incrementally loaded
344 while ent >= 10 do -- need to reduce the size of the anchor index?
345 ent = ent / 10
346 ANCHOR_INTV = ANCHOR_INTV * 10
347 end -- should be power of 10 (10, 100, 1000..)
348 --grab an index for every ANCHOR_INTV entries
349 local aidx={}
350 local acount = 0
351 local next_idx = 1
352 local index = 1
353 local tlen
354 if ANCHOR_INTV == 1 then acount = 1 end
355 while index <= tag_entries and posln < fsz do
356 if next_idx == index then
357 acount = acount + 1
358 next_idx = acount * ANCHOR_INTV
359 aidx[index] = posln
360 else -- fill the weak table, we already did the work afterall
361 track_index[index] = {posln} -- put vals inside table to make them collectable
362 end
363 index = index + 1
364 tlen = bytesLE_n(readchrs(DATASZ))
365 posln = posln + tlen + DATASZ
366 end
367 return aidx
368 end
369
370 function parse_database_offsets(plidx)
371 local tlen
372 -- round to nearest anchor entry that is less than plidx
373 local aidx = (plidx / ANCHOR_INTV) * ANCHOR_INTV
374 local cidx = aidx
375 track_index[cidx] = {anchor_index[aidx] or fbegin};
376 -- maybe we can use previous work to get closer to the desired offset
377 while track_index[cidx] ~= nil and cidx <= plidx do
378 cidx = cidx + 1 --keep seeking till we find an empty entry
379 end
380 posln = track_index[cidx - 1][1]
381 while cidx <= plidx do --[[ walk the remaining entries from the last known
382 & save the entries on the way to our desired entry ]]
383 tlen = bytesLE_n(readchrs(DATASZ))
384 posln = posln + tlen + DATASZ
385 track_index[cidx] = {posln} -- put vals inside table to make them collectable
386 if posln >= fsz then posln = fbegin end
387 cidx = cidx + 1
388 end
389 end
390
391 if trackcount ~= nil then
392 rb.splash(10, sSEARCHINGFILES)
393 fbegin = posln --Mark the beginning for later loops
394 tag_len = 0
395 anchor_index = build_anchor_index() -- index track offsets
396 if ANCHOR_INTV == 1 then
397 -- all track indexes are in ram
398 track_index = anchor_index
399 anchor_index = nil
400 end
401
402 rb.splash(10, sREMOVEPLAYLIST)
403 rb.audio("stop")
404 os.remove( playlistpath .. "/" .. playlist)
405 rb.playlist("remove_all_tracks")
406 rb.playlist("create", playlistpath .. "/", playlist)
407--[[ --profiling
408 local starttime = rb.current_tick();
409 get_tracks_random()
410 local endtime = rb.current_tick();
411 rb.splash(1000, (endtime - starttime) .. " ticks");
412 end
413 if (false) then
414--]]
415 get_tracks_random()
416 end
417
418 file:close()
419 collectgarbage("collect")
420 if trackcount and rb.playlist("amount") >= trackcount and play == true then
421 rb.playlist("start", 0, 0, 0)
422 end
423
424end -- create_playlist
425
426local function main()
427 if not rb.file_exists(rb.ROCKBOX_DIR .. "/database_4.tcd") then
428 rb.splash(rb.HZ, sINITDATABASE)
429 os.exit(1);
430 end
431 if rb.cpu_boost then rb.cpu_boost(true) end
432 rb.backlight_force_on()
433 if not rb.dir_exists(playlistpath) then
434 luadir.mkdir(playlistpath)
435 end
436 rb.lcd_clear_display()
437 rb.lcd_update()
438 collectgarbage("collect")
439 create_random_playlist(rb.ROCKBOX_DIR .. "/database_4.tcd",
440 "random_playback.m3u8", max_tracks, play_on_success);
441 rb.splash(rb.HZ * 2, sGOODBYE)
442 -- Restore user backlight settings
443 rb.backlight_use_settings()
444 if rb.cpu_boost then rb.cpu_boost(false) end
445
446--[[
447local used, allocd, free = rb.mem_stats()
448local lu = collectgarbage("count")
449local fmt = function(t, v) return string.format("%s: %d Kb\n", t, v /1024) end
450
451-- this is how lua recommends to concat strings rather than ..
452local s_t = {}
453s_t[1] = "rockbox:\n"
454s_t[2] = fmt("Used ", used)
455s_t[3] = fmt("Allocd ", allocd)
456s_t[4] = fmt("Free ", free)
457s_t[5] = "\nlua:\n"
458s_t[6] = fmt("Used", lu * 1024)
459s_t[7] = "\n\nNote that the rockbox used count is a high watermark"
460rb.splash_scroller(10 * rb.HZ, table.concat(s_t)) --]]
461
462end --MAIN
463
464main() -- BILGUS