Jump to content

Local radio?


LoneWolfiNTj
 Share

Recommended Posts

I have a radio sitting on my couch in my house in Zindra and it's driving me crazy for multiple reasons:

  1. While it does have 27 channels, it's not programmable, so I'm stuck with the 27 music streams it came with, mostly rock; I can't add jazz, classical, etc.
  2. It works by changing the parcel sound stream to any of those 27 URLs, so I can't have different music in my skybox above my house.
  3. It's no-modify, so I can't even look-at its scripts much less modify or replace them.

So what I'd like to do (if this is even possible; I'm still new at building and scripting), is this: rez a box and dump into it a script such that when I touch the box a menu pops up with buttons programmed to cause the box to play one of several internet music streams of my choosing, not parcel-wide, but with distance limited to about 20m, about the same distance as llSay (but sound instead of text). Is this even possible?

And if this is possible, how do I do the following 2 things?

  1. How do I bring up a menu with pushbuttons? I've seen many objects (houses, HUDs, teleporters, etc) pop those up on touch, but I can't see a function in the LSL functions list on the wiki which does that.
  2. Is there a function for playing internet sound streams by URL locally rather than parcel-wide? (If not, I imagine this whole project is impossible.)

 

Link to comment
Share on other sites

34 minutes ago, LoneWolfiNTj said:
  1. How do I bring up a menu with pushbuttons? I've seen many objects (houses, HUDs, teleporters, etc) pop those up on touch, but I can't see a function in the LSL functions list on the wiki which does that.
  2. Is there a function for playing internet sound streams by URL locally rather than parcel-wide? (If not, I imagine this whole project is impossible.)

For #1, see llDialog(). It works with llListen() to return the user's response to the script's listen() event handler. You can probably find cool sample scripts for land radio, as a starting point.

