From 4383db4a2cfb8538d0e35f1a8b4e735a89ee76ec Mon Sep 17 00:00:00 2001 From: Stephen Date: Wed, 20 Sep 2023 05:35:07 +0200 Subject: [PATCH] basic controls working --- modules/context.js | 4 + modules/sequencer.js | 5 +- modules/sequencer/get-duration.js | 14 ++ modules/soundstage.js | 12 +- player/module.js | 278 ++++++++++++++++++++++++++++++ test.html | 6 +- test/soundstage-test.json | 31 ++++ 7 files changed, 342 insertions(+), 8 deletions(-) create mode 100644 modules/sequencer/get-duration.js create mode 100644 player/module.js create mode 100644 test/soundstage-test.json diff --git a/modules/context.js b/modules/context.js index 16f6ccc..1e48740 100755 --- a/modules/context.js +++ b/modules/context.js @@ -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"; diff --git a/modules/sequencer.js b/modules/sequencer.js index d55da57..8842b49 100644 --- a/modules/sequencer.js +++ b/modules/sequencer.js @@ -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); } }, diff --git a/modules/sequencer/get-duration.js b/modules/sequencer/get-duration.js new file mode 100644 index 0000000..ee7df93 --- /dev/null +++ b/modules/sequencer/get-duration.js @@ -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); +} diff --git a/modules/soundstage.js b/modules/soundstage.js index 1df7776..bbb3d9d 100755 --- a/modules/soundstage.js +++ b/modules/soundstage.js @@ -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'), diff --git a/player/module.js b/player/module.js new file mode 100644 index 0000000..d901d32 --- /dev/null +++ b/player/module.js @@ -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: ` + + + + + + + + + About Soundstage + `, + + 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 + + ``` + **/ + + /** + .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 + ... + ``` + **/ + + 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')); + } + }, +}); diff --git a/test.html b/test.html index 682fb2f..a747eac 100755 --- a/test.html +++ b/test.html @@ -10,6 +10,8 @@

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

+ + @@ -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())); diff --git a/test/soundstage-test.json b/test/soundstage-test.json new file mode 100644 index 0000000..de2395e --- /dev/null +++ b/test/soundstage-test.json @@ -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] + ] +}