Jump to content
Wulfie Reanimator

Yet Another Music Player™

Recommended Posts

With just about 100 SLOC (yes I counted), here's a little thing I made (with questionable origins) back in 2014, last updated in 2018. I thought I already posted this here though, but couldn't find it.

This obviously doesn't come with any songs, but you can get free and fullperm ones from here:
https://marketplace.secondlife.com/p/Trotman-Music-Notecards/7297342 (The Trotman is NOT mine nor have I worked on it.)

I'm gonna post the code below, but let's start with the user-manual first:


Full list of chat (channel 1) commands:

Play a randomly selected song:
/1 random

Play a specific song by name (of the notecard):
/1 play songname
/1 song songname

Play a specific song by inventory number:
/1 playid 30
/1 songid 30

Play the next song when the current song ends:
/1 autoplay on
/1 autoplay off

Stop playing the current song:
/1 stop

Search for songs with partially matching names:
/1 find newbie
/1 find ne

Change the player's volume:
/1 volume 100
/1 volume 0


Setting up a compatible notecard is very simple.

- The name of the notecard is used for the "play" command.

- The first line of the notecard should include the length per sound clip in seconds. (If your sound clips aren't equal length, there will be gaps/skips.)

- The contents of the notecard should include UUIDs for each clip of the song in order from beginning to end, one UUID per line.

- Here's an example of notecard contents, delay and 4 sound UUIDs


If you experience gaps and skipping, this is sometimes unavoidable and caused by how the SL viewer communicates with SL servers. (Even with a strong/fast connection.)

Gaps are only prevented when queueing is successful. If a song doesn't fully load before it is played, there will be a gap because no new sound has been queued.

If a gap happens, the script's timer will not be in sync with which sound is playing for your viewer. (queuing is client-side) This can cause more than one clip to try getting queued, which will discard the second clip, and you will eventually hear another gap or a skip.

Skips/gaps can also be caused by improperly configured notecards or if the sounds aren't exactly the same length. (Or are cut incorrectly.)

The only way to fix the desync is to play another song or stop and restart the current one.


Notes for anyone wishing to edit the script:

You're best off enabling the LSL preprocessor (Firestorm feature) to properly see the comments and take advantage of the features used in the script. (Just #define in this case.)

You can read more about the Processor here:


Changelog since 1.5:

Update 1.8
- New "find" chat command
- General code overhaul, new code style
- General bug fixes, optimizing, cleanup/clarification
- No longer using LSL Preprocessor features (but you need it to see proper comments)

Update 1.7
- New "next" chat command
- New "last" chat command
- Fixed "missing sound" error on autoplay
- Fixed silent gaps early on

Update 1.6
- New: "playid" chat command
- New: autoplay feature

Update 1.5
- Refactoring (variable names, syntax format)
- Additional comments
- Alternative chat commands (vol, play)
- New method for volume display

// For reading the notecard:
string  notecardName;
integer notecardLine;
key     notecardRequest;

// For playing a song:
list    soundList       = []; // all clips
integer soundCurrent    = 0;  // current clip
float   soundDelay      = 0;  // timer until next clip
float   soundVolume     = 1;  // duh

// For playing all songs:
integer songAutoplay    = 0; // move to next song when current one ends
integer songCount       = 0; // inventory notecard count, 0 is user manual!
integer songCurrent     = 0; // tracking for autoplay

    // Play next sound, then increment for the next clip.
    llPlaySound(llList2String(soundList, soundCurrent++), soundVolume);
    // If the next clip is past the end of the song..
    if(soundCurrent >= llGetListLength(soundList))
        if(songAutoplay) // Start loading the next song.
        else soundCurrent = 0; // Or loop the current song.
    // Note: Preload is limited by distance and is somewhat unreliable.
    llPreloadSound(llList2String(soundList, soundCurrent));

LoadFromNotecard(integer inventoryNum)
    // Range-validity check (Inventory is 0-based but 0 is the "info" notecard)
    if(inventoryNum >= llGetInventoryNumber(INVENTORY_NOTECARD)) inventoryNum = 1;
    else if(inventoryNum < 1) inventoryNum = llGetInventoryNumber(INVENTORY_NOTECARD)-1;
    songCurrent = inventoryNum;
    notecardName = llGetInventoryName(INVENTORY_NOTECARD, inventoryNum);
    notecardRequest = llGetNotecardLine(notecardName, notecardLine);
    llOwnerSay("Loading: "+ notecardName +" ("+ (string)inventoryNum +")");

   llSetText("", <1,1,1>, 1);
   soundList    = [];
   soundCurrent = 0;
   notecardLine = 0;

        llListen(1, "", llGetOwner(), "");
        songCount = llGetInventoryNumber(INVENTORY_NOTECARD)-1;
        // Allows up to 2 sounds be queued for play. (current and ONE awaiting to be played)
        // Prevents skipping when llPlaySound is used before the currently playing sound has ended.
        // The next sound (but only one) will always be kept queued. (adding more sounds to queue will be discarded)
    dataserver(key current_query, string data)
        if(data != EOF) // Read until End Of File
            if(notecardLine) // Reading sound UUIDs.
                // Start preloading the first sound already to avoid silence.
                if(notecardLine == 1) llPreloadSound(data);
                soundList += [data];
            else soundDelay = (float)data; // Reading the first line. (time)
            // Move onto the next line.
            notecardRequest = llGetNotecardLine(notecardName, ++notecardLine);
        else // End of File
            llSetTimerEvent(soundDelay - 0.01);
            llSetText(notecardName, <1,1,1>, 1.0);
    listen(integer c, string n, key id, string m)
        // Play a random song.
        if(m == "random")
        // Play the n-th song, based on its position in the inventory.
        // This check MUST be done before the "play" check to avoid bugs.
        else if(llGetSubString(m,0,5) == "playid" || llGetSubString(m,0,5) == "songid")
            string inputNumberStr = llGetSubString(m,7,-1);
            if(inputNumberStr != "0" && !(integer)inputNumberStr)
                llOwnerSay("ID must be a number!");
            if((integer)inputNumberStr >= llGetInventoryNumber(INVENTORY_NOTECARD))
            else LoadFromNotecard((integer)inputNumberStr);
        // Play a specific song.
        else if(llGetSubString(m,0,3) == "play" || llGetSubString(m,0,3) == "song")
            // Get the song name. (rest of the message)
            string inputName = llGetSubString(m,5,-1);
            // If the name is found in the object's inventory..
            if(llGetInventoryType(inputName) == INVENTORY_NOTECARD)
                // Loop through inventory until a matching name is found.
                integer inventoryNum;
                while(llGetInventoryName(INVENTORY_NOTECARD, inventoryNum) != inputName) { ++inventoryNum; }
                songCurrent = inventoryNum;
                // Save the notecard name and start reading.
                notecardRequest = llGetNotecardLine(notecardName = inputName, notecardLine);
            else llOwnerSay("Invalid name!");
        // Play the next song.
        else if (llGetSubString(m, 0, 3) == "next")
        // Play the last song.
        else if (llGetSubString(m, 0, 3) == "last")
        // Stop the song.
        else if(m == "stop")
        else if(llGetSubString(m,0,3) == "find")
            string inputSearch = llGetSubString(m, 5, -1);
            list matches;
            llOwnerSay("Searching for songs with \"" + inputSearch + "\"");
            integer i = llGetInventoryNumber(INVENTORY_NOTECARD)-1;
                if(~llSubStringIndex(llGetInventoryName(INVENTORY_NOTECARD, i), inputSearch))
                    matches += llGetInventoryName(INVENTORY_NOTECARD, i);
            llOwnerSay("Found: " + llList2CSV(matches));
        // Set the volume.
        else if(llGetSubString(m,0,2) == "vol") // New short chat command
            // Check for old longer command to take volume value from the right place.
            // Get the value, divide to scale it roughly within the range of 0.0-1.0
            if(llGetSubString(m,3,5) == "ume") //      ┏━ Notice here
                 soundVolume = (float)llGetSubString(m,7,-1)/100;
            else soundVolume = (float)llGetSubString(m,4,-1)/100;
            // Correct large numbers (only necessary for OwnerSay display)
            if(soundVolume > 1) soundVolume = 1;
            // Set the new volume.
            // Type-cast to integer to truncate decimals for cleaner display.
            llOwnerSay("Volume: "+ (string)((integer)(soundVolume*100)) +"%");
        // Toggle autoplay
        else if(llGetSubString(m,0,7) == "autoplay")
            string inputState = llGetSubString(m,9,-1);
            // Set autoplay and display confirmation.
            if(inputState == "on")
                songAutoplay = 1;
                llOwnerSay("Autoplay enabled.");
            else if(inputState == "off")
                songAutoplay = 0;
                llOwnerSay("Autoplay disabled.");
            else llOwnerSay("Invalid command. (on/off)");
        else if(llGetSubString(m,0,3) == "help")
            llOwnerSay("\nThere should also be an included \"info\" notecard in the contents!"
                + "\n\nCurrently usable chat commands:"
                + "\nrandom"
                + "\nfind [partial name]"
                + "\nplay [notecard name]"
                + "\nplayid [inventory number]"
                + "\nstop"
                + "\nautoplay [on/off]"
                + "\nvol [0-100]");
    on_rez(integer param)
    changed(integer change)
        if(change & CHANGED_OWNER) llResetScript();


  • Like 2
  • Thanks 3

Share this post

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Create New...