Skip to content

Commit

Permalink
<soundstage-player> basic controls working
Browse files Browse the repository at this point in the history
  • Loading branch information
stephband committed Sep 20, 2023
1 parent babbddc commit 4383db4
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 8 deletions.
4 changes: 4 additions & 0 deletions modules/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ if (!AudioContext.prototype.getOutputTimestamp) {
};
};
}
else /*if (isSafari)*/ {
// TODO: Safari appears to get contextTime VERY wrong on the native
// getOutputTimestamp()... can we sanitise it?
}

export const context = new window.AudioContext();
context.destination.channelInterpretation = "discrete";
Expand Down
5 changes: 4 additions & 1 deletion modules/sequencer.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,10 @@ define(Sequencer.prototype, {
**/
time: {
get: function() {
return this.context.getOutputTimestamp().contextTime;
return this.context.getOutputTimestamp().contextTime - this.startTime;
},
set: function(time) {
console.log('TODO: set time', time);
}
},

Expand Down
14 changes: 14 additions & 0 deletions modules/sequencer/get-duration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

import by from '../../../fn/modules/by.js';
import get from '../../../fn/modules/get.js';
import last from '../../../fn/modules/last.js';

import { getDuration as getEventDuration } from '../event.js';

const by0 = by(get(0));

export function getSequenceDuration(sequence) {
// TODO: Account for tempo
const lastEvent = last(sequence.events.sort(by0));
return lastEvent[0] + getEventDuration(lastEvent);
}
12 changes: 6 additions & 6 deletions modules/soundstage.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,12 @@ export default function Soundstage(data = defaultData, settings = nothing) {

define(Soundstage.prototype, {
version: { value: 1 },
//time: getOwnPropertyDescriptor(Sequencer.prototype, 'time'),
//rate: getOwnPropertyDescriptor(Sequencer.prototype, 'rate'),
//tempo: getOwnPropertyDescriptor(Sequencer.prototype, 'tempo'),
//meter: getOwnPropertyDescriptor(Sequencer.prototype, 'meter'),
//beat: getOwnPropertyDescriptor(Sequencer.prototype, 'beat'),
//bar: getOwnPropertyDescriptor(Sequencer.prototype, 'bar'),
time: Object.getOwnPropertyDescriptor(Sequencer.prototype, 'time'),
//rate: Object.getOwnPropertyDescriptor(Sequencer.prototype, 'rate'),
//tempo: Object.getOwnPropertyDescriptor(Sequencer.prototype, 'tempo'),
//meter: Object.getOwnPropertyDescriptor(Sequencer.prototype, 'meter'),
//beat: Object.getOwnPropertyDescriptor(Sequencer.prototype, 'beat'),
//bar: Object.getOwnPropertyDescriptor(Sequencer.prototype, 'bar'),
status: Object.getOwnPropertyDescriptor(Sequencer.prototype, 'status'),

//blockDuration: getOwnPropertyDescriptor(Transport.prototype, 'blockDuration'),
Expand Down
278 changes: 278 additions & 0 deletions player/module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@

import Stream, { frames } from '../../fn/modules/stream.js';
import { formatTime } from '../../fn/modules/time.js';
import observe from '../../fn/observer/observe.js';
import Observer from '../../fn/observer/observer.js';
import delegate from '../../dom/modules/delegate.js';
import createBoolean from '../../dom/modules/element/create-boolean.js';
import createTokenList from '../../dom/modules/element/create-token-list.js';
import element, { getInternals } from '../../dom/modules/element.js';
import events from '../../dom/modules/events.js';
import request from '../../dom/modules/request.js';
import { getSequenceDuration } from '../modules/sequencer/get-duration.js'
import Soundstage from '../modules/soundstage.js';


export default element('soundstage-player', {
template: `
<style>
:host {
font-family: system-ui;
display: inline-flex;
align-items: center;
height: 3rem;
padding-left: 0.125rem;
padding-right: 0.625rem;
background-color: #cccccc;
border-radius: 1.5rem;
}
:host > [name="timeline"],
:host > [name="metronome"],
:host > time,
:host > .duration {
margin-left: 0.125rem;
}
:host > [name="timeline"] {
margin-top: 0;
margin-bottom: 0;
margin-left: 0.25rem;
margin-right: 0.25rem;
}
:host > .button {
margin-left: 0.25rem;
}
button, .button {
box-sizing: border-box;
cursor: pointer;
width: 0;
border-width: 0;
background: white;
}
time {
font-size: 0.75rem;
width: 2.25rem; /* Prevent layout jumping around as time changes */
overflow: visible;
}
.time { text-align: right; }
.duration { text-align: left; }
[name="start"] {
padding: 0 0 0 2.75rem;
height: 2.75rem;
overflow: hidden;
border-radius: 1.375rem;
background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">\
<path d="M4.4,3.2 L4.4,8.8 L8.8,6 Z"></path>\
</svg>');
}
[name="start"].playing {
background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">\
<path d="M4,4 L4,8 L8,8 L8,4 Z"></path>\
</svg>');
}
.button,
[name="metronome"],
[name="loop"] {
padding: 0 0 0 2.5rem;
height: 2.5rem;
overflow: hidden;
}
.button {
padding: 0 0 0 1.75rem;
height: 1.75rem;
border-radius: 0.875rem;
}
</style>
<button type="button" name="start">Play</button>
<time class="time">0:00</time>
<input type="range" name="timeline" min="0" max="10" step="any" value="0" />
<time class="duration">0:00</time>
<button type="button" name="metronome" title="Toggle metronome">Metronome</button>
<button type="button" name="loop" title="Toggle loop">Loop</button>
<a class="button" href="https://stephen.band/soundstage/">About Soundstage</a>
`,

construct: function(shadow, internals) {
// DOM
const dom = {
play: shadow.querySelector('[name="start"]'),
time: shadow.querySelector('.time'),
timeline: shadow.querySelector('[name="timeline"]'),
duration: shadow.querySelector('.duration')
};
internals.dom = dom;

// Soundstage data
var stage;
internals.datas = Stream.of();
internals.datas
.map((data) => new Soundstage(data))
.each((s) => {
// TEMP
stage = window.stage = internals.stage = s;
//observe('startTime', stage).each((v) => console.log('DD', v));
const duration = getSequenceDuration(stage);
dom.timeline.max = duration;
dom.duration.textContent = formatTime('m:ss', duration);
});

const updates = frames('frame').each((t) => {
dom.time.textContent = formatTime('m:ss', stage.time);
dom.timeline.value = stage.time;
});

// Controls
events('click', shadow)
.each(delegate({
'[name="start"]': (node) => {
if (stage.status === 'playing') {
stage.stop();
updates.stop();
dom.play.textContent = 'Play';
dom.play.classList.remove('playing');
// TODO: Stop other instances of soundstage-player?
}
else {
stage.start();
updates.start();
dom.play.textContent = 'Stop';
dom.play.classList.add('playing');
}
},

'[name="metronome"]': (node) => stage.metronome = !stage.metronome,
'[name="loop"]': (node) => stage.loop = !stage.looop
}));

events('input', shadow)
.each(delegate({
'[name="timeline"]': (node) => {
const time = parseFloat(node.value);
stage.time = time;
// This should update in reponse to data, not in response to input
dom.time.textContent = formatTime('m:ss', time);
}
}));
}
}, {
/**
controls=""
An attribute that accepts the tokens `"navigation"`, `"pagination"`
and `"fullscreen"`. The presence of one of these tokens enables the
corresponding controls.
```html
<slide-show controls="navigation fullscreen">…</slide-show>
```
**/

/**
.controls
A TokenList object (like `.classList`) that supports the tokens
`"navigation"`, `"pagination"` and `"fullscreen"`.
```js
slideshow.controls.add('pagination');
```
**/

controls: createTokenList({
'play': {
enable: function() {},
disable: function() {},
getState: function() {}
},

'time': {
enable: function() {},
disable: function() {},
getState: function() {}
},

'meter': {
enable: function() {},
disable: function() {},
getState: function() {}
},

'tempo': {
enable: function() {},
disable: function() {},
getState: function() {}
},

'metronome': {
enable: function() {},
disable: function() {},
getState: function() {}
},

'info': {
enable: function() {},
disable: function() {},
getState: function() {}
}
}),

start: {
value: function() {

}
},

stop: {
value: function() {

}
},

duration: {
get: function() {}
},

playing: {
get: function() {}
},

loop: {
attribute: function() {},
get: function() {},
set: function() {}
},

/**
src=""
A path to a Soundstage JSON file.
```html
<soundstage-player src="./tune.json">...</soundstage-player>
```
**/

src: {
attribute: function(value) { this.src = value; },
get: function() { return getInternals(this).src; },
set: function(value) {
const internals = getInternals(this);
internals.src = value;

// Add loading indication
internals.dom.play.classList.add('loading');

request('get', value, 'application/json')
// Push data into datas stream
.then((data) => internals.datas.push(data))
// Remove loading indication
.finally(() => internals.dom.play.classList.remove('loading'));
}
},
});
6 changes: 5 additions & 1 deletion test.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

<p>Click somewhere to launch the WebAudio clock: some tests – see them in the console – depend on it.</p>

<soundstage-player src="./test/soundstage-test.json"></soundstage-player>

<script>
window.DEBUG = true;
</script>
Expand Down Expand Up @@ -45,7 +47,9 @@
//import './nodes/test/instrument-test.js';

// Soundstage
import './test/soundstage-test.js';
//import './test/soundstage-test.js';

import SoundstagePlayer from './player/module.js';

// Report results
done((totals) => (totals.fail > 0 ? fail() : pass()));
Expand Down
31 changes: 31 additions & 0 deletions test/soundstage-test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"nodes": [
{ "id": "tone", "type": "tone", "node": { "attack": 0.004, "release": 0.02 } },
{ "id": "output", "type": "output" }
],

"connections": [
{ "source": "tone", "target": "output" }
],

"sequences": [{
"id": "test",
"events": [
[0, "note", 800, 0.6, 0.1],
[0.25, "note", 400, 0.7, 0.1],
[0.5, "note", 600, 0.8, 0.1],
[0.75, "note", 300, 0.8, 0.1],
[1, "note", 1200, 0.7, 0.8],
[2, "detune", -1, "step"],
[2, "note", 800, 0.6, 0.2],
[2.25, "note", 400, 0.7, 0.2],
[2.5, "note", 600, 0.8, 0.2],
[2.75, "note", 300, 0.8, 0.2],
[3, "note", 400, 0.7, 0.8]
]
}],

"events": [
[0, "sequence", "test", "tone", 4]
]
}

0 comments on commit 4383db4

Please sign in to comment.