+
+
+
diff --git a/bin/web/assets/sound.svg b/bin/web/assets/sound.svg
new file mode 100644
index 0000000..d3876f2
--- /dev/null
+++ b/bin/web/assets/sound.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/bin/web/css/demo.css b/bin/web/css/demo.css
index 13b9fbc..6012523 100755
--- a/bin/web/css/demo.css
+++ b/bin/web/css/demo.css
@@ -926,3 +926,18 @@ h4{
/* ABOUT END */
+.audioWrapper {
+ position: absolute;
+ left: 0px;
+ bottom: 0px;
+ z-index: 42000;
+ background-color: rgba(255,125,125,0.5);
+ opacity: 0;
+ pointer-events: none;
+}
+.audioWrapper canvas.visualizer {
+ border-top: 1px solid black;
+ border-bottom: 1px solid black;
+ margin-bottom: -3px;
+ box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.7), 0 3px 4px rgba(0, 0, 0, 0.7);
+}
diff --git a/bin/web/js/audio.js b/bin/web/js/audio.js
new file mode 100644
index 0000000..ea49584
--- /dev/null
+++ b/bin/web/js/audio.js
@@ -0,0 +1,637 @@
+import {
+ mapValue,
+ mix,
+} from './utils.js';
+
+window.mapValue = mapValue;
+
+const Audio = function(tp, record) {
+ const audioDom = document.querySelector('.audioWrapper');
+ const heading = audioDom.querySelector("h1");
+ heading.textContent = "CLICK HERE TO START";
+ //document.body.addEventListener("click", init);
+ let started = false;
+
+ const mapping = {};
+
+ const addAudioOptions = (layer, propTitle) => {
+ const panelPropTitle = tp.getPanelPropTitle(propTitle);
+ if (panelPropTitle === null) {
+ console.log('Audio::addAudioOptions::error',`cannot find panelPropTitle "${propTitle}"`);
+ return;
+ }
+ const container = tp.getPanelPropContainer(panelPropTitle);
+ const mappingOptions = mapping[layer.id()][propTitle];
+ const panel = tp.getPanel();
+ const audioOptions = document.createElement('div');
+ audioOptions.classList.add('audioOptions');
+ audioOptions.classList.add('audioOptionsTypeDefault');
+ audioOptions.classList.add(`audioOptions${propTitle}`);
+ audioOptions.style.position = 'relative';
+ audioOptions.style.width = '100%';
+ audioOptions.style.background = 'rgba(0,255,255,0.2)';
+ audioOptions.style.order = window.getComputedStyle(container).order;
+
+ mappingOptions.freq_min = 0;
+ mappingOptions.freq_max = 256 * 8 / 2;
+
+ const updateMappingOptions = () => {
+ mappingOptions.min_out = parseFloat(panel.querySelector(`#audio_min${propTitle}`).value);
+ mappingOptions.max_out = parseFloat(panel.querySelector(`#audio_max${propTitle}`).value);
+ mappingOptions.sync =
+ panel.querySelector(`input[name="audio_sync${propTitle}"]:checked`).value;
+ const s = panel.querySelector(`#audio_smoothing${propTitle}`).value;
+ mappingOptions.smoothing = parseFloat(s);
+ };
+
+ const min_max_Dom = document.createElement('div');
+ min_max_Dom.classList.add('audio_min_max');
+ const min_inputDom_label = document.createElement('label');
+ min_inputDom_label.for = 'audio_min';
+ min_inputDom_label.innerHTML = 'audio_min';
+ const min_inputDom = document.createElement('input');
+ min_inputDom.type = 'number';
+ min_inputDom.name = `audio_min${propTitle}`;
+ min_inputDom.id = `audio_min${propTitle}`;
+ min_inputDom.value = '0';
+ const max_inputDom_label = document.createElement('label');
+ max_inputDom_label.for = 'audio_max';
+ max_inputDom_label.innerHTML = 'audio_max';
+ const max_inputDom = document.createElement('input');
+ max_inputDom.type = 'number';
+ max_inputDom.name = `audio_max${propTitle}`;
+ max_inputDom.id = `audio_max${propTitle}`;
+ max_inputDom.value = '255';
+ const smoothing_inputDom_label = document.createElement('label');
+ smoothing_inputDom_label.for = 'audio_smoothing';
+ smoothing_inputDom_label.innerHTML = 'audio_smoothing';
+ const smoothing_inputDom = document.createElement('input');
+ smoothing_inputDom.type = 'number';
+ smoothing_inputDom.name = `audio_smoothing${propTitle}`;
+ smoothing_inputDom.id = `audio_smoothing${propTitle}`;
+ smoothing_inputDom.value = config.audio.defaultSmoothing;
+ smoothing_inputDom.min = 0;
+ smoothing_inputDom.max = 1;
+ smoothing_inputDom.step = 0.01;
+ min_max_Dom.append(smoothing_inputDom_label);
+ min_max_Dom.append(smoothing_inputDom);
+ min_max_Dom.append(min_inputDom_label);
+ min_max_Dom.append(min_inputDom);
+ min_max_Dom.append(max_inputDom_label);
+ min_max_Dom.append(max_inputDom);
+ audioOptions.append(min_max_Dom);
+
+ const sync_Dom = document.createElement('div');
+ const sync_titleDom = document.createElement('p');
+ sync_titleDom.innerHTML = 'sync with:';
+ sync_Dom.append(sync_titleDom);
+
+ const sync_options = ['volume', 'pitch', 'frequency'];
+ sync_options.forEach((o, oi) => {
+ const sync_inputDom_label = document.createElement('label');
+ sync_inputDom_label.for = `audio_sync${o}`;
+ sync_inputDom_label.innerHTML = o;
+ const sync_inputDom = document.createElement('input');
+ sync_inputDom.type = 'radio';
+ sync_inputDom.name = `audio_sync${propTitle}`;
+ sync_inputDom.id = `audio_sync${propTitle}${o}`;
+ sync_inputDom.value = o;
+ // default select first option
+ if (oi === 0) {
+ sync_inputDom.checked = '1';
+ }
+ sync_Dom.append(sync_inputDom_label);
+ sync_Dom.append(sync_inputDom);
+ sync_inputDom.addEventListener('change', updateMappingOptions);
+ });
+ audioOptions.append(sync_Dom);
+
+ const fft_Dom = document.createElement('div');
+ const fft_imgDom = document.createElement('img');
+ const fft_selectDom = document.createElement('div');
+ fft_Dom.style.position = 'relative';
+ fft_Dom.style.top = '0px';
+ fft_Dom.style.left = '0px';
+ fft_imgDom.classList.add('audio_fft');
+ fft_imgDom.style.width = '100%';
+ fft_imgDom.style.userDrag = 'none';
+ fft_imgDom.style.userSelect = 'none';
+ fft_imgDom.style.pointerEvents = 'none';
+ fft_selectDom.style.position = 'absolute';
+ fft_selectDom.style.top = '0px';
+ fft_selectDom.style.left = '0px';
+ fft_selectDom.style.width = '100%';
+ fft_selectDom.style.height = '100%';
+ fft_selectDom.style.pointerEvents = 'none';
+ fft_selectDom.style.backgroundColor = 'rgba(0,255,0,0.2)';
+ fft_selectDom.style.border = '1px solid rgba(0,255,0,1.0)';
+ fft_Dom.append(fft_imgDom);
+ fft_Dom.append(fft_selectDom);
+ audioOptions.append(fft_Dom);
+ min_inputDom.addEventListener('change', updateMappingOptions);
+ max_inputDom.addEventListener('change', updateMappingOptions);
+ smoothing_inputDom.addEventListener('change', updateMappingOptions);
+ let setFrequency = false;
+ let freq_down = 0;
+ let freq_up = 0;
+ fft_Dom.addEventListener('mousedown', (e) => {
+ setFrequency = true;
+ const bb = fft_Dom.getBoundingClientRect();
+ const x = e.clientX - bb.x;
+ freq_down = mapValue(e.clientX, bb.x, bb.x + bb.width, 0, 256 * 8 / 2, true);
+ });
+ fft_Dom.addEventListener('mouseup', (e) => {
+ setFrequency = false;
+ const bb = fft_Dom.getBoundingClientRect();
+ const x = e.clientX - bb.x;
+ freq_down = mapValue(e.clientX, bb.x, bb.x + bb.width, 0, 256 * 8 / 2, true);
+ console.log('up',JSON.parse(JSON.stringify(e)), e);
+ });
+
+ //removeAudioOptions();
+ container.after(audioOptions);
+
+ updateMappingOptions();
+ mappingOptions.value = mappingOptions.min_out;
+ };
+
+ const removeAudioOptions = (propTitle = '') => {
+ const panel = tp.getPanel();
+ if (propTitle === '') {
+ const otherAudioOptions = panel.querySelectorAll('.audioOptions');
+ if (otherAudioOptions !== null) {
+ for (let i = 0; i < otherAudioOptions.length; i++) {
+ otherAudioOptions[i].remove();
+ }
+ }
+ } else {
+ const audioOptions = panel.querySelector(`.audioOptions${propTitle}`);
+ if (audioOptions !== null) {
+ audioOptions.remove();
+ }
+ }
+ };
+
+ const addAudioButton = (layer, propTitle, isActive) => {
+ const panel = tp.getPanel();
+ const panelPropTitle = tp.getPanelPropTitle(propTitle);
+ if (panelPropTitle !== null) {
+ const container = tp.getPanelPropContainer(panelPropTitle);
+
+ if (container === null) {
+ console.log("Audio::addAudioButton",
+ `impossible! cannot find panelPropContainer for ${propTitle}`);
+ } else if (container.querySelector('.audioButton') !== null) {
+ console.log("Audio::addAudioButton",
+ `already added an audio button for ${propTitle}`);
+ } else {
+ const button = document.createElement('div');
+ button.classList.add('audioButton');
+ button.classList.add(`audioButton${propTitle}`);
+ button.innerHTML = `
`;
+ container.append(button);
+ button.addEventListener('click', () => {
+ if (!started) {
+ init();
+ }
+ if (!mapping.hasOwnProperty(layer.id())) {
+ mapping[layer.id()] = {};
+ }
+ if (!mapping[layer.id()].hasOwnProperty(propTitle)) {
+ mapping[layer.id()][propTitle] = {};
+ button.classList.add('active');
+ addAudioOptions(layer, propTitle);
+ } else {
+ delete mapping[layer.id()][propTitle];
+ if (Object.keys(mapping[layer.id()]).length === 0) {
+ delete mapping[layer.id()];
+ }
+ button.classList.remove('active');
+ removeAudioOptions(propTitle);
+ }
+ });
+ if (isActive) {
+ button.classList.add('active');
+ addAudioOptions(layer, propTitle);
+ }
+ }
+ } else {
+ console.log("Audio::addAudioButton",
+ `cannot find panelPropTitle for ${propTitle}`);
+ }
+ };
+
+ const injectPanel = (layer) => {
+ const props = Object.keys(layer.theatreObject.value);
+ props.forEach((propTitle) => {
+ if (config.audio.ignoreProps.indexOf(propTitle) < 0) {
+ let isActive = false;
+ if (mapping.hasOwnProperty(layer.id())) {
+ if (mapping[layer.id()].hasOwnProperty(propTitle)) {
+ isActive = true;
+ }
+ }
+ addAudioButton(layer, propTitle, isActive);
+ }
+ });
+ };
+
+ function init() {
+ started = true;
+ heading.textContent = "Voice-change-O-matic";
+ //document.body.removeEventListener("click", init);
+
+ // Older browsers might not implement mediaDevices at all, so we set an empty object first
+ if (navigator.mediaDevices === undefined) {
+ navigator.mediaDevices = {};
+ }
+
+ // Some browsers partially implement mediaDevices. We can't assign an object
+ // with getUserMedia as it would overwrite existing properties.
+ // Add the getUserMedia property if it's missing.
+ if (navigator.mediaDevices.getUserMedia === undefined) {
+ navigator.mediaDevices.getUserMedia = function(constraints) {
+ // First get ahold of the legacy getUserMedia, if present
+ const getUserMedia =
+ navigator.webkitGetUserMedia ||
+ navigator.mozGetUserMedia ||
+ navigator.msGetUserMedia;
+
+ // Some browsers just don't implement it - return a rejected promise with an error
+ // to keep a consistent interface
+ if (!getUserMedia) {
+ return Promise.reject(
+ new Error("getUserMedia is not implemented in this browser")
+ );
+ }
+
+ // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
+ return new Promise(function(resolve, reject) {
+ getUserMedia.call(navigator, constraints, resolve, reject);
+ });
+ };
+ }
+
+ // Set up forked web audio context, for multiple browsers
+ // window. is needed otherwise Safari explodes
+ const audioCtx = new(window.AudioContext || window.webkitAudioContext)();
+ const voiceSelect = audioDom.querySelector("#voice");
+ let source;
+ let stream;
+
+ // Grab the mute button to use below
+ const mute = audioDom.querySelector(".mute");
+
+ // Set up the different audio nodes we will use for the app
+ const analyser = audioCtx.createAnalyser();
+ analyser.minDecibels = -90;
+ analyser.maxDecibels = -10;
+ analyser.smoothingTimeConstant = 0.85;
+ window.analyser = analyser;
+
+ const distortion = audioCtx.createWaveShaper();
+ const gainNode = audioCtx.createGain();
+ const biquadFilter = audioCtx.createBiquadFilter();
+ const convolver = audioCtx.createConvolver();
+
+ const echoDelay = createEchoDelayEffect(audioCtx);
+
+ // Distortion curve for the waveshaper, thanks to Kevin Ennis
+ // http://stackoverflow.com/questions/22312841/waveshaper-node-in-webaudio-how-to-emulate-distortion
+ function makeDistortionCurve(amount) {
+ let k = typeof amount === "number" ? amount : 50,
+ n_samples = 44100,
+ curve = new Float32Array(n_samples),
+ deg = Math.PI / 180,
+ i = 0,
+ x;
+ for (; i < n_samples; ++i) {
+ x = (i * 2) / n_samples - 1;
+ curve[i] = ((3 + k) * x * 20 * deg) / (Math.PI + k * Math.abs(x));
+ }
+ return curve;
+ }
+
+ // Grab audio track via XHR for convolver node
+ let soundSource;
+ const ajaxRequest = new XMLHttpRequest();
+
+ ajaxRequest.open(
+ "GET",
+ "https://mdn.github.io/voice-change-o-matic/audio/concert-crowd.ogg",
+ true
+ );
+
+ ajaxRequest.responseType = "arraybuffer";
+
+ ajaxRequest.onload = function() {
+ const audioData = ajaxRequest.response;
+
+ audioCtx.decodeAudioData(
+ audioData,
+ function(buffer) {
+ soundSource = audioCtx.createBufferSource();
+ convolver.buffer = buffer;
+ },
+ function(e) {
+ console.log("Error with decoding audio data" + e.err);
+ }
+ );
+ };
+
+ ajaxRequest.send();
+
+ // Set up canvas context for visualizer
+ const canvas = audioDom.querySelector(".visualizer");
+ const canvasCtx = canvas.getContext("2d");
+
+ const intendedWidth = audioDom.clientWidth;
+ canvas.setAttribute("width", 256 * 8 / 2);
+ const visualSelect = audioDom.querySelector("#visual");
+ let drawVisual;
+
+ // Main block for doing the audio recording
+ if (navigator.mediaDevices.getUserMedia) {
+ console.log("getUserMedia supported.");
+ const constraints = {
+ audio: true
+ };
+ navigator.mediaDevices
+ .getUserMedia(constraints)
+ .then(function(stream) {
+ source = audioCtx.createMediaStreamSource(stream);
+ source.connect(distortion);
+ distortion.connect(biquadFilter);
+ biquadFilter.connect(gainNode);
+ convolver.connect(gainNode);
+ echoDelay.placeBetween(gainNode, analyser);
+ analyser.connect(audioCtx.destination);
+
+ visualize();
+ voiceChange();
+ })
+ .catch(function(err) {
+ console.log("The following gUM error occured: " + err);
+ });
+ } else {
+ console.log("getUserMedia not supported on your browser!");
+ }
+
+ function visualize() {
+ const WIDTH = canvas.width;
+ const HEIGHT = canvas.height;
+
+ const visualSetting = visualSelect.value;
+
+ if (visualSetting === "sinewave") {
+ analyser.fftSize = 2048;
+ const bufferLength = analyser.fftSize;
+
+ // We can use Float32Array instead of Uint8Array if we want higher precision
+ // const dataArray = new Float32Array(bufferLength);
+ const dataArray = new Uint8Array(bufferLength);
+
+ canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
+
+ const draw = function() {
+ drawVisual = requestAnimationFrame(draw);
+
+ analyser.getByteTimeDomainData(dataArray);
+
+ canvasCtx.fillStyle = "rgb(200, 200, 200)";
+ canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
+
+ canvasCtx.lineWidth = 2;
+ canvasCtx.strokeStyle = "rgb(0, 0, 0)";
+
+ canvasCtx.beginPath();
+
+ const sliceWidth = (WIDTH * 1.0) / bufferLength;
+ let x = 0;
+
+ for (let i = 0; i < bufferLength; i++) {
+ let v = dataArray[i] / 128.0;
+ let y = (v * HEIGHT) / 2;
+
+ if (i === 0) {
+ canvasCtx.moveTo(x, y);
+ } else {
+ canvasCtx.lineTo(x, y);
+ }
+
+ x += sliceWidth;
+ }
+
+ canvasCtx.lineTo(canvas.width, canvas.height / 2);
+ canvasCtx.stroke();
+ };
+
+ draw();
+ } else if (visualSetting == "frequencybars") {
+ analyser.fftSize = 256 * 8;
+ const bufferLengthAlt = analyser.frequencyBinCount / 2;
+
+ // See comment above for Float32Array()
+ const dataArrayAlt = new Uint8Array(bufferLengthAlt);
+
+ canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
+
+ let frameCount = 0;
+ const drawAlt = function() {
+ drawVisual = requestAnimationFrame(drawAlt);
+
+ analyser.getByteFrequencyData(dataArrayAlt);
+
+ canvasCtx.fillStyle = "rgb(0, 0, 0)";
+ canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
+
+ const barWidth = (WIDTH / bufferLengthAlt) * 2.5;
+ let barHeight;
+ let x = 0;
+
+ let max_i = 0;
+ let max_v = 0;
+ for (let i = 0; i < bufferLengthAlt; i++) {
+ barHeight = dataArrayAlt[i];
+
+ if (barHeight > max_v) {
+ max_v = barHeight;
+ max_i = i;
+ }
+ canvasCtx.fillStyle = "rgb(" + (barHeight + 100) + ",50,50)";
+ canvasCtx.fillRect(
+ x,
+ HEIGHT - barHeight / 2,
+ barWidth,
+ barHeight / 2
+ );
+
+ x += barWidth + 1;
+ }
+ const propsToSet = [];
+ getLayers().forEach((layer) => {
+ if (mapping.hasOwnProperty(layer.id())) {
+ Object.keys(mapping[layer.id()]).forEach((propTitle) => {
+ const m = mapping[layer.id()][propTitle];
+ switch(m.sync) {
+ case 'volume': {
+ let a = mapValue(max_v, 0, 255, m.min_out, m.max_out, true);
+ m.value = m.value * m.smoothing + (1.0 - m.smoothing) * a;
+ propsToSet.push({
+ prop: layer.theatreObject.props[propTitle],
+ value: m.value,
+ });
+ break;
+ }
+ case 'pitch': {
+ let a = mapValue(max_i, 0, bufferLengthAlt-1, m.min_out, m.max_out, true);
+ m.value = m.value * m.smoothing + (1.0 - m.smoothing) * a;
+ propsToSet.push({
+ prop: layer.theatreObject.props[propTitle],
+ value: m.value,
+ });
+ break;
+ }
+ default:
+ break;
+ }
+ if (m.sync === 'volume') {
+ }
+ });
+ }
+ });
+ if (propsToSet.length > 0 && frameCount % 2 === 0) {
+ tp.studio.transaction(({
+ set
+ }) => {
+ propsToSet.forEach((p) => {
+ set(p.prop, p.value, true);
+ });
+ });
+ }
+ const panel = tp.getPanel();
+ const fft_images = panel.querySelectorAll('.audio_fft');
+ if (fft_images !== null) {
+ const src = canvas.toDataURL();
+ if (window.printDebug === true) {
+ console.log({canvas, src, fft_images, panel}, "DEBUG AUDIO");
+ }
+ fft_images.forEach((e) => {
+ e.src = src;
+ });
+ }
+ frameCount++;
+ };
+ drawAlt();
+ } else if (visualSetting == "off") {
+ canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
+ canvasCtx.fillStyle = "red";
+ canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
+ }
+ }
+
+ function voiceChange() {
+ distortion.oversample = "4x";
+ biquadFilter.gain.setTargetAtTime(0, audioCtx.currentTime, 0);
+
+ const voiceSetting = voiceSelect.value;
+
+ if (echoDelay.isApplied()) {
+ echoDelay.discard();
+ }
+
+ // When convolver is selected it is connected back into the audio path
+ if (voiceSetting == "convolver") {
+ biquadFilter.disconnect(0);
+ biquadFilter.connect(convolver);
+ } else {
+ biquadFilter.disconnect(0);
+ biquadFilter.connect(gainNode);
+
+ if (voiceSetting == "distortion") {
+ distortion.curve = makeDistortionCurve(400);
+ } else if (voiceSetting == "biquad") {
+ biquadFilter.type = "lowshelf";
+ biquadFilter.frequency.setTargetAtTime(1000, audioCtx.currentTime, 0);
+ biquadFilter.gain.setTargetAtTime(25, audioCtx.currentTime, 0);
+ } else if (voiceSetting == "delay") {
+ echoDelay.apply();
+ } else if (voiceSetting == "off") {
+ console.log("Voice settings turned off");
+ }
+ }
+ }
+
+ function createEchoDelayEffect(audioContext) {
+ const delay = audioContext.createDelay(1);
+ const dryNode = audioContext.createGain();
+ const wetNode = audioContext.createGain();
+ const mixer = audioContext.createGain();
+ const filter = audioContext.createBiquadFilter();
+
+ delay.delayTime.value = 0.75;
+ dryNode.gain.value = 1;
+ wetNode.gain.value = 0;
+ filter.frequency.value = 1100;
+ filter.type = "highpass";
+
+ return {
+ apply: function() {
+ wetNode.gain.setValueAtTime(0.75, audioContext.currentTime);
+ },
+ discard: function() {
+ wetNode.gain.setValueAtTime(0, audioContext.currentTime);
+ },
+ isApplied: function() {
+ return wetNode.gain.value > 0;
+ },
+ placeBetween: function(inputNode, outputNode) {
+ inputNode.connect(delay);
+ delay.connect(wetNode);
+ wetNode.connect(filter);
+ filter.connect(delay);
+
+ inputNode.connect(dryNode);
+ dryNode.connect(mixer);
+ wetNode.connect(mixer);
+ mixer.connect(outputNode);
+ },
+ };
+ }
+
+ // Event listeners to change visualize and voice settings
+ visualSelect.onchange = function() {
+ window.cancelAnimationFrame(drawVisual);
+ visualize();
+ };
+
+ voiceSelect.onchange = function() {
+ voiceChange();
+ };
+
+ mute.onclick = voiceMute;
+
+ let previousGain;
+
+ function voiceMute() {
+ if (mute.id === "") {
+ previousGain = gainNode.gain.value;
+ gainNode.gain.value = 0;
+ mute.id = "activated";
+ mute.innerHTML = "Unmute";
+ } else {
+ gainNode.gain.value = previousGain;
+ mute.id = "";
+ mute.innerHTML = "Mute";
+ }
+ }
+ }
+
+ this.init = init;
+ this.injectPanel = injectPanel;
+ this.mapping = mapping;
+};
+
+export {
+ Audio
+}
diff --git a/bin/web/js/config.js b/bin/web/js/config.js
index bdd5ed5..ad719c6 100644
--- a/bin/web/js/config.js
+++ b/bin/web/js/config.js
@@ -81,6 +81,10 @@ const config = {
zoomBaseFactor: 0.001,
zoomDynamicMax: 42,
},
+ audio: {
+ ignoreProps: ['transformOrigin', 'fontFamily', 'text', 'mirror_x', 'mirror_y', 'mirror_xy', 'fontVariationAxes', 'color'],
+ defaultSmoothing: 0.7,
+ },
midi: {
touchTimeThreshold_s: 0.5,
smoothingMix: 0.1,
diff --git a/bin/web/js/layer.js b/bin/web/js/layer.js
index 26bebab..df3d77f 100644
--- a/bin/web/js/layer.js
+++ b/bin/web/js/layer.js
@@ -792,6 +792,12 @@ const Layer = function(tp, layerID, fontsAndAxes, autoInit = true) {
panel.addEventListener("mouseover", showBoundingBoxDivIfSelected);
panel.addEventListener("mouseleave", hideBoundingBoxDiv);
+ if (typeof audio === 'object' && audio.hasOwnProperty('injectPanel')) {
+ audio.injectPanel(this);
+ } else {
+ console.log('Layer::findInjectPanel', `cannot inject audio panel for ${this.id()} for some reason.`);
+ }
+
injectedPanel = true;
const detail = {titles: Object.keys(panelPropTitles), containers: Object.keys(panelPropContainers)};
const e = new CustomEvent('injected', {detail});
diff --git a/bin/web/js/main.js b/bin/web/js/main.js
index f2984ad..19a25b1 100644
--- a/bin/web/js/main.js
+++ b/bin/web/js/main.js
@@ -26,6 +26,14 @@ import {
ModuleFS
} from './moduleFS.js';
+import {
+ Audio
+} from './audio.js';
+
+import {
+ Record
+} from './record.js';
+
//import {
//MidiController
//} from './midiController.js';
@@ -63,6 +71,10 @@ const exporter = new Exporter();
const interactor = new Interactor();
const moduleFS = new ModuleFS();
window.moduleFS = moduleFS;
+const record = new Record(tp);
+window.debug_record = record;
+const audio = new Audio(tp, record); // possibly nicer if we pass tp instead of attaching to window
+window.audio = audio;
window.panelFinderTimeout = false;
const sequenceEventBuffer = {};
diff --git a/bin/web/js/theatre-play.js b/bin/web/js/theatre-play.js
index 35674c1..501ccc3 100644
--- a/bin/web/js/theatre-play.js
+++ b/bin/web/js/theatre-play.js
@@ -569,6 +569,12 @@ const TheatrePlay = function(autoInit = false) {
align-self: end;
margin-top: 5px;
}
+ .audioButton{
+ width: 20px;
+ }
+ .audioButton.active{
+ background: green;
+ }
`;
this.shadowRoot.appendChild(style);
}