Rockbox Development > Feature Ideas

Random track selection

(1/3) > >>

philden:
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!

philden:
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.

philden:
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.

Bilgus:
its pretty easy to throw something together..


--- Code: -----[[ 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


--- End code ---

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)
 



Bilgus:
Well I went ahead and made probably exactly what you want


--- Code: -----[[ 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


--- End code ---

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

Navigation

[0] Message Index

[#] Next page

Go to full version