(Many will read notecards in which you can specify a list of internet radio station URLs, etc., but because you're writing your own script you could populate the list variable(s) directly inside the script.)

For #2, there are a couple ways to approximate what you want here. The more common is to use "shared media" (aka Media On A Prim) instead of parcel radio. You can actually play around with this just in the "Build Tool" object editor without needing to write a script at all (but you'll eventually want a script to make it easy to change channels).

For completeness, the other way to get location-specific sound is with PARCEL_MEDIA_COMMAND_AGENT while keeping track of which agents are within range of the emitter. This puts the script in charge of who gets which sounds, thus incurring some overhead that's instead absorbed by the viewer with the more common Shared Media approach.

Note that users more commonly enable parcel-wide "land radio" audio than either parcel media or shared media, so visitors may need some instruction to enable the stream(s).

Edited by Qie Niangao
  • Like 1
Link to comment
Share on other sites

Qie Niangao : Thanks for the tip on "llDialog! I was wondering how those menus were done. Searching around, I found a script that uses llGetNotecardLine and llDialog to load up to 12 buttons with lines from a notecard, and I've been playing around with it a bit, but there's several things about it that I don't understand. Such as, llGetNotecardLine is called once each from the "state_entry" and "changed" events, then repeatedly in a loop in the "dataserver" event. What's up with that? Are the first too instances of llGetNotecardLine just to trigger the dataserver event to happen, then the repeated calls inside dataserver are to continue getting data from the notecard as long as it's available? That's how I'm reading it, as when I put // before those first two calls, then the script reads NO data, so apparently the "dataserver" event never occurs.

Regarding "shared media" and "build tool", I don't see anything in the build tool about "shared media" or "media on a prim". Hmmm. Let me google "second life media on a prim". AH, I see: in lieu of a "texture", one can apply media. How bizarre! Ok, let me play with that. Hmmm... I rezzed a box in a sandbox and dropped a media on the front face and nothing happened. Then I dropped a different media and it worked! A third one, fails with "You have requested a file download, which is not allowed in Firestorm". Hmmm... seems to balk at any media URL that contains "mp3", thinking it's a "file" when it actually isn't.

Also, it seems the range is going to be about 80m rather than 20m, so "sound on a prim" apparently spams the local sound environment. I'd think it would have the option of using "WhisperCast" instead of "SHOUT-CAST", but I'm not seeing that option. Anyone using Firestorm wanting to avoid that can manually set the range; but it apparently defaults to 80m. And for those not using Firestorm I have no idea how far the range is.

So it seems that "media on a prim" is still a bit buggy. 

Still, that should be enough to get me started making myself a multi-channel "local radio". Thanks!

Wait, one more question: How do I attach a media to a face with a script? Surely not llSetTexture? llSetLinkMedia perhaps?

Link to comment
Share on other sites

24 minutes ago, LoneWolfiNTj said:

llGetNotecardLine just to trigger the dataserver event to happen, then the repeated calls inside dataserver are to continue getting data from the notecard as long as it's available?

Pretty much. llGetNotecardLine, only gets one line of data, (and only up to 255 characters..) so to read the whole notecard you have to call it for each line until the dataserver returns EOF.

(or, if you wanted to be clever, you could request all llGetNumberOfNotecardLines at once in a loop after 1 dataserver event, but then you might have some overhead in sorting which line is which, because they would come to the script out of order)

  • Like 1
Link to comment
Share on other sites

51 minutes ago, LoneWolfiNTj said:

Wait, one more question: How do I attach a media to a face with a script? Surely not llSetTexture? llSetLinkMedia perhaps?

Yeah, llSetLinkMedia() establishes shared media for one face of an element in the linkset. (Of mostly historical interest, the llSetPrimMediaParams() does the same thing but only for the scripted link itself, so those functions share parameters such as PRIM_MEDIA_CURRENT_URL.)

It is tricky finding viable URLs to feed these various functions (same problem for parcel sound, too), so it's not surprising that it doesn't like an .mp3 (which it will see as a file rather than a streaming format). I don't think playlists (.pls) will work either. I generally end up specifying a URL in the form of http:// <IP address> : <port number> for these streams, which are often found by persistent snooping around an internet radio source site. SomaFM, for example, calls these things "direct stream links" hidden under "alternative stream links for internet radios and other media players" and even then only the non-.pls, non-.m3u links seem viable. Maybe somebody else has a more rigorous way of finding working stream links.

And yeah, shared media's range is problematic. Used to be even less predictable. If I really need to constrain streaming audio, I revert to the old PARCEL_MEDIA_COMMAND_AGENT and periodically check where the avatars are located, so they can hear for example different streams in adjacent "apartment" spaces. It's not that difficult, but a bit of a heavy lift for a first script, and requires some script processing time in contrast to merely leaving a URL on an object surface for the viewer to discover and pass off to an audio streaming library.

  • Like 2
Link to comment
Share on other sites

1 hour ago, LoneWolfiNTj said:

Such as, llGetNotecardLine is called once each from the "state_entry" and "changed" events

In addition to the repeated calls in the dataserver() handler, these ones in state_entry() and changed() will indeed be to kick-off reading or re-reading the notecard from the top. The reason it arises in changed() is usually to handle times when the notecard is edited or replaced, which is evident when the CHANGED_INVENTORY flag is set. (A notecard gets a brand new "key" or UUID every time it's saved, so that's a way of telling that specifically that notecard was the thing in object inventory that changed.)

  • Like 2
Link to comment
Share on other sites

7 hours ago, Qie Niangao said:

I don't think playlists (.pls) will work either.

True, because those are actually tiny text files that contain URLs for audio streams, not the audio streams themselves. For example, I had a devil of a time last night trying to find the actual URL for radio station WQXR (New York, New York, USA) classical stream, because their web site was hiding the URL behind Javascript & form controls so well that even using netstat in a DOS box wasn't revealing it. But after much digging I found an "alternative media formats" page on their web site, giving a pls file. It turned out to be a text file with these contents:

[Playlist]
NumberOfEntries=1
File1=http://stream.wqxr.org/wqxr
Title1=WQXR 105.9 FM
Length1=-1

So I went into SL Marketplace, bought a parcel radio with copy & mod perms for $L15, and set one of its channels to "http://stream.wqxr.org/wqxr", and voila, it worked! Now, I don't know what the "http://32.89.125.52:8036" style URL is for that, but I might be able to find out by shutting-down all apps, opening a single browser with a single tab running that stream, then looking at netstat for URLS with ports in the thousands. Ideally all sound URLs SL should be the actual numerical URL, if one can find out what those are.

Link to comment
Share on other sites

7 minutes ago, Quistess Alpha said:

wget will spit it out for you easily. It's http://204.93.244.168:80/wqxr

Cool! Thanks! I'm unfamiliar with that tool, though. Is that from Mac? Linux? Windows? I'm running Windows 10 on both my computers currently. Wait, let me google that. Ah, it's Gnu! Let's see if it's in my Cygwin. It is! 

Every time I run it, though, I get a different URL. Apparently WQXR is using multi-server distributed streaming, so a single numeric URL won't work for that stream. And I can see why: it's a world-class radio station (The Classical Music Voice Of New York, New York, USA) with people listening to it all over the world, tens of millions of people at once, so a single server would crash almost instantly (DDOS from Hell).

But a single numeric IP:Port should work for servers that have a smaller user base just fine, which is most of the niche-market streams I'll be dropping in radios, so wget is a good tool to know for that. Thanks for the tip! 🙂

 

  • Like 1
Link to comment
Share on other sites

10 hours ago, Qie Niangao said:

Yeah, llSetLinkMedia() establishes shared media for one face of an element in the linkset. (Of mostly historical interest, the llSetPrimMediaParams() does the same thing but only for the scripted link itself, so those functions share parameters such as PRIM_MEDIA_CURRENT_URL.)

Well, my radio boxes are simple ugly 1-prim 1-foot-cube boxes, so I only need to select a face, so I used llSetPrimMediaParams(). After a couple of hours of fighting, I realized I need to set all three of these to get it to work: home URL, current URL, and auto-play. Finally, my script works! 🙂 It looks like so:

set_media()
{
   llSetPrimMediaParams(3, [PRIM_MEDIA_HOME_URL,    "http://185.33.21.112:11269/"]);
   llSetPrimMediaParams(3, [PRIM_MEDIA_CURRENT_URL, "http://185.33.21.112:11269/"]);
   llSetPrimMediaParams(3, [PRIM_MEDIA_AUTO_PLAY,   1                            ]);
}

default
{
   state_entry()
   {
      set_media();
   }
}

That works for 1 channel. Now I just need to incorporate "read notecard" and "llDialog" code, then make a prettier-looking box, and I'll have a decent "local radio". I think I'll sell it in SL Marketplace. (I'm sure I could buy something like that, but I'd rather script, build, and sell, learning as I go.)

Anyway, thanks for the help! 🙂

Link to comment
Share on other sites

15 minutes ago, LoneWolfiNTj said:
set_media()
{
   llSetPrimMediaParams(3, 
   [  PRIM_MEDIA_HOME_URL,    "http://185.33.21.112:11269/",
      PRIM_MEDIA_CURRENT_URL, "http://185.33.21.112:11269/",
      PRIM_MEDIA_AUTO_PLAY,   1
   ]);
}

would probably work just as well. (bah, it's late too lazy to deal with formatting. . .)

 

  • Like 2
Link to comment
Share on other sites

It's a good idea to get accustomed to the format @Quistess Alpha suggests because there are many LSL functions that take a list of parameters as an argument, and eventually in some script it's likely to matter how many distinct function calls get made, or whether all the parameters are likely to get set during the same simulation frame.

Just a note about the IP address thing: if you're lucky enough to have an actual domain name such as stream.wqxr.org, by all means use that in preference to a corresponding IP address which will become invalid if, for example, WQXR changes streaming providers.

  • Like 2
Link to comment
Share on other sites

On 9/14/2021 at 1:42 AM, Qie Niangao said:

It's a good idea to get accustomed to the format @Quistess Alpha suggests because there are many LSL functions that take a list of parameters as an argument, and eventually in some script it's likely to matter how many distinct function calls get made, or whether all the parameters are likely to get set during the same simulation frame.

Just a note about the IP address thing: if you're lucky enough to have an actual domain name such as stream.wqxr.org, by all means use that in preference to a corresponding IP address which will become invalid if, for example, WQXR changes streaming providers.

Ah, OK. Yes, it is a bit weird and inefficient to use 3 function calls and 3 lists of one key-value pair each. I'll fix that.

And you're right about the IP thing. In the case of WQXR, they use multiple numeric IP addresses, so in their case, it's especially desirable to use "http://stream.wqxr.org/wqxr" instead of "http://204.93.244.168:80/wqxr" because any one numeric address may be overloaded or down at any one time, but if you use the domain name, the server does automatic load balancing.

Here's my final script, debugged and running, for a 12-channel 1-prim local radio:

list    urls;
list    rstations; 
integer linenr = 0;
string  NC = "Radio Stations";

reset_radio() 
{
   urls = [];
   rstations = []; 
   linenr = 0;
   llGetNotecardLine(NC, linenr);
}

set_media(string URL)
{
   llSetPrimMediaParams
   (
      3, 
      [
         PRIM_MEDIA_HOME_URL,    URL,
         PRIM_MEDIA_CURRENT_URL, URL,
         PRIM_MEDIA_AUTO_PLAY,   1  
      ]
   );
}

next_station(string line)
{
   list newurl = llParseString2List(line, [" ", " ", "="], []);
   if (llGetListLength(newurl) < 2)
   {
      return;
   }
   string url = llList2String(newurl, llGetListLength(newurl) - 1);
   string station = "";
   integer i;
   for (i=0; i<llGetListLength(newurl) - 1; i++)
   {
      if (llStringLength(station) > 0)
      {
         station += " ";
      }
      station += llList2String(newurl, i);
   }
   urls += [url];
   rstations += [station];
}

default
{
   state_entry()
   {
      reset_radio();
   }
    
   changed(integer change)
   {
      if (change & CHANGED_INVENTORY)
      {
         reset_radio();
      }
   }

   dataserver(key query_id, string data)
   {
      if (llGetListLength(urls) > 12)
      {
         llSay(0, "12 stations max");
      }
      else
      {
         if (data != EOF)
         {
            next_station(data);
            ++linenr;
            llGetNotecardLine(NC, linenr);
            return;
         }
      }
      llListen(93, "", NULL_KEY, ""); 
      llSay(0, "Touch for changing station.");
   }
    
   touch_start(integer touchNumber)
   {
      llDialog(llDetectedKey(0), "Stations: ", rstations, 93);        
   }

   listen(integer channel, string name, key id, string message)
   {
      string newURL = llList2String(urls, llListFindList(rstations, [message]));
      llSay(0, "New Station = " + message);
      llSay(0, "URL = " + newURL);
      set_media(newURL);
   }
}

 

Edited by LoneWolfiNTj
  • Like 1
Link to comment
Share on other sites

45 minutes ago, LoneWolfiNTj said:

Here's my final script,

If it works it works, good job! Just as a general tip though, I'd recommend moving specific literals (in this case, the link number 3, and the channel number 93) to global variables. (or firestorm preprocessor defines . . . YUCK.) If you need to re-link later, or avoid the dialog interfering with another script, much easier to change a single value at the top of the script than search-replace.

Link to comment
Share on other sites

1 hour ago, Quistess Alpha said:

... I'd recommend moving specific literals (in this case, the link number 3, and the channel number 93) to global variables...

Good point. Let's see what "93" is... ah, a comm channel number. That's the wrong answer, though; everyone knows the answer (to everything) is 42. Lemme try "int dialog_channel = 42;" at the top of the script then change all instances of 93 to "dialog_channel".... Yep, works. 🙂

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...