Rockbox Technical Forums
Rockbox Development => Feature Ideas => Topic started by: philden on May 04, 2021, 06:08:04 PM
-
I periodically ponder the random play options in Rockbox. When I use an iPod in original firmware, I do like the option to easily shuffle all songs.
There are options in Rockbox to select 'Random' from the Database 'Artist', 'Album' menus, and others, but not from 'Track'. As far as I can see, the current random play options involve making a non-random playlist, and then shuffling it. That is why to 'shuffle all songs', you first need to make a playlist of all the songs, and then shuffle it. This becomes unwieldy with a large library, and is limited by the maximum playlist size.
I would be happy with the ability to make a random selection of <1,000 songs from the library. I doubt that I would ever get close to playing that number of songs before I next alter the library and would make a new list.
So, I'm wondering if it would be possible to add a random playlist generator of some sort?
Thanks!
-
Adding some more information, as I've been devising a workaround to make the random playlist externally, using a Mac.
I currently organize my music for Rockbox in a dedicated iTunes library, then use Carbon Copy Cloner to copy the music to the iPod (after removing embedded art and replacing with suitably sized cover.bmp files). So I found a way of making a random playlist in iTunes.
1. Make a SmartPlaylist to select a suitable number of songs, less than the Rockbox limit. This is a random selection, but listed in a sorted order. I could put this is Rockbox and then shuffle, but instead:
2. Make a second SmartPlaylist by random selection from the first, with the same number of songs.
3. Export as m3u
4. Put in a text editor and globally replace the file paths to match those on the iPod.
5. Copy to iPod and load into Rockbox.
-
Answering myself again, the best answer lies here - https://forums.rockbox.org/index.php/topic,53990.0.html
However, I would still like the option of a 'real' random play. By which I mean the next track is randomly selected from the whole library, which could even mean repeating the same song. This would go against the 'playlist' principle of Rockbox, unless it is a randomly created one-song playlist.
-
its pretty easy to throw something together..
--[[ Lua RB Random Playlist
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2020 William Wilgus
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
]]
require ("actions")
--require("dbgettags")
local scrpath = rb.current_path()
local max_entries = 500;
local CANCEL_BUTTON = rb.actions.PLA_CANCEL
local sINVALIDDATABASE = "Invalid Database"
local sERROROPENING = "Error opening"
-- tag cache header
local sTCVERSION = string.char(0x10)
local sTCHEADER = string.reverse("TCH" .. sTCVERSION)
local DATASZ = 4 -- int32_t
local TCHSIZE = 3 * DATASZ -- 3 x int32_t
local function bytesLE_n(str)
str = str or ""
local tbyte={str:byte(1, -1)}
local bpos = 1
local num = 0
for k = 1,#tbyte do -- (k = #t, 1, -1 for BE)
num = num + tbyte[k] * bpos
bpos = bpos * 256
end
return num
end
-- uses database files to retrieve database tags
-- adds all unique tags into a lua table
-- ftable is optional
function get_tags(filename, hstr, ftable)
if not filename then return end
if not ftable then ftable = {} end
hstr = hstr or filename
local file = io.open('/' .. filename or "", "r") --read
if not file then rb.splash(100, sERROROPENING .. " " .. filename) return end
local fsz = file:seek("end")
local posln = 0
local tag_len = TCHSIZE
local idx
local function readchrs(count)
if posln >= fsz then return nil end
file:seek("set", posln)
posln = posln + count
return file:read(count)
end
-- check the header and get size + #entries
local tagcache_header = readchrs(DATASZ) or ""
local tagcache_sz = readchrs(DATASZ) or ""
local tagcache_entries = readchrs(DATASZ) or ""
if tagcache_header ~= sTCHEADER or
bytesLE_n(tagcache_sz) ~= (fsz - TCHSIZE) then
rb.splash(100, sINVALIDDATABASE .. " " .. filename)
return
end
-- local tag_entries = bytesLE_n(tagcache_entries)
for k, v in pairs(ftable) do ftable[k] = nil end -- clear table
ftable[1] = hstr
local tline = #ftable + 1
ftable[tline] = ""
local str = ""
while true do
tag_len = bytesLE_n(readchrs(DATASZ))
readchrs(DATASZ) -- idx = bytesLE_n(readchrs(DATASZ))
str = readchrs(tag_len) or ""
str = string.match(str, "(%Z+)%z") -- \0 terminated string
if str and math.random(10) > 5 then
if ftable[tline - 1] ~= str then -- Remove dupes
ftable[tline] = str
tline = tline + 1
if tline >= max_entries then break end
end
elseif posln >= fsz then
break
end
if rb.get_plugin_action(0) == CANCEL_BUTTON then
break
end
end
file:close()
return ftable
end -- get_tags
local function create_playlist()
math.randomseed(rb.current_tick()); -- some kind of randomness
local files = get_tags(rb.ROCKBOX_DIR .. "/database_4.tcd", "",nil)
local fcount = #files
if fcount > 1 then
rb.audio("stop")
rb.playlist("create", scrpath .. "/", "playback.m3u8")
end
for i = 2, fcount - 1 do
repeat
next_item = math.random(2, fcount - 1)
until (files[next_item] ~= "")
rb.playlist("insert_track", string.match(files[next_item], "[^;]+") or "?")
files[next_item] = ""
end
if fcount > 1 then
rb.playlist("start", 0, 0, 0)
end
files = nil -- empty table
end -- create_playlist
local function main()
if not rb.file_exists(rb.ROCKBOX_DIR .. "/database_4.tcd") then
rb.splash(rb.HZ, "Initialize Database")
os.exit(1);
end
rb.splash(10, "Searching for Files..")
collectgarbage("collect")
create_playlist();
collectgarbage("collect")
rb.splash(100, "Goodbye")
end
main() -- BILGUS
problem being that for most there isn't enough ram to load all the tracks in the database
something incremental would have to be made
currently it just (pseudo) randomly grabs tracks and shuffles them randomly and spits out a playlist
as you play around with it..
math.random(10) > 5 make this something less selective for really large databases math.random(1000) > 750
you could probably make it happen in place by looping through the database multiple times with a really selective function
it could even be expanded to take the names into consideration or other database fields (see tagnav lua script)
-
Well I went ahead and made probably exactly what you want
--[[ Lua RB Random Playlist
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2020 William Wilgus
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
]]
require ("actions")
--require("dbgettags")
local scrpath = rb.current_path()
local max_entries = 500;
local CANCEL_BUTTON = rb.actions.PLA_CANCEL
local sINVALIDDATABASE = "Invalid Database"
local sERROROPENING = "Error opening"
-- tag cache header
local sTCVERSION = string.char(0x10)
local sTCHEADER = string.reverse("TCH" .. sTCVERSION)
local DATASZ = 4 -- int32_t
local TCHSIZE = 3 * DATASZ -- 3 x int32_t
local function bytesLE_n(str)
str = str or ""
local tbyte={str:byte(1, -1)}
local bpos = 1
local num = 0
for k = 1,#tbyte do -- (k = #t, 1, -1 for BE)
num = num + tbyte[k] * bpos
bpos = bpos * 256
end
return num
end
-- uses database files to retrieve track file names
-- adds songs at random to new playlist
-- play plays the playlist when done
function create_random_playlist(filename, play)
if not filename then return end
if not play then play = false end
local file = io.open('/' .. filename or "", "r") --read
if not file then rb.splash(100, sERROROPENING .. " " .. filename) return end
local fsz = file:seek("end")
rb.audio("stop")
rb.playlist("create", scrpath .. "/", "random_playback.m3u8")
local posln = 0
local tag_len = TCHSIZE
local idx
local function readchrs(count)
if posln >= fsz then return nil end
file:seek("set", posln)
posln = posln + count
return file:read(count)
end
-- check the header and get size + #entries
local tagcache_header = readchrs(DATASZ) or ""
local tagcache_sz = readchrs(DATASZ) or ""
local tagcache_entries = readchrs(DATASZ) or ""
if tagcache_header ~= sTCHEADER or
bytesLE_n(tagcache_sz) ~= (fsz - TCHSIZE) then
rb.splash(100, sINVALIDDATABASE .. " " .. filename)
return
end
-- local tag_entries = bytesLE_n(tagcache_entries)
local fbegin = file:seek("cur") --Mark the beginning for later loop
local tracks = 0
local str = ""
local rand
local spread = 10;
while true do
tag_len = bytesLE_n(readchrs(DATASZ))
readchrs(DATASZ) -- idx = bytesLE_n(readchrs(DATASZ))
str = readchrs(tag_len) or ""
str = string.match(str, "(%Z+)%z") -- \0 terminated string
rand = math.random(1000)
if str and rand >= (500 - spread) and rand < (500 + spread) then
rb.playlist("insert_track", str)
tracks = tracks + 1
if tracks >= max_entries then break end
elseif posln >= fsz then
if tracks < max_entries then
spread = spread + 10
posln = fbegin -- go through again
else
break
end
end
if rb.get_plugin_action(0) == CANCEL_BUTTON then
break
end
end
file:close()
if tracks == max_entries and play == true then
rb.playlist("start", 0, 0, 0)
end
return ftable
end -- create_playlist
local function main()
if not rb.file_exists(rb.ROCKBOX_DIR .. "/database_4.tcd") then
rb.splash(rb.HZ, "Initialize Database")
os.exit(1);
end
math.randomseed(rb.current_tick()); -- some kind of randomness
rb.splash(10, "Searching for Files..")
collectgarbage("collect")
create_random_playlist(rb.ROCKBOX_DIR .. "/database_4.tcd", true);
collectgarbage("collect")
rb.splash(100, "Goodbye")
end
main() -- BILGUS
Now:
it will go through the database file multiple times if need be and it lowers the selectivity the more times it loops through
you can make max entries as large as you like but 500 songs takes quite a while with my clip zip
-
Wow, thank you very much for doing this, Bilgus.
With a bit of experimentation I got it working. In case anyone else, like me, has no experience of lua files, this is what I did.
I copied the code to a file, named it 'random.lua' and placed it in .rockbox/rocks/demos/lau_scripts
I used the Mac GUI to copy the files and was initially misled by the icon files starting with '.'.
Running the file works perfectly, ending with a 'Goodbye' message, and resuming playback uses the new random playlist.
Thanks again! Phil.
-
Ive cleaned it up made it possible to set num tracks and selectivity mind testing this one out?
--[[ Lua RB Random Playlist --playlist_random.lua
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2021 William Wilgus
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
]]
require ("actions")
--require("dbgettags")
local scrpath = rb.current_path()
local max_tracks = 500;
local min_repeat = 50; --this many songs before a repeat 200k/50 careful with the mem!
local CANCEL_BUTTON = rb.actions.PLA_CANCEL
local OK_BUTTON = rb.actions.PLA_SELECT
local ADD_BUTTON = rb.actions.PLA_UP
local SUB_BUTTON = rb.actions.PLA_DOWN
local sINVALIDDATABASE = "Invalid Database"
local sERROROPENING = "Error opening"
-- tag cache header
local sTCVERSION = string.char(0x10)
local sTCHEADER = string.reverse("TCH" .. sTCVERSION)
local DATASZ = 4 -- int32_t
local TCHSIZE = 3 * DATASZ -- 3 x int32_t
local function bytesLE_n(str)
str = str or ""
local tbyte={str:byte(1, -1)}
local bpos = 1
local num = 0
for k = 1,#tbyte do -- (k = #t, 1, -1 for BE)
num = num + tbyte[k] * bpos
bpos = bpos * 256
end
return num
end
-- Gets size of text
local function text_extent(msg, font)
font = font or rb.FONT_UI
return rb.font_getstringsize(msg, font)
end
function create_random_playlist(filename, play)
if not filename then return end
if not play then play = false end
local file = io.open('/' .. filename or "", "r") --read
if not file then rb.splash(100, sERROROPENING .. " " .. filename) return end
local fsz = file:seek("end")
rb.audio("stop")
rb.playlist("create", scrpath .. "/", "random_playback.m3u8")
local posln = 0
local tag_len = TCHSIZE
local idx
local function readchrs(count)
if posln >= fsz then return nil end
file:seek("set", posln)
posln = posln + count
return file:read(count)
end
-- check the header and get size + #entries
local tagcache_header = readchrs(DATASZ) or ""
local tagcache_sz = readchrs(DATASZ) or ""
local tagcache_entries = readchrs(DATASZ) or ""
if tagcache_header ~= sTCHEADER or
bytesLE_n(tagcache_sz) ~= (fsz - TCHSIZE) then
rb.splash(100, sINVALIDDATABASE .. " " .. filename)
return
end
local entries = bytesLE_n(tagcache_entries)
local fbegin = file:seek("cur") --Mark the beginning for later loop
local tracks = 0
local str = ""
local rand
local rand_range = entries;
local spread = (entries / max_tracks) / 2;
if spread < 1 then
spread = 1
rand_range = max_tracks
end
local res, w, h = text_extent("I")
local max_w = rb.LCD_WIDTH / w
local max_h = rb.LCD_HEIGHT - h
local y = 0
local action
local ask
local t_desc = {scroll = true}
function do_progress_header()
rb.lcd_put_line(1, 0, string.format("%d \\ %d tracks",tracks, max_tracks))
rb.lcd_update()
--rb.sleep(100)
end
function show_progress()
local sdisp = str:match("([^/]+)$") or "?"
sdisp = sdisp:sub(1, max_w)
rb.lcd_put_line(1, y, sdisp);
y = y + h
if y >= max_h then
do_progress_header()
rb.lcd_clear_display()
y = h
end
collectgarbage("collect")
end
function show_setup_header()
rb.lcd_clear_display()
rb.lcd_put_line(1, 0, "Random Playlist", {icon = 9, scroll = true}) -- 9 == ICON_PLUGIN
end
function get_play()
rb.lcd_put_line(1, h, string.format("Play? [%s] (up/dn)", tostring(play)),t_desc)
rb.lcd_put_line(1, h + h, "overwrites dynamic", tdesc)
rb.lcd_update()
action = rb.get_plugin_action(-1);
if action == ADD_BUTTON then
play = true
elseif action == SUB_BUTTON then
play = false
end
end
function get_selectivity()
if spread > max_tracks then spread = 0 end
rb.lcd_put_line(1, h, string.format("selectivity? [%d] (up/dn)",
max_tracks - spread), t_desc)
rb.lcd_put_line(1, h + h, "higher = more selective", tdesc)
rb.lcd_update()
action = rb.get_plugin_action(-1);
if action == SUB_BUTTON then --LESS SELECTIVE
spread = spread + 1
if spread > max_tracks then spread = max_tracks end
elseif action == ADD_BUTTON then
spread = spread - 1
if spread < 0 then spread = 0 end
elseif action == OK_BUTTON then
ask = get_play;
get_selectivity = nil
action = 0
end
end
function get_playlist_size()
rb.lcd_put_line(1, h, string.format("find [%d] tracks? (up/dn)", max_tracks), t_desc)
rb.lcd_update()
action = rb.get_plugin_action(-1);
if action == ADD_BUTTON then
if max_tracks == 1 then max_tracks = 0 end
max_tracks = max_tracks + 50
elseif action == SUB_BUTTON then
max_tracks = max_tracks - 50
if max_tracks < 1 then max_tracks = 1 end
elseif action == OK_BUTTON then
ask = get_selectivity;
get_playlist_size = nil
action = 0
end
end
ask = get_playlist_size; -- \!FIRSTRUN!/
repeat
show_setup_header()
ask()
collectgarbage("collect")
if action == CANCEL_BUTTON then return action end
until (action == OK_BUTTON)
rb.lcd_scroll_stop() -- I'm still weary of not doing this..
rb.splash(10, "Searching for Files..")
if min_repeat > entries then min_repeat = entries / 2 + 1 end
local lastrpt = {}
local irpt = 1
local minspread
local maxspread
local tries
while true do
minspread = tracks - spread
maxspread = tracks + spread
tries = 0
if minspread <= 0 then minspread = 1 end
if maxspread > rand_range then maxspread = rand_range end
rand = math.random(0, rand_range)
while not ((rand >= minspread and rand <= maxspread)) do
posln = math.random(fbegin, fsz)
rand = math.random(0, rand_range)
tries = tries + 1
if tries > fsz then break end
end
local rep = 0
while posln > fbegin do -- need to get to end of a string
str = readchrs(10) or ""
str = string.match(str, "(%Z%Z%Z%Z%Z)%z%Z%Z?%z?%z%z") -- \0 terminated string and size
if str == nil and rep < fsz then
posln = math.random(fbegin + 1, fsz)
rep = rep + 1;
elseif str == nil then
posln = fbegin
else
posln = posln - 4 -- start at next string size entry
break;
end
end
tag_len = bytesLE_n(readchrs(DATASZ))
readchrs(DATASZ) -- idx = bytesLE_n(readchrs(DATASZ))
str = readchrs(tag_len) or ""
str = string.match(str, "(%Z+)%z") -- \0 terminated string
if str ~= nil then -- check for recent repeats
for _, pos in ipairs(lastrpt) do
if pos == posln then
str = nil
break
end
end
end
if str ~= nil then
local lastr = #lastrpt
if lastr > min_repeat then
lastrpt[irpt] = posln
irpt = irpt + 1
if irpt > min_repeat then irpt = 1 end
else
lastrpt[lastr + 1] = posln
end
rb.playlist("insert_track", str)
tracks = tracks + 1
show_progress()
end
if tracks >= max_tracks then
do_progress_header()
break
end
if rb.get_plugin_action(0) == CANCEL_BUTTON then
break
end
end
file:close()
if tracks == max_tracks and play == true then
rb.playlist("start", 0, 0, 0)
end
end -- create_playlist
local function main()
if not rb.file_exists(rb.ROCKBOX_DIR .. "/database_4.tcd") then
rb.splash(rb.HZ, "Initialize Database")
os.exit(1);
end
math.randomseed(rb.current_tick()); -- some kind of randomness
rb.lcd_clear_display()
rb.lcd_update()
collectgarbage("collect")
create_random_playlist(rb.ROCKBOX_DIR .. "/database_4.tcd", true);
collectgarbage("collect")
rb.splash(rb.HZ * 2, "Goodbye")
end
main() -- BILGUS
let me know anything you don't like or get hung up on and i'll push it to main in the next few days
--Edit
added a min repeat buffer I noticed even with all that randomizing songs still bunched upped more than I could stand
I didn't add a setting for it but be aware it eats ram fast keeping arrays full of last played items
-
Thanks Bilgus, very neat work.
-
Thanks again!
This works much better than the first version. I tried more tests of that and found that selecting 500 random tracks from 59,000 never found anything from an artist beyond 'G' in the database, and the list was in alphabetical order.
The new version makes a nice random list and even selecting just 100 tracks goes through the database better. I do have two comments, though. Selecting 'yes' to play works fine. However, selecting 'no' doesn't offer the option to save the playlist. At least that is what I expected choosing 'no' to mean.
What does the value of 'selectivity' mean? My desire is for a totally random selection, should that have a high or low value?
A very minor point is that saving the playlist, while playing, via the context menu, offers the default path as the lua_scripts directory. It would be nice if this went to 'Playlists' instead.
Anyway, I'm very impressed, nice work!
-
@Duceboia, philden thanks I had fun with it
here is what should be the final
unfortunately the plugins get the dynamic playlist buffer and no function is exposed to save it
but, you can go to the playlist catalog long press and view it there
I added warnings made repeat history smaller and configurable
added better descriptions
create /Playlists if it doesn't exist, make default the Playlists/random_playback.m3u8
it also buffers the database file a bit better hopefully thrashing it less
--[[ Lua RB Random Playlist --playlist_random.lua V 0.4
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2021 William Wilgus
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
]]
require ("actions")
local playlistpath = "/Playlists"
local max_tracks = 500;
local min_repeat = 500; --this many songs before a repeat
local random = math.random;
local CANCEL_BUTTON = rb.actions.PLA_CANCEL
local OK_BUTTON = rb.actions.PLA_SELECT
local ADD_BUTTON = rb.actions.PLA_UP
local SUB_BUTTON = rb.actions.PLA_DOWN
local sINVALIDDATABASE = "Invalid Database"
local sERROROPENING = "Error opening"
-- tag cache header
local sTCVERSION = string.char(0x10)
local sTCHEADER = string.reverse("TCH" .. sTCVERSION)
local DATASZ = 4 -- int32_t
local TCHSIZE = 3 * DATASZ -- 3 x int32_t
local function bytesLE_n(str)
str = str or ""
local tbyte={str:byte(1, -1)}
local bpos = 1
local num = 0
for k = 1,#tbyte do -- (k = #t, 1, -1 for BE)
num = num + tbyte[k] * bpos
bpos = bpos * 256
end
return num
end
-- Gets size of text
local function text_extent(msg, font)
font = font or rb.FONT_UI
return rb.font_getstringsize(msg, font)
end
function create_random_playlist(database, playlist, play)
if not database or not database then return end
if not play then play = false end
local file = io.open('/' .. database or "", "r") --read
if not file then rb.splash(100, sERROROPENING .. " " .. database) return end
local fsz = file:seek("end")
local posln = 0
local tag_len = TCHSIZE
local idx
function readchrs(count)
if posln >= fsz then return nil end
file:seek("set", posln)
posln = posln + count
return file:read(count)
end
-- check the header and get size + #entries
local tagcache_header = readchrs(DATASZ) or ""
local tagcache_sz = readchrs(DATASZ) or ""
local tagcache_entries = readchrs(DATASZ) or ""
if tagcache_header ~= sTCHEADER or
bytesLE_n(tagcache_sz) ~= (fsz - TCHSIZE) then
rb.splash(100, sINVALIDDATABASE .. " " .. database)
return
end
local tag_entries = bytesLE_n(tagcache_entries)
local rand_range = tag_entries;
local spread = (tag_entries / max_tracks) / 2;
if spread < 1 then
spread = 1
rand_range = max_tracks
end
local res, w, h = text_extent("I")
local max_w = rb.LCD_WIDTH / w
local max_h = rb.LCD_HEIGHT - h
local y = 0
local action
local ask
local t_desc = {scroll = true}
function ask_user_action(desc, ln1, ln2, ln3)
if ln1 then rb.lcd_put_line(1, h + 5, ln1, desc) end
if ln2 then rb.lcd_put_line(1, h + h + 5, ln2, desc) end
if ln3 then rb.lcd_put_line(1, h + h + h + 5, ln3, desc) end
rb.lcd_hline(1,rb.LCD_WIDTH - 1, h);
rb.lcd_update()
return rb.get_plugin_action(-1); -- Waits for action
end
function show_setup_header()
rb.lcd_clear_display()
rb.lcd_put_line(1, 0, "Random Playlist", {icon = 2, show_icons = true, scroll = true}) -- 2 == Icon_Playlist
end
function get_play()
action = ask_user_action(tdesc,
string.format("Play? [%s] (up/dn)", tostring(play)));
if action == ADD_BUTTON then
play = true
elseif action == SUB_BUTTON then
play = false
end
end
function get_repeat()
if min_repeat > tag_entries then min_repeat = tag_entries end
action = ask_user_action(t_desc,
string.format("Repeat hist? [%d] (up/dn)",min_repeat),
"higher = less repeated songs");
if action == ADD_BUTTON then
min_repeat = min_repeat + 50
elseif action == SUB_BUTTON then -- MORE REPEATS LESS RAM
min_repeat = min_repeat - 50
if min_repeat < 0 then min_repeat = 0 end
elseif action == OK_BUTTON then
ask = get_play;
get_repeat = nil
action = 0
end
end
function get_selectivity()
if spread > max_tracks then spread = 0 end
action = ask_user_action(tdesc,
string.format("Selectivity? [%d] (up/dn)", max_tracks - spread),
"higher = more random");
if action == SUB_BUTTON then -- LESS SELECTIVE
spread = spread + 1
if spread > max_tracks then spread = max_tracks end
elseif action == ADD_BUTTON then
spread = spread - 1
if spread < 0 then spread = 0 end
elseif action == OK_BUTTON then
ask = get_repeat;
get_selectivity = nil
action = 0
end
end
function get_playlist_size()
action = ask_user_action(t_desc,
string.format("Find [%d] tracks? (up/dn)", max_tracks),
"Warning overwrites dynamic playlist",
"Press back to cancel");
if action == ADD_BUTTON then
if max_tracks == 1 then max_tracks = 0 end
max_tracks = max_tracks + 50
elseif action == SUB_BUTTON then
max_tracks = max_tracks - 50
if max_tracks < 1 then max_tracks = 1 end
elseif action == OK_BUTTON then
if max_tracks == 1 then max_tracks = 2 end
ask = get_selectivity;
get_playlist_size = nil
action = 0
end
end
ask = get_playlist_size; -- \!FIRSTRUN!/
repeat
show_setup_header()
ask()
collectgarbage("collect")
if action == CANCEL_BUTTON then return action end
until (action == OK_BUTTON)
rb.lcd_scroll_stop() -- I'm still weary of not doing this..
rb.lcd_clear_display()
rb.audio("stop")
rb.playlist("create", playlistpath .. "/", playlist)
rb.splash(10, "Searching for Files..")
function get_files()
local minspread, maxspread, tries
local fbegin = posln --Mark the beginning for later loop
local tracks = 0
local str = ""
local rand
local t_lru = {}
local lru_widx = 1
local lru_max = min_repeat
if lru_max >= tag_entries then lru_max = tag_entries / 2 + 1 end
function do_progress_header()
rb.lcd_put_line(1, 0, string.format("%d \\ %d tracks",tracks, max_tracks))
rb.lcd_update()
--rb.sleep(300)
end
function show_progress()
local sdisp = str:match("([^/]+)$") or "?"
sdisp = sdisp:sub(1, max_w)
rb.lcd_put_line(1, y, sdisp);
y = y + h
if y >= max_h then
do_progress_header()
rb.lcd_clear_display()
y = h
collectgarbage("collect")
end
end
function check_lru(lru, val)
if lru_max <= 0 or val == nil then return 0 end --user wants all repeats
local lru_ridx = 1
local rv
repeat
rv = lru[lru_ridx]
if val == rv then
return lru_ridx
end
lru_ridx = lru_ridx + 1
until (lru_ridx > lru_max or rv == nil)
return 0
end
function push_lru(lru, val)
lru[lru_widx] = val
lru_widx = lru_widx + 1
if lru_widx > lru_max then lru_widx = 1 end
end
function seek_next_string()
local retry = 0
local data, rem, start, rewind
while posln > fbegin or posln > fsz do -- need to get to beginning of next string
data = readchrs(64) or ""
rem, start = data:match("(%Z%Z%Z%Z%Z)%z(.*)") -- \0 terminated string and next
if (rem == nil or start == nil) and retry < fsz then
if posln > fsz then posln = random(fbegin, fsz) end
retry = retry + 1;
elseif retry > fsz then
posln = fbegin
else
rewind = string.len(start or "")
posln = posln - rewind -- start at next string size entry
break;
end
end
end
local tries
while true do
str = nil
minspread = tracks - spread
maxspread = tracks + spread
tries = 0
if minspread <= 0 then minspread = 1 end
if maxspread > rand_range then maxspread = rand_range end
rand = random(0, rand_range)
while not ((rand >= minspread and rand <= maxspread)) do
rand = random(0, rand_range)
posln = random(fbegin, fsz) + rand
tries = tries + 1
if tries > fsz then break end
end
seek_next_string()
local pos = check_lru(t_lru, posln) -- check for recent repeats
while pos > 0 and posln < fsz do
tag_len = bytesLE_n(readchrs(DATASZ))
-- idx = bytesLE_n(readchrs(DATASZ))
posln = posln + DATASZ + tag_len
pos = check_lru(t_lru, posln) -- check for recent repeats
end
local lpos = posln
tag_len = bytesLE_n(readchrs(DATASZ))
posln = posln + DATASZ -- idx = bytesLE_n(readchrs(DATASZ))
str = readchrs(tag_len) or "\0"
str = str:match("(%Z+)%z$") -- \0 terminated string
if str ~= nil then
push_lru(t_lru, lpos)
rb.playlist("insert_track", str)
tracks = tracks + 1
show_progress()
end
if tracks >= max_tracks then
do_progress_header()
break
end
if rb.get_plugin_action(0) == CANCEL_BUTTON then
break
end
end
end
get_files()
file:close()
if tracks == max_tracks and play == true then
rb.playlist("start", 0, 0, 0)
end
end -- create_playlist
local function main()
if not rb.file_exists(rb.ROCKBOX_DIR .. "/database_4.tcd") then
rb.splash(rb.HZ, "Initialize Database")
os.exit(1);
end
if not rb.dir_exists(playlistpath) then
luadir.mkdir(playlistpath)
end
math.randomseed(rb.current_tick()); -- some kind of randomness
rb.lcd_clear_display()
rb.lcd_update()
collectgarbage("collect")
create_random_playlist(rb.ROCKBOX_DIR .. "/database_4.tcd",
"random_playback.m3u8", true);
rb.splash(rb.HZ * 2, "Goodbye")
end
main() -- BILGUS
-
Works nicely!
I'm still bemused by the concept of more or less random, though!
Thanks very much, Bilgus.
-
sorry I updated it once more it stopped working with a single track in one of the updates
anyway the idea is that it has a large range to pick at random and you can lessen that range at the ends or in the middle
selectivity is that range in the middle
2. the degree to which an electronic circuit or instrument can distinguish particular frequencies.
also consider the database file has a long range of positions that still end up with the same string
it is the whole file path of the track previous.
The code iterates to the next track which now has 5-260?
possible values that could match the random number
this is why it is or was prone to repeats and not so much the rand num generator being bad
-
This is much more complicated than I expected, I'm impressed by your work, Bilgus. I had naively thought that if the database contains 60,000 songs, you'd just need to generate a random number between 1 and 60,000 and play that track.
My limited understanding of the database is that it doesn't involve such a list. Might the above approach be an option from a playlist file that did contain all of the tracks? I don't know if it would be any easier for the device to handle than just playing or shuffling the list, but it could mean indefinite random play including song repeats.
-
philden, thanks It really isn't much more complicated than that and the track file paths are indexed as well but we are kinda going around that whole
process and just reading the database
I wanted to parse the whole thing and index it that was my first try and SLOW
what I have currently works well but its still pretty slow and it's got a bug that garbles the first track from the database
I'll have to get that figured out before I can finish it
-
Now I can safely say that was more involved than I anticipated
I think what I have now checks all the boxes and it is much faster too
https://gerrit.rockbox.org/r/c/rockbox/+/4027
I went with building a sparse index with a lua coroutine that walks to the desired string saving everything on the way
these items may be deleted by the garbage collector under memory pressure and later rebuilt
finally this allows rand(0-ndatabaseentries) and should work with massive playlists
-
I wasn't sure which file to use from the gerrit page, but now 'Random_Playlist' appears in the latest build. Unfortunately os doesn't work for me.
First I tried 100 songs with the randomizer set to 0, this crashes with a Panic. Same settings but with a randomizer of 1 hung up with the message "Removing Dynamic Playlist". Accepting the default options crashed with a Panic.
This is all on a 7th Gen iPod with iFlash Quad, 2TB storage and around 60,000 songs.
When I go to 'resume playback', the previous Dynamic plalylist is still loaded, from before any of these tests.
-
well that is annoying development, can you try commenting out line 405?
change rb.playlist("remove_all_tracks")
to
--rb.playlist("remove_all_tracks")
-
I just tried this, selected 100 tracks but defaults otherwise, and it crashed with:
*PANIC*
playlist_create() : OOM
pc:080118b4 sp:00009ca4
A: 0bf915rc
A: 0bf835b8
A: 0bf8fbc0
bt end
-
ok one last try can you try deleting the file playlists/playlists_random.m3u8 if it exists, set to a single song so it removes the current playlist
here is what I uploaded today
--[[ Lua RB Random Playlist -- random_playlist.lua V 1.0
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2021 William Wilgus
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
]]
require ("actions")
require("dbgettags")
get_tags = nil -- unneeded
-- User defaults
local playlistpath = "/Playlists"
local max_tracks = 500; -- size of playlist to create
local min_repeat = 500; -- this many songs before a repeat
local play_on_success = true;
-- Random integer function
local random = math.random; -- ref random(min, max)
math.randomseed(rb.current_tick()); -- some kind of randomness
-- Button definitions
local CANCEL_BUTTON = rb.actions.PLA_CANCEL
local OK_BUTTON = rb.actions.PLA_SELECT
local ADD_BUTTON = rb.actions.PLA_UP
local ADD_BUTTON_RPT = rb.actions.PLA_UP_REPEAT or ADD_BUTTON
local SUB_BUTTON = rb.actions.PLA_DOWN
local SUB_BUTTON_RPT = rb.actions.PLA_DOWN_REPEAT or SUB_BUTTON
-- remove action and context tables to free some ram
rb.actions = nil
rb.contexts = nil
-- Program strings
local sINITDATABASE = "Initialize Database"
local sHEADERTEXT = "Random Playlist"
local sPLAYLISTERROR = "Playlist Error!"
local sREMOVEPLAYLIST = "Removing Dynamic Playlist"
local sSEARCHINGFILES = "Searching for Files.."
local sERROROPENFMT = "Error Opening %s"
local sINVALIDDBFMT = "Invalid Database %s"
local sPROGRESSHDRFMT = "%d \\ %d Tracks"
local sGOODBYE = "Goodbye"
-- Gets size of text
local function text_extent(msg, font)
font = font or rb.FONT_UI
return rb.font_getstringsize(msg, font)
end
local function _setup_random_playlist(tag_entries, play, min_repeat, trackcount)
-- Setup string tables
local tPLAYTEXT = {"Play? [ %s ] (up/dn)", "true = play tracks on success"}
local tREPEATTEXT = {"Repeat hist? [ %d ] (up/dn)","higher = less repeated songs"}
local tPLSIZETEXT = {"Find [ %d ] tracks? (up/dn)",
"Warning overwrites dynamic playlist",
"Press back to cancel"};
-- how many lines can we fit on the screen?
local res, w, h = text_extent("I")
h = h + 5 -- increase spacing in the setup menu
local max_w = rb.LCD_WIDTH / w
local max_h = rb.LCD_HEIGHT - h
local y = 0
-- User Setup Menu
local action, ask, increment
local t_desc = {scroll = true} -- scroll the setup items
-- Clears screen and adds title and icon, called first..
function show_setup_header()
local desc = {icon = 2, show_icons = true, scroll = true} -- 2 == Icon_Playlist
rb.lcd_clear_display()
rb.lcd_put_line(1, 0, sHEADERTEXT, desc)
end
-- Display up to 3 items and waits for user action -- returns action
function ask_user_action(desc, ln1, ln2, ln3)
if ln1 then rb.lcd_put_line(1, h, ln1, desc) end
if ln2 then rb.lcd_put_line(1, h + h, ln2, desc) end
if ln3 then rb.lcd_put_line(1, h + h + h, ln3, desc) end
rb.lcd_hline(1,rb.LCD_WIDTH - 1, h - 5);
rb.lcd_update()
local act = rb.get_plugin_action(-1); -- Blocking wait for action
-- handle magnitude of the increment here so consumer fn doesn't need to
if act == ADD_BUTTON_RPT and act ~= ADD_BUTTON then
increment = increment + 1
if increment > 1000 then increment = 1000 end
act = ADD_BUTTON
elseif act == SUB_BUTTON_RPT and act ~= SUB_BUTTON then
increment = increment + 1
if increment > 1000 then increment = 1000 end
act = SUB_BUTTON
else
increment = 1;
end
return act
end
-- Play the playlist on successful completion true/false?
function setup_get_play()
action = ask_user_action(tdesc,
string.format(tPLAYTEXT[1], tostring(play)),
tPLAYTEXT[2]);
if action == ADD_BUTTON then
play = true
elseif action == SUB_BUTTON then
play = false
end
end
-- Repeat song buffer list of previously added tracks 0-??
function setup_get_repeat()
if min_repeat >= trackcount then min_repeat = trackcount - 1 end
if min_repeat >= tag_entries then min_repeat = tag_entries - 1 end
action = ask_user_action(t_desc,
string.format(tREPEATTEXT[1],min_repeat),
tREPEATTEXT[2]);
if action == ADD_BUTTON then
min_repeat = min_repeat + increment
elseif action == SUB_BUTTON then -- MORE REPEATS LESS RAM USED
if min_repeat < increment then increment = 1 end
min_repeat = min_repeat - increment
if min_repeat < 0 then min_repeat = 0 end
elseif action == OK_BUTTON then
ask = setup_get_play;
setup_get_repeat = nil
action = 0
end
end
-- How many tracks to find
function setup_get_playlist_size()
action = ask_user_action(t_desc,
string.format(tPLSIZETEXT[1], trackcount),
tPLSIZETEXT[2],
tPLSIZETEXT[3]);
if action == ADD_BUTTON then
trackcount = trackcount + increment
elseif action == SUB_BUTTON then
if trackcount < increment then increment = 1 end
trackcount = trackcount - increment
if trackcount < 1 then trackcount = 1 end
elseif action == OK_BUTTON then
ask = setup_get_repeat;
setup_get_playlist_size = nil
action = 0
end
end
ask = setup_get_playlist_size; -- \!FIRSTRUN!/
repeat -- SETUP MENU LOOP
show_setup_header()
ask()
rb.lcd_scroll_stop() -- I'm still wary of not doing this..
collectgarbage("collect")
if action == CANCEL_BUTTON then rb.lcd_scroll_stop(); return nil end
until (action == OK_BUTTON)
return play, min_repeat, trackcount;
end
--[[ Given the filenameDB file [database]
creates a random dynamic playlist with a default savename of [playlist]
containing [trackcount] tracks, played on completion if [play] is true]]
function create_random_playlist(database, playlist, trackcount, play)
if not database or not playlist or not trackcount then return end
if not play then play = false end
local file = io.open('/' .. database or "", "r") --read
if not file then rb.splash(100, string.format(sERROROPENFMT, database)) return end
local fsz = file:seek("end")
local fbegin
local posln = 0
local tag_len = TCHSIZE
local anchor_index
local ANCHOR_INTV
local track_index = setmetatable({},{__mode = "v"}) --[[ weak table values
this allows them to be garbage collected as space is needed / rebuilt as needed ]]
-- Read character function sets posln as file position
function readchrs(count)
if posln >= fsz then return nil end
file:seek("set", posln)
posln = posln + count
return file:read(count)
end
-- Check the header and get size + #entries
local tagcache_header = readchrs(DATASZ) or ""
local tagcache_sz = readchrs(DATASZ) or ""
local tagcache_entries = readchrs(DATASZ) or ""
if tagcache_header ~= sTCHEADER or
bytesLE_n(tagcache_sz) ~= (fsz - TCHSIZE) then
rb.splash(100, string.format(sINVALIDDBFMT, database))
return
end
local tag_entries = bytesLE_n(tagcache_entries)
play, min_repeat, trackcount = _setup_random_playlist(
tag_entries, play, min_repeat, trackcount);
_setup_random_playlist = nil
collectgarbage("collect")
-- how many lines can we fit on the screen?
local res, w, h = text_extent("I")
local max_w = rb.LCD_WIDTH / w
local max_h = rb.LCD_HEIGHT - h
local y = 0
rb.lcd_clear_display()
function get_tracks_random()
local tries, idxp
local tracks = 0
local str = ""
local t_lru = {}
local lru_widx = 1
local lru_max = min_repeat
if lru_max >= tag_entries then lru_max = tag_entries / 2 + 1 end
function do_progress_header()
rb.lcd_put_line(1, 0, string.format(sPROGRESSHDRFMT,tracks, trackcount))
rb.lcd_update()
--rb.sleep(300)
end
function show_progress()
local sdisp = str:match("([^/]+)$") or "?" --just the track name
rb.lcd_put_line(1, y, sdisp:sub(1, max_w));-- limit string length
y = y + h
if y >= max_h then
do_progress_header()
rb.lcd_clear_display()
rb.yield()
y = h
end
end
-- check for repeated tracks
function check_lru(val)
if lru_max <= 0 or val == nil then return 0 end --user wants all repeats
local rv
local i = 1
repeat
rv = t_lru[i]
if rv == nil then
break;
elseif rv == val then
return i
end
i = i + 1
until (i == lru_max)
return 0
end
-- add a track to the repeat list (overwrites oldest if full)
function push_lru(val)
t_lru[lru_widx] = val
lru_widx = lru_widx + 1
if lru_widx > lru_max then lru_widx = 1 end
end
function get_index()
if ANCHOR_INTV > 1 then
get_index =
function(plidx)
local p = track_index[plidx]
if p == nil then
parse_database_offsets(plidx)
end
return track_index[plidx][1]
end
else -- all tracks are indexed
get_index =
function(plidx)
return track_index[plidx]
end
end
end
get_index() --init get_index fn
-- Playlist insert loop
while true do
str = nil
tries = 0
repeat
idxp = random(1, tag_entries)
tries = tries + 1 -- prevent endless loops
until check_lru(idxp) == 0 or tries > fsz -- check for recent repeats
posln = get_index(idxp)
tag_len = bytesLE_n(readchrs(DATASZ))
posln = posln + DATASZ -- idx = bytesLE_n(readchrs(DATASZ))
str = readchrs(tag_len) or "\0" -- Read the database string
str = str:match("^(%Z+)%z$") -- \0 terminated string
-- Insert track into playlist
if str ~= nil then
tracks = tracks + 1
show_progress()
push_lru(idxp) -- add to repeat list
if rb.playlist("insert_track", str) < 0 then
rb.splash(rb.HZ, sPLAYLISTERROR)
break; -- ERROR, PLAYLIST FULL?
end
end
if tracks >= trackcount then
do_progress_header()
break
end
-- check for cancel non-blocking
if rb.get_plugin_action(0) == CANCEL_BUTTON then
break
end
end
end -- get_files
function build_anchor_index()
-- index every n files
ANCHOR_INTV = 1 -- for small db we can put all the entries in ram
local ent = tag_entries / 1000 -- more than 10,000 will be incrementally loaded
while ent >= 10 do -- need to reduce the size of the anchor index?
ent = ent / 10
ANCHOR_INTV = ANCHOR_INTV * 10
end -- should be power of 10 (10, 100, 1000..)
--grab an index for every ANCHOR_INTV entries
local aidx={}
local acount = 0
local next_idx = 1
local index = 1
local tlen
if ANCHOR_INTV == 1 then acount = 1 end
while index <= tag_entries and posln < fsz do
if next_idx == index then
acount = acount + 1
next_idx = acount * ANCHOR_INTV
aidx[index] = posln
else -- fill the weak table, we already did the work afterall
track_index[index] = {posln} -- put vals inside table to make them collectable
end
index = index + 1
tlen = bytesLE_n(readchrs(DATASZ))
posln = posln + tlen + DATASZ
end
return aidx
end
function parse_database_offsets(plidx)
local tlen
-- round to nearest anchor entry that is less than plidx
local aidx = (plidx / ANCHOR_INTV) * ANCHOR_INTV
local cidx = aidx
track_index[cidx] = {anchor_index[aidx] or fbegin};
-- maybe we can use previous work to get closer to the desired offset
while track_index[cidx] ~= nil and cidx <= plidx do
cidx = cidx + 1 --keep seeking till we find an empty entry
end
posln = track_index[cidx - 1][1]
while cidx <= plidx do --[[ walk the remaining entries from the last known
& save the entries on the way to our desired entry ]]
tlen = bytesLE_n(readchrs(DATASZ))
posln = posln + tlen + DATASZ
track_index[cidx] = {posln} -- put vals inside table to make them collectable
if posln >= fsz then posln = fbegin end
cidx = cidx + 1
end
end
if trackcount ~= nil then
rb.splash(10, sSEARCHINGFILES)
fbegin = posln --Mark the beginning for later loops
tag_len = 0
anchor_index = build_anchor_index() -- index track offsets
if ANCHOR_INTV == 1 then
-- all track indexes are in ram
track_index = anchor_index
anchor_index = nil
end
rb.splash(10, sREMOVEPLAYLIST)
rb.audio("stop")
os.remove( playlistpath .. "/" .. playlist)
--rb.playlist("remove_all_tracks")
rb.playlist("create", playlistpath .. "/", playlist)
--[[ --profiling
local starttime = rb.current_tick();
get_tracks_random()
local endtime = rb.current_tick();
rb.splash(1000, (endtime - starttime) .. " ticks");
end
if (false) then
--]]
get_tracks_random()
end
file:close()
collectgarbage("collect")
if trackcount and rb.playlist("amount") >= trackcount and play == true then
rb.playlist("start", 0, 0, 0)
end
end -- create_playlist
local function main()
if not rb.file_exists(rb.ROCKBOX_DIR .. "/database_4.tcd") then
rb.splash(rb.HZ, sINITDATABASE)
os.exit(1);
end
if rb.cpu_boost then rb.cpu_boost(true) end
rb.backlight_force_on()
if not rb.dir_exists(playlistpath) then
luadir.mkdir(playlistpath)
end
rb.lcd_clear_display()
rb.lcd_update()
collectgarbage("collect")
create_random_playlist(rb.ROCKBOX_DIR .. "/database_4.tcd",
"random_playback.m3u8", max_tracks, play_on_success);
rb.splash(rb.HZ * 2, sGOODBYE)
-- Restore user backlight settings
rb.backlight_use_settings()
if rb.cpu_boost then rb.cpu_boost(false) end
--[[
local used, allocd, free = rb.mem_stats()
local lu = collectgarbage("count")
local fmt = function(t, v) return string.format("%s: %d Kb\n", t, v /1024) end
-- this is how lua recommends to concat strings rather than ..
local s_t = {}
s_t[1] = "rockbox:\n"
s_t[2] = fmt("Used ", used)
s_t[3] = fmt("Allocd ", allocd)
s_t[4] = fmt("Free ", free)
s_t[5] = "\nlua:\n"
s_t[6] = fmt("Used", lu * 1024)
s_t[7] = "\n\nNote that the rockbox used count is a high watermark"
rb.splash_scroller(10 * rb.HZ, table.concat(s_t)) --]]
end --MAIN
main() -- BILGUS
And finally can you send me your database_4.tcd if the above doesn't work
this is why I initially went for such hoops of reading the file incrementally but AFAICT it actually uses less ram
I think the last option is to write my own playlist manually and load it from disk instead
-
The playlists/playlists_random.m3u8 did not exist. I loaded a single track as a playlist, then tried your new script. It crashed with similar 'Panic' message.
My database_4.tcd is 5.3MB, how would you like me to send it?
-
Filesharing is fine or attach it here or msg me and I'll give you my mail
-
I just pushed an update that should at least allow you to generate the playlist, hopefully the reduced memory pressure will allow it but
I suspect that lua takes over your audio buffer and the playlist is denied the ram it needs
If this ends up being the case well at least it can be started manually
I might be able to dump the lua instance and restart lua to free ram then load the playlist, I still think its worth
it for the speed increase and reduce disk reading but i'm beginning to wonder
-
Ok after trying your database file it is indeed what I suspected but get this
it was the anchor index being too large so instead I made the index even more sparse
could you try this on your device?
--[[ Lua RB Random Playlist -- random_playlist.lua V 1.0
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2021 William Wilgus
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
]]
require ("actions")
require("dbgettags")
get_tags = nil -- unneeded
-- User defaults
local playlistpath = "/Playlists"
local max_tracks = 500; -- size of playlist to create
local min_repeat = 500; -- this many songs before a repeat
local play_on_success = true;
--program vars
local playlist_handle
local t_playlistbuf -- table for playlist write buffer
-- Random integer function
local random = math.random; -- ref random(min, max)
math.randomseed(rb.current_tick()); -- some kind of randomness
-- Button definitions
local CANCEL_BUTTON = rb.actions.PLA_CANCEL
local OK_BUTTON = rb.actions.PLA_SELECT
local ADD_BUTTON = rb.actions.PLA_UP
local ADD_BUTTON_RPT = rb.actions.PLA_UP_REPEAT or ADD_BUTTON
local SUB_BUTTON = rb.actions.PLA_DOWN
local SUB_BUTTON_RPT = rb.actions.PLA_DOWN_REPEAT or SUB_BUTTON
-- remove action and context tables to free some ram
rb.actions = nil
rb.contexts = nil
-- Program strings
local sINITDATABASE = "Initialize Database"
local sHEADERTEXT = "Random Playlist"
local sPLAYLISTERROR = "Playlist Error!"
local sSEARCHINGFILES = "Searching for Files.."
local sERROROPENFMT = "Error Opening %s"
local sINVALIDDBFMT = "Invalid Database %s"
local sPROGRESSHDRFMT = "%d \\ %d Tracks"
local sGOODBYE = "Goodbye"
-- Gets size of text
local function text_extent(msg, font)
font = font or rb.FONT_UI
return rb.font_getstringsize(msg, font)
end
local function _setup_random_playlist(tag_entries, play, min_repeat, trackcount)
-- Setup string tables
local tPLAYTEXT = {"Play? [ %s ] (up/dn)", "true = play tracks on success"}
local tREPEATTEXT = {"Repeat hist? [ %d ] (up/dn)","higher = less repeated songs"}
local tPLSIZETEXT = {"Find [ %d ] tracks? (up/dn)",
"Warning overwrites dynamic playlist",
"Press back to cancel"};
-- how many lines can we fit on the screen?
local res, w, h = text_extent("I")
h = h + 5 -- increase spacing in the setup menu
local max_w = rb.LCD_WIDTH / w
local max_h = rb.LCD_HEIGHT - h
local y = 0
-- User Setup Menu
local action, ask, increment
local t_desc = {scroll = true} -- scroll the setup items
-- Clears screen and adds title and icon, called first..
function show_setup_header()
local desc = {icon = 2, show_icons = true, scroll = true} -- 2 == Icon_Playlist
rb.lcd_clear_display()
rb.lcd_put_line(1, 0, sHEADERTEXT, desc)
end
-- Display up to 3 items and waits for user action -- returns action
function ask_user_action(desc, ln1, ln2, ln3)
if ln1 then rb.lcd_put_line(1, h, ln1, desc) end
if ln2 then rb.lcd_put_line(1, h + h, ln2, desc) end
if ln3 then rb.lcd_put_line(1, h + h + h, ln3, desc) end
rb.lcd_hline(1,rb.LCD_WIDTH - 1, h - 5);
rb.lcd_update()
local act = rb.get_plugin_action(-1); -- Blocking wait for action
-- handle magnitude of the increment here so consumer fn doesn't need to
if act == ADD_BUTTON_RPT and act ~= ADD_BUTTON then
increment = increment + 1
if increment > 1000 then increment = 1000 end
act = ADD_BUTTON
elseif act == SUB_BUTTON_RPT and act ~= SUB_BUTTON then
increment = increment + 1
if increment > 1000 then increment = 1000 end
act = SUB_BUTTON
else
increment = 1;
end
return act
end
-- Play the playlist on successful completion true/false?
function setup_get_play()
action = ask_user_action(tdesc,
string.format(tPLAYTEXT[1], tostring(play)),
tPLAYTEXT[2]);
if action == ADD_BUTTON then
play = true
elseif action == SUB_BUTTON then
play = false
end
end
-- Repeat song buffer list of previously added tracks 0-??
function setup_get_repeat()
if min_repeat >= trackcount then min_repeat = trackcount - 1 end
if min_repeat >= tag_entries then min_repeat = tag_entries - 1 end
action = ask_user_action(t_desc,
string.format(tREPEATTEXT[1],min_repeat),
tREPEATTEXT[2]);
if action == ADD_BUTTON then
min_repeat = min_repeat + increment
elseif action == SUB_BUTTON then -- MORE REPEATS LESS RAM USED
if min_repeat < increment then increment = 1 end
min_repeat = min_repeat - increment
if min_repeat < 0 then min_repeat = 0 end
elseif action == OK_BUTTON then
ask = setup_get_play;
setup_get_repeat = nil
action = 0
end
end
-- How many tracks to find
function setup_get_playlist_size()
action = ask_user_action(t_desc,
string.format(tPLSIZETEXT[1], trackcount),
tPLSIZETEXT[2],
tPLSIZETEXT[3]);
if action == ADD_BUTTON then
trackcount = trackcount + increment
elseif action == SUB_BUTTON then
if trackcount < increment then increment = 1 end
trackcount = trackcount - increment
if trackcount < 1 then trackcount = 1 end
elseif action == OK_BUTTON then
ask = setup_get_repeat;
setup_get_playlist_size = nil
action = 0
end
end
ask = setup_get_playlist_size; -- \!FIRSTRUN!/
repeat -- SETUP MENU LOOP
show_setup_header()
ask()
rb.lcd_scroll_stop() -- I'm still wary of not doing this..
collectgarbage("collect")
if action == CANCEL_BUTTON then rb.lcd_scroll_stop(); return nil end
until (action == OK_BUTTON)
return play, min_repeat, trackcount;
end
--deletes existing file and creates a new playlist
local function playlist_create(filename)
local filehandle = io.open(filename, "w+") --overwrite
if not filehandle then
rb.splash(rb.HZ, "Error opening " .. filename)
return false
end
t_playlistbuf = {}
filehandle:write("\239\187\191") -- Write BOM --"\xEF\xBB\xBF"
playlist_handle = filehandle
return true
--os.remove( playlistpath .. "/" .. playlist)
--rb.playlist("remove_all_tracks")
--rb.playlist("create", playlistpath .. "/", playlist)
end
-- writes track path to a buffer must be flushed
local function playlist_write(trackpath)
t_playlistbuf[#t_playlistbuf + 1] = trackpath
t_playlistbuf[#t_playlistbuf + 1] = "\n"
--[[if rb.playlist("insert_track", str) < 0 then
rb.splash(rb.HZ, sPLAYLISTERROR)
break; -- ERROR, PLAYLIST FULL?
end]]
end
-- flushes playlist buffer to file
local function playlist_flush()
playlist_handle:write(table.concat(t_playlistbuf))
t_playlistbuf = {}
--[[if rb.playlist("insert_track", str) < 0 then
rb.splash(rb.HZ, sPLAYLISTERROR)
break; -- ERROR, PLAYLIST FULL?
end]]
end
-- closes playlist file descriptor
local function playlist_finalize()
playlist_handle:close()
end
--[[ Given the filenameDB file [database]
creates a random dynamic playlist with a default savename of [playlist]
containing [trackcount] tracks, played on completion if [play] is true]]
local function create_random_playlist(database, playlist, trackcount, play)
if not database or not playlist or not trackcount then return end
if not play then play = false end
local playlist_handle
local file = io.open('/' .. database or "", "r") --read
if not file then rb.splash(100, string.format(sERROROPENFMT, database)) return end
local fsz = file:seek("end")
local fbegin
local posln = 0
local tag_len = TCHSIZE
local anchor_index
local ANCHOR_INTV
local track_index = setmetatable({},{__mode = "v"}) --[[ weak table values
this allows them to be garbage collected as space is needed / rebuilt as needed ]]
-- Read character function sets posln as file position
function readchrs(count)
if posln >= fsz then return nil end
file:seek("set", posln)
posln = posln + count
return file:read(count)
end
-- Check the header and get size + #entries
local tagcache_header = readchrs(DATASZ) or ""
local tagcache_sz = readchrs(DATASZ) or ""
local tagcache_entries = readchrs(DATASZ) or ""
if tagcache_header ~= sTCHEADER or
bytesLE_n(tagcache_sz) ~= (fsz - TCHSIZE) then
rb.splash(100, string.format(sINVALIDDBFMT, database))
return
end
local tag_entries = bytesLE_n(tagcache_entries)
--if tag_entries > 50000 then play = false end
play, min_repeat, trackcount = _setup_random_playlist(
tag_entries, play, min_repeat, trackcount);
_setup_random_playlist = nil
collectgarbage("collect")
-- how many lines can we fit on the screen?
local res, w, h = text_extent("I")
local max_w = rb.LCD_WIDTH / w
local max_h = rb.LCD_HEIGHT - h
local y = 0
rb.lcd_clear_display()
function get_tracks_random()
local tries, idxp
local tracks = 0
local str = ""
local t_lru = {}
local lru_widx = 1
local lru_max = min_repeat
if lru_max >= tag_entries then lru_max = tag_entries / 2 + 1 end
function do_progress_header()
rb.lcd_put_line(1, 0, string.format(sPROGRESSHDRFMT,tracks, trackcount))
rb.lcd_update()
--rb.sleep(300)
end
function show_progress()
local sdisp = str:match("([^/]+)$") or "?" --just the track name
rb.lcd_put_line(1, y, sdisp:sub(1, max_w));-- limit string length
y = y + h
if y >= max_h then
do_progress_header()
rb.lcd_clear_display()
playlist_flush(playlist_handle)
rb.yield()
y = h
end
end
-- check for repeated tracks
function check_lru(val)
if lru_max <= 0 or val == nil then return 0 end --user wants all repeats
local rv
local i = 1
repeat
rv = t_lru[i]
if rv == nil then
break;
elseif rv == val then
return i
end
i = i + 1
until (i == lru_max)
return 0
end
-- add a track to the repeat list (overwrites oldest if full)
function push_lru(val)
t_lru[lru_widx] = val
lru_widx = lru_widx + 1
if lru_widx > lru_max then lru_widx = 1 end
end
function get_index()
if ANCHOR_INTV > 1 then
get_index =
function(plidx)
local p = track_index[plidx]
if p == nil then
parse_database_offsets(plidx)
end
return track_index[plidx][1]
end
else -- all tracks are indexed
get_index =
function(plidx)
return track_index[plidx]
end
end
end
get_index() --init get_index fn
-- Playlist insert loop
while true do
str = nil
tries = 0
repeat
idxp = random(1, tag_entries)
tries = tries + 1 -- prevent endless loops
until check_lru(idxp) == 0 or tries > fsz -- check for recent repeats
posln = get_index(idxp)
tag_len = bytesLE_n(readchrs(DATASZ))
posln = posln + DATASZ -- idx = bytesLE_n(readchrs(DATASZ))
str = readchrs(tag_len) or "\0" -- Read the database string
str = str:match("^(%Z+)%z$") -- \0 terminated string
-- Insert track into playlist
if str ~= nil then
tracks = tracks + 1
show_progress()
push_lru(idxp) -- add to repeat list
playlist_write(str)
end
if tracks >= trackcount then
playlist_flush()
do_progress_header()
break
end
-- check for cancel non-blocking
if rb.get_plugin_action(0) == CANCEL_BUTTON then
break
end
end
end -- get_files
function build_anchor_index()
-- index every n files
ANCHOR_INTV = 1 -- for small db we can put all the entries in ram
local ent = tag_entries / 100 -- more than 1000 will be incrementally loaded
while ent >= 10 do -- need to reduce the size of the anchor index?
ent = ent / 10
ANCHOR_INTV = ANCHOR_INTV * 10
end -- should be power of 10 (10, 100, 1000..)
--grab an index for every ANCHOR_INTV entries
local aidx={}
local acount = 0
local next_idx = 1
local index = 1
local tlen
if ANCHOR_INTV == 1 then acount = 1 end
while index <= tag_entries and posln < fsz do
if next_idx == index then
acount = acount + 1
next_idx = acount * ANCHOR_INTV
aidx[index] = posln
else -- fill the weak table, we already did the work afterall
track_index[index] = {posln} -- put vals inside table to make them collectable
end
index = index + 1
tlen = bytesLE_n(readchrs(DATASZ))
posln = posln + tlen + DATASZ
end
return aidx
end
function parse_database_offsets(plidx)
local tlen
-- round to nearest anchor entry that is less than plidx
local aidx = (plidx / ANCHOR_INTV) * ANCHOR_INTV
local cidx = aidx
track_index[cidx] = {anchor_index[aidx] or fbegin};
-- maybe we can use previous work to get closer to the desired offset
while track_index[cidx] ~= nil and cidx <= plidx do
cidx = cidx + 1 --keep seeking till we find an empty entry
end
posln = track_index[cidx - 1][1]
while cidx <= plidx do --[[ walk the remaining entries from the last known
& save the entries on the way to our desired entry ]]
tlen = bytesLE_n(readchrs(DATASZ))
posln = posln + tlen + DATASZ
track_index[cidx] = {posln} -- put vals inside table to make them collectable
if posln >= fsz then posln = fbegin end
cidx = cidx + 1
end
end
if trackcount ~= nil then
rb.splash(10, sSEARCHINGFILES)
fbegin = posln --Mark the beginning for later loops
tag_len = 0
anchor_index = build_anchor_index() -- index track offsets
if ANCHOR_INTV == 1 then
-- all track indexes are in ram
track_index = anchor_index
anchor_index = nil
end
if not playlist_create(playlistpath .. "/" .. playlist) then return end
--[[ --profiling
local starttime = rb.current_tick();
get_tracks_random()
local endtime = rb.current_tick();
rb.splash(1000, (endtime - starttime) .. " ticks");
end
if (false) then
--]]
get_tracks_random()
playlist_finalize(playlist_handle)
end
file:close()
collectgarbage("collect")
if trackcount and play == true then
rb.audio("stop")
rb.yield()
rb.playlist("create", playlistpath .. "/", "dynamic_playlist.m3u8")
rb.playlist("insert_playlist", playlistpath .. "/" .. playlist)
rb.playlist("start", 0, 0, 0)
end
end -- playlist_create
local function main()
if not rb.file_exists(rb.ROCKBOX_DIR .. "/database_4.tcd") then
rb.splash(rb.HZ, sINITDATABASE)
os.exit(1);
end
if rb.cpu_boost then rb.cpu_boost(true) end
rb.backlight_force_on()
if not rb.dir_exists(playlistpath) then
luadir.mkdir(playlistpath)
end
rb.lcd_clear_display()
rb.lcd_update()
collectgarbage("collect")
--rb.ROCKBOX_DIR = "" -- TESTING!!
create_random_playlist(rb.ROCKBOX_DIR .. "/database_4.tcd",
"random_playback.m3u8", max_tracks, play_on_success);
rb.splash(rb.HZ * 2, sGOODBYE)
-- Restore user backlight settings
rb.backlight_use_settings()
if rb.cpu_boost then rb.cpu_boost(false) end
--[[
local used, allocd, free = rb.mem_stats()
local lu = collectgarbage("count")
local fmt = function(t, v) return string.format("%s: %d Kb\n", t, v /1024) end
-- this is how lua recommends to concat strings rather than ..
local s_t = {}
s_t[1] = "rockbox:\n"
s_t[2] = fmt("Used ", used)
s_t[3] = fmt("Allocd ", allocd)
s_t[4] = fmt("Free ", free)
s_t[5] = "\nlua:\n"
s_t[6] = fmt("Used", lu * 1024)
s_t[7] = "\n\nNote that the rockbox used count is a high watermark"
rb.splash_scroller(10 * rb.HZ, table.concat(s_t)) --]]
end --MAIN
main() -- BILGUS
-
SO I can enable the adding tracks to playlist in-situ again and now even give the option to save it to disk --
But..
I added a patch a long time ago to lua that allows it to do an emergency garbage collection before taking over the playback buffer
what I think I need to do is give a way to check if that has occurred
from lua that way we can disable things that we know won't run after we take the playback buffer
-
I'm afraid this one fails with "Error opening /database_4.tcd"
-
Try again I forgot to comment out a line for testing
or copy your db4.tcd file to the root of your drive..
-
This one nearly works! I named it random_playlist6.lua and kept the default options. The list of 500 tracks scrolled by on the screen, but was replaced by the attached error message.
-
Well you know it will work if you grab the latest dev version :P
-
I also now have a version ready that lets you choose to save to disk or use the inbuilt rockbox playlist_insert functions
--[[ Lua RB Random Playlist -- random_playlist.lua V 1.0
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2021 William Wilgus
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
]]
require ("actions")
require("dbgettags")
get_tags = nil -- unneeded
-- User defaults
local playlistpath = "/Playlists"
local max_tracks = 500; -- size of playlist to create
local min_repeat = 500; -- this many songs before a repeat
local play_on_success = true;
local playlist_name = "random_playback.m3u8"
--program vars
local playlist_handle
local t_playlistbuf -- table for playlist write buffer
-- Random integer function
local random = math.random; -- ref random(min, max)
math.randomseed(rb.current_tick()); -- some kind of randomness
-- Button definitions
local CANCEL_BUTTON = rb.actions.PLA_CANCEL
local OK_BUTTON = rb.actions.PLA_SELECT
local ADD_BUTTON = rb.actions.PLA_UP
local ADD_BUTTON_RPT = rb.actions.PLA_UP_REPEAT or ADD_BUTTON
local SUB_BUTTON = rb.actions.PLA_DOWN
local SUB_BUTTON_RPT = rb.actions.PLA_DOWN_REPEAT or SUB_BUTTON
-- remove action and context tables to free some ram
rb.actions = nil
rb.contexts = nil
-- Program strings
local sINITDATABASE = "Initialize Database"
local sHEADERTEXT = "Random Playlist"
local sPLAYLISTERROR = "Playlist Error!"
local sSEARCHINGFILES = "Searching for Files.."
local sERROROPENFMT = "Error Opening %s"
local sINVALIDDBFMT = "Invalid Database %s"
local sPROGRESSHDRFMT = "%d \\ %d Tracks"
local sGOODBYE = "Goodbye"
-- Gets size of text
local function text_extent(msg, font)
font = font or rb.FONT_UI
return rb.font_getstringsize(msg, font)
end
local function _setup_random_playlist(tag_entries, play, savepl, min_repeat, trackcount)
-- Setup string tables
local tPLAYTEXT = {"Play? [ %s ] (up/dn)", "true = play tracks on success"}
local tSAVETEXT = {"Save to disk? [ %s ] (up/dn)",
"true = tracks saved to",
playlist_name};
local tREPEATTEXT = {"Repeat hist? [ %d ] (up/dn)","higher = less repeated songs"}
local tPLSIZETEXT = {"Find [ %d ] tracks? (up/dn)",
"Warning overwrites dynamic playlist",
"Press back to cancel"};
-- how many lines can we fit on the screen?
local res, w, h = text_extent("I")
h = h + 5 -- increase spacing in the setup menu
local max_w = rb.LCD_WIDTH / w
local max_h = rb.LCD_HEIGHT - h
local y = 0
-- User Setup Menu
local action, ask, increment
local t_desc = {scroll = true} -- scroll the setup items
-- Clears screen and adds title and icon, called first..
function show_setup_header()
local desc = {icon = 2, show_icons = true, scroll = true} -- 2 == Icon_Playlist
rb.lcd_clear_display()
rb.lcd_put_line(1, 0, sHEADERTEXT, desc)
end
-- Display up to 3 items and waits for user action -- returns action
function ask_user_action(desc, ln1, ln2, ln3)
if ln1 then rb.lcd_put_line(1, h, ln1, desc) end
if ln2 then rb.lcd_put_line(1, h + h, ln2, desc) end
if ln3 then rb.lcd_put_line(1, h + h + h, ln3, desc) end
rb.lcd_hline(1,rb.LCD_WIDTH - 1, h - 5);
rb.lcd_update()
local act = rb.get_plugin_action(-1); -- Blocking wait for action
-- handle magnitude of the increment here so consumer fn doesn't need to
if act == ADD_BUTTON_RPT and act ~= ADD_BUTTON then
increment = increment + 1
if increment > 1000 then increment = 1000 end
act = ADD_BUTTON
elseif act == SUB_BUTTON_RPT and act ~= SUB_BUTTON then
increment = increment + 1
if increment > 1000 then increment = 1000 end
act = SUB_BUTTON
else
increment = 1;
end
return act
end
-- Play the playlist on successful completion true/false?
function setup_get_play()
action = ask_user_action(tdesc,
string.format(tPLAYTEXT[1], tostring(play)),
tPLAYTEXT[2]);
if action == ADD_BUTTON then
play = true
elseif action == SUB_BUTTON then
play = false
end
end
-- Play the playlist on successful completion true/false?
function setup_get_save()
action = ask_user_action(tdesc,
string.format(tSAVETEXT[1], tostring(savepl)),
tSAVETEXT[2], tSAVETEXT[3]);
if action == ADD_BUTTON then
savepl = true
elseif action == SUB_BUTTON then
savepl = false
elseif action == OK_BUTTON then
ask = setup_get_play;
setup_get_save = nil
action = 0
end
end
-- Repeat song buffer list of previously added tracks 0-??
function setup_get_repeat()
if min_repeat >= trackcount then min_repeat = trackcount - 1 end
if min_repeat >= tag_entries then min_repeat = tag_entries - 1 end
action = ask_user_action(t_desc,
string.format(tREPEATTEXT[1],min_repeat),
tREPEATTEXT[2]);
if action == ADD_BUTTON then
min_repeat = min_repeat + increment
elseif action == SUB_BUTTON then -- MORE REPEATS LESS RAM USED
if min_repeat < increment then increment = 1 end
min_repeat = min_repeat - increment
if min_repeat < 0 then min_repeat = 0 end
elseif action == OK_BUTTON then
ask = setup_get_save;
setup_get_repeat = nil
action = 0
end
end
-- How many tracks to find
function setup_get_playlist_size()
action = ask_user_action(t_desc,
string.format(tPLSIZETEXT[1], trackcount),
tPLSIZETEXT[2],
tPLSIZETEXT[3]);
if action == ADD_BUTTON then
trackcount = trackcount + increment
elseif action == SUB_BUTTON then
if trackcount < increment then increment = 1 end
trackcount = trackcount - increment
if trackcount < 1 then trackcount = 1 end
elseif action == OK_BUTTON then
ask = setup_get_repeat;
setup_get_playlist_size = nil
action = 0
end
end
ask = setup_get_playlist_size; -- \!FIRSTRUN!/
repeat -- SETUP MENU LOOP
show_setup_header()
ask()
rb.lcd_scroll_stop() -- I'm still wary of not doing this..
collectgarbage("collect")
if action == CANCEL_BUTTON then rb.lcd_scroll_stop(); return nil end
until (action == OK_BUTTON)
return play, savepl, min_repeat, trackcount;
end
--deletes existing file and creates a new playlist
local function playlist_create(filename)
local filehandle = io.open(filename, "w+") --overwrite
if not filehandle then
rb.splash(rb.HZ, "Error opening " .. filename)
return false
end
t_playlistbuf = {}
filehandle:write("\239\187\191") -- Write BOM --"\xEF\xBB\xBF"
playlist_handle = filehandle
return true
end
-- writes track path to a buffer must be flushed
local function playlist_insert(trackpath)
local bufp = #t_playlistbuf + 1
t_playlistbuf[bufp] = trackpath
bufp = bufp + 1
t_playlistbuf[bufp] = "\n"
return bufp
end
-- flushes playlist buffer to file
local function playlist_flush()
playlist_handle:write(table.concat(t_playlistbuf))
t_playlistbuf = {}
end
-- closes playlist file descriptor
local function playlist_finalize()
playlist_handle:close()
return true
end
--[[ Given the filenameDB file [database]
creates a random dynamic playlist with a default savename of [playlist]
containing [trackcount] tracks, played on completion if [play] is true]]
local function create_random_playlist(database, playlist, trackcount, play, savepl)
if not database or not playlist or not trackcount then return end
if not play then play = false end
if not savepl then savepl = false end
local playlist_handle
local playlistisfinalized = false
local file = io.open('/' .. database or "", "r") --read
if not file then rb.splash(100, string.format(sERROROPENFMT, database)) return end
local fsz = file:seek("end")
local fbegin
local posln = 0
local tag_len = TCHSIZE
local anchor_index
local ANCHOR_INTV
local track_index = setmetatable({},{__mode = "v"}) --[[ weak table values
this allows them to be garbage collected as space is needed / rebuilt as needed ]]
-- Read character function sets posln as file position
function readchrs(count)
if posln >= fsz then return nil end
file:seek("set", posln)
posln = posln + count
return file:read(count)
end
-- Check the header and get size + #entries
local tagcache_header = readchrs(DATASZ) or ""
local tagcache_sz = readchrs(DATASZ) or ""
local tagcache_entries = readchrs(DATASZ) or ""
if tagcache_header ~= sTCHEADER or
bytesLE_n(tagcache_sz) ~= (fsz - TCHSIZE) then
rb.splash(100, string.format(sINVALIDDBFMT, database))
return
end
local tag_entries = bytesLE_n(tagcache_entries)
--if tag_entries > 50000 then play = false end
play, savepl, min_repeat, trackcount = _setup_random_playlist(
tag_entries, play, savepl, min_repeat, trackcount);
_setup_random_playlist = nil
if savepl == false then
-- Use the rockbox playlist functions to add tracks to in-ram playlist
playlist_create = function(filename)
return (rb.playlist("create", playlistpath .. "/", playlist) >= 0)
end
playlist_insert = function(str)
return rb.playlist("insert_track", str)
end
playlist_flush = function() end
playlist_finalize = function()
return (rb.playlist("amount") >= trackcount)
end
end
if not playlist_create(playlistpath .. "/" .. playlist) then return end
collectgarbage("collect")
-- how many lines can we fit on the screen?
local res, w, h = text_extent("I")
local max_w = rb.LCD_WIDTH / w
local max_h = rb.LCD_HEIGHT - h
local y = 0
rb.lcd_clear_display()
function get_tracks_random()
local tries, idxp
local tracks = 0
local str = ""
local t_lru = {}
local lru_widx = 1
local lru_max = min_repeat
if lru_max >= tag_entries then lru_max = tag_entries / 2 + 1 end
function do_progress_header()
rb.lcd_put_line(1, 0, string.format(sPROGRESSHDRFMT,tracks, trackcount))
rb.lcd_update()
--rb.sleep(300)
end
function show_progress()
local sdisp = str:match("([^/]+)$") or "?" --just the track name
rb.lcd_put_line(1, y, sdisp:sub(1, max_w));-- limit string length
y = y + h
if y >= max_h then
do_progress_header()
rb.lcd_clear_display()
playlist_flush(playlist_handle)
rb.yield()
y = h
end
end
-- check for repeated tracks
function check_lru(val)
if lru_max <= 0 or val == nil then return 0 end --user wants all repeats
local rv
local i = 1
repeat
rv = t_lru[i]
if rv == nil then
break;
elseif rv == val then
return i
end
i = i + 1
until (i == lru_max)
return 0
end
-- add a track to the repeat list (overwrites oldest if full)
function push_lru(val)
t_lru[lru_widx] = val
lru_widx = lru_widx + 1
if lru_widx > lru_max then lru_widx = 1 end
end
function get_index()
if ANCHOR_INTV > 1 then
get_index =
function(plidx)
local p = track_index[plidx]
if p == nil then
parse_database_offsets(plidx)
end
return track_index[plidx][1]
end
else -- all tracks are indexed
get_index =
function(plidx)
return track_index[plidx]
end
end
end
get_index() --init get_index fn
-- Playlist insert loop
while true do
str = nil
tries = 0
repeat
idxp = random(1, tag_entries)
tries = tries + 1 -- prevent endless loops
until check_lru(idxp) == 0 or tries > fsz -- check for recent repeats
posln = get_index(idxp)
tag_len = bytesLE_n(readchrs(DATASZ))
posln = posln + DATASZ -- idx = bytesLE_n(readchrs(DATASZ))
str = readchrs(tag_len) or "\0" -- Read the database string
str = str:match("^(%Z+)%z$") -- \0 terminated string
-- Insert track into playlist
if str ~= nil then
tracks = tracks + 1
show_progress()
push_lru(idxp) -- add to repeat list
if playlist_insert(str) < 0 then
rb.sleep(rb.HZ)
rb.splash(rb.HZ, sPLAYLISTERROR)
break; -- ERROR, PLAYLIST FULL?
end
end
if tracks >= trackcount then
playlist_flush()
do_progress_header()
break
end
-- check for cancel non-blocking
if rb.get_plugin_action(0) == CANCEL_BUTTON then
break
end
end
end -- get_files
function build_anchor_index()
-- index every n files
ANCHOR_INTV = 1 -- for small db we can put all the entries in ram
local ent = tag_entries / 100 -- more than 1000 will be incrementally loaded
while ent >= 10 do -- need to reduce the size of the anchor index?
ent = ent / 10
ANCHOR_INTV = ANCHOR_INTV * 10
end -- should be power of 10 (10, 100, 1000..)
--grab an index for every ANCHOR_INTV entries
local aidx={}
local acount = 0
local next_idx = 1
local index = 1
local tlen
if ANCHOR_INTV == 1 then acount = 1 end
while index <= tag_entries and posln < fsz do
if next_idx == index then
acount = acount + 1
next_idx = acount * ANCHOR_INTV
aidx[index] = posln
else -- fill the weak table, we already did the work afterall
track_index[index] = {posln} -- put vals inside table to make them collectable
end
index = index + 1
tlen = bytesLE_n(readchrs(DATASZ))
posln = posln + tlen + DATASZ
end
return aidx
end
function parse_database_offsets(plidx)
local tlen
-- round to nearest anchor entry that is less than plidx
local aidx = (plidx / ANCHOR_INTV) * ANCHOR_INTV
local cidx = aidx
track_index[cidx] = {anchor_index[aidx] or fbegin};
-- maybe we can use previous work to get closer to the desired offset
while track_index[cidx] ~= nil and cidx <= plidx do
cidx = cidx + 1 --keep seeking till we find an empty entry
end
posln = track_index[cidx - 1][1]
while cidx <= plidx do --[[ walk the remaining entries from the last known
& save the entries on the way to our desired entry ]]
tlen = bytesLE_n(readchrs(DATASZ))
posln = posln + tlen + DATASZ
track_index[cidx] = {posln} -- put vals inside table to make them collectable
if posln >= fsz then posln = fbegin end
cidx = cidx + 1
end
end
if trackcount ~= nil then
rb.splash(10, sSEARCHINGFILES)
fbegin = posln --Mark the beginning for later loops
tag_len = 0
anchor_index = build_anchor_index() -- index track offsets
if ANCHOR_INTV == 1 then
-- all track indexes are in ram
track_index = anchor_index
anchor_index = nil
end
--[[ --profiling
local starttime = rb.current_tick();
get_tracks_random()
local endtime = rb.current_tick();
rb.splash(1000, (endtime - starttime) .. " ticks");
end
if (false) then
--]]
get_tracks_random()
playlistisfinalized = playlist_finalize(playlist_handle)
end
file:close()
collectgarbage("collect")
if trackcount and play == true and playlistisfinalized == true then
rb.audio("stop")
rb.yield()
if savepl == true then
rb.playlist("create", playlistpath .. "/", "dynamic_playlist.m3u8")
rb.playlist("insert_playlist", playlistpath .. "/" .. playlist)
end
rb.playlist("start", 0, 0, 0)
end
end -- playlist_create
local function main()
if not rb.file_exists(rb.ROCKBOX_DIR .. "/database_4.tcd") then
rb.splash(rb.HZ, sINITDATABASE)
os.exit(1);
end
if rb.cpu_boost then rb.cpu_boost(true) end
rb.backlight_force_on()
if not rb.dir_exists(playlistpath) then
luadir.mkdir(playlistpath)
end
rb.lcd_clear_display()
rb.lcd_update()
collectgarbage("collect")
--rb.ROCKBOX_DIR = "" -- TESTING!!
create_random_playlist(rb.ROCKBOX_DIR .. "/database_4.tcd",
playlist_name, max_tracks, play_on_success);
rb.splash(rb.HZ * 2, sGOODBYE)
-- Restore user backlight settings
rb.backlight_use_settings()
if rb.cpu_boost then rb.cpu_boost(false) end
--[[
local used, allocd, free = rb.mem_stats()
local lu = collectgarbage("count")
local fmt = function(t, v) return string.format("%s: %d Kb\n", t, v /1024) end
-- this is how lua recommends to concat strings rather than ..
local s_t = {}
s_t[1] = "rockbox:\n"
s_t[2] = fmt("Used ", used)
s_t[3] = fmt("Allocd ", allocd)
s_t[4] = fmt("Free ", free)
s_t[5] = "\nlua:\n"
s_t[6] = fmt("Used", lu * 1024)
s_t[7] = "\n\nNote that the rockbox used count is a high watermark"
rb.splash_scroller(10 * rb.HZ, table.concat(s_t)) --]]
end --MAIN
main() -- BILGUS
it defaults to using the inbuilt rockbox playlist insert so should work for you as well
-
this ended up being a pretty complicated journey in the end thanks philden :)
should be a nice example plugin now, I may revisit it to address the taking
of the audio buffer but in the meantime I'll probably remove that panic from
rockbox since clearly there is a way to run out of memory for the playlist
(giving it all to lua)
-
I installed the latest dev build today, and both version do work.
Thanks very much!
-
Philden you are most welcome thanks for testing, I'll get everything updated in the next day or two