JavaScript player¶
About¶
You can directly embed our player into your page, with the following benefits:
- Avoid the requirement for a genuine interaction (click) to activate the audio context, assuming users are already interacting with your page.
- Implement your own interface and visualisation, as simple or complex as you need.
- Connect our Web Audio API nodes directly to your own.
Bundles¶
We have several bundles that can be loaded separately or together. Each bundle will add to the public API exposed at window.syrinscape
.
integration
-
Convenience functions for integrations.
player
-
Web Audio API player, with no interface. You decide how (or if) the player appears on your page, and how users interact with it.
visualisation
-
Efficient and configurable
requestAnimationFrame
loop for pluggable visualisers.
These URLs will always redirect to the latest versions:
<script src="https://syrinscape.com/integration.js"></script>
<script src="https://syrinscape.com/player.js"></script>
<script src="https://syrinscape.com/visualisation.js"></script>
Tip
Use the pinned versions referenced in https://syrinscape.com/latest-bundles/ to avoid unexpected changes.
Player¶
Init¶
Start the player with an anonymous session:
syrinscape.player.init()
Note
A genuine user interaction is required to activate the audio context.
If not already active, syrinscape.player.init()
configures a one-time click
event handler on document
that captures and later replays the first click, after activating the audio context and starting the player.
Configure¶
Pass a configure()
callback to configure the player before startup:
syrinscape.player.init({
async configure() {
// Audio context. Leave undefined to create one.
syrinscape.config.audioContext = new AudioContext()
// Auth token.
syrinscape.config.token = prompt(
'Enter your Syrinscape auth token (from https://syrinscape.com/account/auth-token/). By default an anonymous token has been created for you. Leave blank to create a new one.',
syrinscape.config.token,
)
// Wait until async token change is finished, because `sessionId` might change.
await syrinscape.config.sync()
// Session ID. Use the GM's session ID for all users. Get from
// `syrinscape.config.sessionId` in the GM's browser.
syrinscape.config.sessionId = prompt(
"Enter the GM's session ID. By default a session has been created with you as the GM.",
syrinscape.config.sessionId,
)
},
})
Reconfigure¶
You can reconfigure any time after startup.
Join a new session:
syrinscape.config.sessionId = "new-session-id"
Reconnect as a new user:
syrinscape.config.token = "new-token"
Note
Assigning a new config.token
will asynchronously update config.authenticated
and config.sessionId
in the background. If you need to access these immediately, call await config.sync()
.
Interface¶
You can implement your own interface and visualisation, as simple or complex as you need.
For players, you should implement (at least):
- Local volume
- Mute
- Visualisation
For Game Masters, you should implement:
- Global volume
- Oneshot volume
- Stop
- Launch (Syrinscape, for full control)
- Login
You might also want to consider:
- Global Oneshots
- Search
- Embedded Mood and Element links in adventure text
Show and hide¶
Because the player will only start after the web audio context is activated, you can use the onActive()
and onInactive()
callbacks to show or hide your interface and a CTA to activate.
<style>
.cta,
.interface {
display: none;
}
</style>
<button class="cta">
Activate
</button>
<div class="interface">
...
</div>
<script>
syrinscape.player.init({
onActive() {
// Hide CTA. Show interface.
document.querySelector('.cta').style.display = 'none'
document.querySelector('.interface').style.display = 'revert'
},
onInactive() {
// Show CTA. Hide interface.
document.querySelector('.cta').style.display = 'revert'
document.querySelector('.interface').style.display = 'none'
},
})
</script>
Local volume and mute¶
- Apply local volume changes immediately (while adjusting).
- Save the last local volume on change, and restore on unmute.
- Set initial volume from config on startup.
<input class="local-volume" type="range" min="0" max="1.5" step="0.01" value="1">
<button class="mute" onclick="syrinscape.player.audioSystem.toggleMute()">
Mute
</button>
<script>
// Get controls.
let localVolume = document.querySelector('.local-volume')
let mute = document.querySelector('.mute')
// Set local volume immediately (while adjusting) and last local volume (to
// restore on unmute) on change.
localVolume.addEventListener('input', () => {
syrinscape.player.audioSystem.setLocalVolume(localVolume.value)
})
localVolume.addEventListener('change', () => {
syrinscape.config.lastLocalVolume = localVolume.value
})
// Update interface on `setLocalVolume` event.
syrinscape.events.setLocalVolume.addListener((event) => {
localVolume.value = event.detail
if (event.detail === 0) {
mute.textContent = 'Unmute'
} else {
mute.textContent = 'Mute'
}
})
</script>
Visualisation¶
Our visualisation bundle runs a requestAnimationFrame
loop that executes registered visualisers on each iteration.
Adjust the frame rate by setting syrinscape.config.visualisationFramerate
. The default is 30 frames per second.
Each visualiser is a function that should:
- Get a chunk of frequency or waveform data. For example, from the global analyser node
syrinscape.player.audioEffectSystem.analyser
.- Call
.getFrequencyData()
or.getTimeDomainData()
to get a chunk of frequency or waveform data, or.getData()
to get both.
- Call
- Render a frame of visualisation, however you like.
- We provide
d3VisualiseFrequencyData()
andd3VisualiseWaveformData()
, which we use in our own interfaces.D3.js
is included in the bundle.
- We provide
- Return
true
if it did any work.- If no visualisers do any work, the loop will be paused until the next sample starts, to avoid needless busywork.
Register global visualiser:
<style>
/* TODO: Slim down to essential styles, and explain them. Move styles into the visualiser, if possible. */
.visualisations-container {
left: 0;
bottom: 0;
opacity: 0.7;
position: absolute;
width: 100%;
}
.visualisations {
padding-top: 50%;
position: relative;
width: 100%;
}
.d3-frequency {
height: 100%;
left: 0;
position: absolute;
top: 0;
transform: scaleY(-1);
width: 100%;
}
.d3-waveform {
height: 100%;
left: 0;
position: absolute;
top: 0;
transform: scaleY(-1);
width: 100%;
}
.d3-waveform svg {
height: 100%;
width: 100%;
}
</style>
<div class="visualisations-container">
<div class="visualisations">
<svg class="d3-frequency"></svg>
<svg class="d3-waveform"></svg>
</div>
</div>
<script>
// Register 'global' frequency and waveform visualiser.
syrinscape.visualisation.add('global', () => {
// Get combined frequency and waveform data.
let data = syrinscape.player.audioEffectSystem.analyser.getData()
// Visualise frequency and waveform data.
// You can replace this section to visualise the data any way you like.
syrinscape.visualisation.d3VisualiseFrequencyData(data, '.d3-frequency')
syrinscape.visualisation.d3VisualiseWaveformData(data, '.d3-waveform')
// If analyser is still active, report that visualiser is still active.
// If no visualisers are active, visualisation loop will be paused.
return syrinscape.player.audioEffectSystem.analyser.isActive
})
</script>
Stop¶
Stop everything:
<button onclick="syrinscape.player.controlSystem.stopAll()">
Stop
</button>
Login¶
Show a login button to anonymous users:
<button class="login" onclick="syrinscape.integration.requestAuthToken()">
Login
</button>
<script>
// Show or hide login button when config is updated.
syrinscape.events.updateConfig.addListener(() => {
if (syrinscape.config.authenticated) {
document.querySelector('.login').style.display = 'none'
} else {
document.querySelector('.login').style.display = 'revert'
}
})
</script>
Launch¶
Launch Syrinscape as Game Master to control the session:
<button onclick="syrinscape.integration.launchAsGameMaster()">
Launch
</button>
Events¶
Several events are emitted, which you may like to listen for.
Each registered event is available at syrinscape.events
. Triggered events will send a CustomEvent
object to listeners with a detail
key, which could be a number, a string, or an object, depending on the event.
List of events¶
syrinscape.playerActive
-
Triggered when the player (audio context) is active.
syrinscape.playerInactive
-
Triggered when the player (audio context) is inactive.
syrinscape.setElementVolume
-
Triggered when an element's volume is set.
Details:
elementId
(number)volume
(number)
syrinscape.setGlobalVolume
-
Triggered when the global volume is set.
Details:
volume
(number)
syrinscape.setLocalVolume
-
Triggered when the local volume is set.
Details:
volume
(number)
syrinscape.startElement
-
Triggered when an element is started.
Details:
elementId
(number)-
timeToFirstSample
(number)-
Time in milliseconds until the element's first sample will start.
syrinscape.startMood
-
Triggered when a mood is started.
Details:
moodId
(number)moodTitle
(string)
syrinscape.startSample
-
Triggered when a sample is started.
Details:
elementId
(number, optional)playlistEntryId
(number, optional)sampleId
(number)-
timeToNextSample
(number, optional)-
Time in milliseconds until the next sample will start, for samples started by an element.
-
timeToStop
(number)-
Time in milliseconds until the sample will stop.
syrinscape.startVisualisation
-
Triggered when the visualisation is started, e.g. when a mood or element is started.
syrinscape.stopElement
-
Triggered when an element is stopped.
Details:
elementId
(number)-
timeToStop
(number)-
Time in milliseconds until the element will stop.
syrinscape.stopSample
-
Triggered when a sample stop is requested and again when the stop is completed.
Details:
elementId
(number, optional)playlistEntryId
(number, optional)sampleId
(number)-
timeToStop
(number)-
Time in milliseconds until the sample will stop or
0
when the stop is completed.
syrinscape.stopMood
-
Triggered when a mood is stopped. Does not trigger when a mood is stopped by a new mood starting.
syrinscape.stopVisualisation
-
Triggered when the visualisation is stopped, e.g. when there are no more samples playing.
syrinscape.updateConfig
-
Triggered when
syrinscape.config
is updated.Details:
authenticated
(boolean, optional)displayName
(string, optional)lastLocalVolume
(number, optional)localVolume
(number, optional)sessionId
(string, optional)token
(string, optional)visualisationFramerate
(number, optional)
Add a listener¶
syrinscape.events.setLocalVolume.addListener(
(event) => console.log(`volume: ${event.detail}`)
)
Complete example¶
- Load the bundles.
- Style the HTML.
- Implement an interface with stop, volume, mute, launch and login.
- Show a call to action when the audio context is not active.
- Activate on first interaction.
- Show the interface.
- Prompt for an auth token and session ID.
- Start the player.
- Reconnect as new user on login.
<!-- Load bundles. -->
<script src="https://syrinscape.com/integration.js"></script>
<script src="https://syrinscape.com/player.js"></script>
<script src="https://syrinscape.com/visualisation.js"></script>
<!-- Styles for the interface. -->
<style>
.syrinscape {
align-items: center;
background-image: url("https://syrinscape.com/static/img/BG-Base.jpg");
background-size: cover;
display: flex;
justify-content: center;
padding-top: 50%;
position: relative;
width: 100%;
}
.cta,
.interface {
display: none;
margin-top: -50%;
}
.visualisations-container {
left: 0;
bottom: 0;
opacity: 0.7;
position: absolute;
width: 100%;
}
.visualisations {
padding-top: 50%;
position: relative;
width: 100%;
}
.d3-frequency {
height: 100%;
left: 0;
position: absolute;
top: 0;
transform: scaleY(-1);
width: 100%;
}
.d3-waveform {
height: 100%;
left: 0;
position: absolute;
top: 0;
transform: scaleY(-1);
width: 100%;
}
.d3-waveform svg {
height: 100%;
width: 100%;
}
.controls {
display: flex;
margin: 50px auto;
position: relative; /* always above the visualisation */
width: 75%;
}
.local-volume {
flex-grow: 1;
margin: 0 1em;
}
</style>
<!-- Markup for the interface. -->
<div class="syrinscape">
<div class="cta">
<button>Activate</button>
</div>
<div class="interface">
<div class="visualisations-container">
<div class="visualisations">
<svg class="d3-frequency"></svg>
<svg class="d3-waveform"></svg>
</div>
</div>
<div class="controls">
<button onclick="syrinscape.player.controlSystem.stopAll()">
Stop
</button>
<input class="local-volume" type="range" min="0" max="1.5" step="0.01" value="1">
<button class="mute" onclick="syrinscape.player.audioSystem.toggleMute()">
Mute
</button>
<button onclick="syrinscape.integration.launchAsGameMaster()">
Launch
</button>
<button class="login" onclick="syrinscape.integration.requestAuthToken()">
Login
</button>
</div>
</div>
</div>
<!-- Configure and start the player. -->
<script>
(() => {
// Get controls.
let localVolume = document.querySelector('.local-volume')
let mute = document.querySelector('.mute')
// Set local volume immediately (while adjusting) and last local volume (to
// restore on unmute) on change.
localVolume.addEventListener('input', () => {
syrinscape.player.audioSystem.setLocalVolume(localVolume.value)
})
localVolume.addEventListener('change', () => {
syrinscape.config.lastLocalVolume = localVolume.value
})
// Update interface on `setLocalVolume` event.
syrinscape.events.setLocalVolume.addListener((event) => {
localVolume.value = event.detail
if (event.detail === 0) {
mute.textContent = 'Unmute'
} else {
mute.textContent = 'Mute'
}
})
// Show or hide login button when config is updated.
syrinscape.events.updateConfig.addListener(() => {
if (syrinscape.config.authenticated) {
document.querySelector('.login').style.display = 'none'
} else {
document.querySelector('.login').style.display = 'revert'
}
})
// Register 'global' frequency and waveform visualiser.
syrinscape.visualisation.add('global', () => {
// Get combined frequency and waveform data.
let data = syrinscape.player.audioEffectSystem.analyser.getData()
// Visualise frequency and waveform data.
// You can replace this section to visualise the data any way you like.
syrinscape.visualisation.d3VisualiseFrequencyData(data, '.d3-frequency')
syrinscape.visualisation.d3VisualiseWaveformData(data, '.d3-waveform')
// If analyser is still active, report that visualiser is still active.
// If no visualisers are active, visualisation loop will be paused.
return syrinscape.player.audioEffectSystem.analyser.isActive
})
syrinscape.player.init({
async configure() {
// Audio context. Leave undefined to create one.
syrinscape.config.audioContext = new AudioContext()
// Auth token.
syrinscape.config.token = prompt(
'Enter your Syrinscape auth token (from https://syrinscape.com/account/auth-token/). By default an anonymous token has been created for you. Leave blank to create a new one.',
syrinscape.config.token,
)
// Wait until async token change is finished, because `sessionId` might change.
await syrinscape.config.sync()
// Session ID. Use the GM's session ID for all users. Get from
// `syrinscape.config.sessionId` in the GM's browser.
syrinscape.config.sessionId = prompt(
"Enter the GM's session ID. By default a session has been created with you as the GM.",
syrinscape.config.sessionId,
)
},
onActive() {
// Hide CTA. Show interface.
document.querySelector('.cta').style.display = 'none'
document.querySelector('.interface').style.display = 'revert'
},
onInactive() {
// Show CTA. Hide interface.
document.querySelector('.cta').style.display = 'revert'
document.querySelector('.interface').style.display = 'none'
},
})
})()
</script>