Here we build a soundscape comprising multiple internet radio streams, a morse-code generator and an ambient drone.
Here's some code to open a single internet stream. Hz.Streamin is a built-in Anode that can stream internet radio stations. It collaborates with StreamMgr as shown in this code snippet:
let anode = ascene.NewAnode("Hz.StreamIn");
let gain = ascene.NewAnode("Hz.Mix", {name, cfg:"stereo"});
let modgain = ascene.NewModulator("Hz.ModRand");
ascene.Chain(anode, gain, rev); // wire the stream into the shared reverb
await ascene.Sync();
gain.SetParam("Gain", -8.);
modgain.SetParam("Mode", 3); // smooth
modgain.SetParam("Frequency", .5 * Math.random());
ascene.ModulateParam(modgain, gain, "Gain",
modgain.NewRemapper({scale: 8, bipolar: true})); // additive mod
let iid = anode.GetId();
console.log(`open ${host} ${stream} ${iid}`);
StreamMgr.AudioStreamOpen(host, stream, iid) // <-----------
.then((ret) =>
{
if(verbose)
console.log(`open returned ${JSON.stringify(ret)}`);
streamStates[stationIndex].tid = ret.tid;
});
This example also shows how to record your performance with Hz.AudioFileOut.
const doRecord = true;
if(doRecord)
{
// this will fail if your workspace doesn't have a directory
// named '_tmp'. You can create one via the workspace panel.
let ts = Util.GetFileTimestamp();
let outputFile = await ResolveWSFile(`_ws_/_tmp/HzRadio.${ts}.wav`, false);
let preset = {filename: outputFile};
let record = ascene.NewAnode("Hz.AudioFileOut", {preset});
console.log("Recording to " + outputFile + " <----------------");
ascene.Chain(dac, record);
}
Here's a portion the audio graph responsible for delivering audio streams into our scene:
Each stream has its own downstream Hz.Mix node which is modulated by a Hz.ModRand to produce a swirling interplay between multiple live streams.
Further downstream we see that all roads lead to reverb.
As an alternate approach to graph-based parameter modulation we periodically update our drone tone and reverb mix explicitly via:
// alternative to graph-based modulator
function changeParams()
{
setTimeout(() =>
{
if(rev)
{
let mix = .8 * Math.random();
// console.log("new mix " + mix);
rev.SetParam("Wet", mix);
}
if(drone)
{
if(lastDroneNote != -1)
drone.NoteOff(lastDroneNote, 1);
lastDroneNote = 40 + Math.floor(Math.random()*24);
// console.log("new drone note " + lastDroneNote);
drone.NoteOn(lastDroneNote, .2 + .8 * Math.random());
}
if(!done)
changeParams(); // reinstall ourselves here
}, 7000 * (1 + Math.random()));
}
changeParams();
Next we perform our morse code using the
async generator idiom
seen below. The text-to-morsecode conversion and performance is implemented
in a separate file, morse.js
included via await Require("./morse.js");
.
Prior to each new quote, we "change the channel" using the channelSweeper
instrument with Glide enabled.
let toMorse = new Morse(60, .1); // note, rate
for(let q of zenquotes)
{
// change the channel
for(let i=0;i<40;i++)
{
let n = Math.floor(40 + 50 * Math.random());
channelSweeper.NoteOn(n, .8);
await ascene.Wait(ascene.Seconds(.05 + .05 * Math.random()));
channelSweeper.NoteOff(n);
yield;
}
let txt = q[0];
for await (const value of toMorse.PerformText(ascene, beeps, txt))
{
yield;
}
}
console.log("Done morseQuotes.");
Finally we gracefully shutdown after all quotes are delivered.
console.log("Done quoting, fade to black.");
let g = -.2;
while(g > -60)
{
master.ModParam("Gain", g);
await ascene.Wait(ascene.Seconds(.1));
g -= .2;
}
done = true; // signal setparam
// trigger (OFF) GUI checkboxes for 4 favorite stations
ToggleStation(0);
ToggleStation(1);
ToggleStation(2);
ToggleStation(3);
await Aengine.Close();
This example produces its own interface in the form of checkboxes that
allow you to add and subtract live feeds while it runs.
In addition, the Hz plugins produce a webview interface that allows you
to modify various gain, oscillator and reverb settings. You can use their
Show()
method make it visible.