From 8070a9db1de5a25afa999929951c771c44c90826 Mon Sep 17 00:00:00 2001 From: Benoit Giannangeli Date: Wed, 29 May 2019 11:37:35 +0200 Subject: [PATCH] Added an effect by voice Resolves #39 To change effect for a voice: If the effect is different than the current one, it's replaced (current one is disconnected and replaced by the new effect) --- desktop/main.js | 2 +- desktop/sources/links/main.css | 90 +++++++------ .../scripts/interface.channel.effect.js | 17 +++ desktop/sources/scripts/interface.js | 21 +++- desktop/sources/scripts/mixer.js | 119 +++++++++++++++--- 5 files changed, 191 insertions(+), 58 deletions(-) create mode 100644 desktop/sources/scripts/interface.channel.effect.js diff --git a/desktop/main.js b/desktop/main.js index af84d6e..3587467 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -23,7 +23,7 @@ app.on('ready', () => { require('electron').protocol.registerBufferProtocol('js', protocolHandler) app.win = new BrowserWindow({ - width: 445, + width: 607, height: 210, minWidth: 200, minHeight: 190, diff --git a/desktop/sources/links/main.css b/desktop/sources/links/main.css index ba6a176..68a800d 100644 --- a/desktop/sources/links/main.css +++ b/desktop/sources/links/main.css @@ -2,7 +2,7 @@ body { background:black; padding: 30px; font-family: 'input_mono_regular'; font- /* Mixer */ -#mixer { line-height: 15px; color:#666; position: relative; text-transform: uppercase; min-width: 420px} +#mixer { line-height: 15px; color:#666; position: relative; text-transform: uppercase; min-width: 582px} #mixer > div { position: absolute; width: 95px} #mixer > div.channel { position: absolute; } #mixer > div.effect { width:65px; } @@ -10,41 +10,57 @@ body { background:black; padding: 30px; font-family: 'input_mono_regular'; font- #mixer > div canvas { position: absolute; right: -10px;top: 0.5px;} #mixer span { padding: 0px 2.55px; } -#mixer #ch0 { top:calc(0 * 15px); left:0px; } -#mixer #ch1 { top:calc(1 * 15px); left:0px; } -#mixer #ch2 { top:calc(2 * 15px); left:0px; } -#mixer #ch3 { top:calc(3 * 15px); left:0px; } -#mixer #ch4 { top:calc(4 * 15px); left:0px; } -#mixer #ch5 { top:calc(5 * 15px); left:0px; } -#mixer #ch6 { top:calc(6 * 15px); left:0px; } -#mixer #ch7 { top:calc(7 * 15px); left:0px; } - -#mixer #ch8 { top:calc(0 * 15px); left:calc(1 * 110px); } -#mixer #ch9 { top:calc(1 * 15px); left:calc(1 * 110px); } -#mixer #cha { top:calc(2 * 15px); left:calc(1 * 110px); } -#mixer #chb { top:calc(3 * 15px); left:calc(1 * 110px); } -#mixer #chc { top:calc(4 * 15px); left:calc(1 * 110px); } -#mixer #chd { top:calc(5 * 15px); left:calc(1 * 110px); } -#mixer #che { top:calc(6 * 15px); left:calc(1 * 110px); } -#mixer #chf { top:calc(7 * 15px); left:calc(1 * 110px); } - -#mixer #chbit { top:calc(0 * 15px); left:calc(2 * 110px); } -#mixer #chdis { top:calc(1 * 15px); left:calc(2 * 110px); } -#mixer #chwah { top:calc(2 * 15px); left:calc(2 * 110px); } -#mixer #chche { top:calc(3 * 15px); left:calc(2 * 110px); } -#mixer #chfee { top:calc(4 * 15px); left:calc(2 * 110px); } -#mixer #chdel { top:calc(5 * 15px); left:calc(2 * 110px); } -#mixer #chtre { top:calc(6 * 15px); left:calc(2 * 110px); } -#mixer #chrev { top:calc(7 * 15px); left:calc(2 * 110px); } - -#mixer #chpha { top:calc(0 * 15px); left:calc(2 * 150px); } -#mixer #chvib { top:calc(1 * 15px); left:calc(2 * 150px); } -#mixer #chcho { top:calc(2 * 15px); left:calc(2 * 150px); } -#mixer #chste { top:calc(3 * 15px); left:calc(2 * 150px); } -#mixer #chequ { top:calc(4 * 15px); left:calc(2 * 150px); } -#mixer #chcom { top:calc(5 * 15px); left:calc(2 * 150px); } -#mixer #chvol { top:calc(6 * 15px); left:calc(2 * 150px); } -#mixer #chlim { top:calc(7 * 15px); left:calc(2 * 150px); } +#mixer #ch0 { top:calc(0 * 15px); left:0px; } +#mixer #ch0-fx { top:calc(0 * 15px); left:110px; } +#mixer #ch1 { top:calc(1 * 15px); left:0px; } +#mixer #ch1-fx { top:calc(1 * 15px); left:110px; } +#mixer #ch2 { top:calc(2 * 15px); left:0px; } +#mixer #ch2-fx { top:calc(2 * 15px); left:110px; } +#mixer #ch3 { top:calc(3 * 15px); left:0px; } +#mixer #ch3-fx { top:calc(3 * 15px); left:110px; } +#mixer #ch4 { top:calc(4 * 15px); left:0px; } +#mixer #ch4-fx { top:calc(4 * 15px); left:110px; } +#mixer #ch5 { top:calc(5 * 15px); left:0px; } +#mixer #ch5-fx { top:calc(5 * 15px); left:110px; } +#mixer #ch6 { top:calc(6 * 15px); left:0px; } +#mixer #ch6-fx { top:calc(6 * 15px); left:110px; } +#mixer #ch7 { top:calc(7 * 15px); left:0px; } +#mixer #ch7-fx { top:calc(7 * 15px); left:110px; } + +#mixer #ch8 { top:calc(0 * 15px); left:calc(1 * 110px + 1 * 81px); } +#mixer #ch8-fx { top:calc(0 * 15px); left:calc(2 * 110px + 1 * 81px); } +#mixer #ch9 { top:calc(1 * 15px); left:calc(1 * 110px + 1 * 81px); } +#mixer #ch9-fx { top:calc(1 * 15px); left:calc(2 * 110px + 1 * 81px); } +#mixer #cha { top:calc(2 * 15px); left:calc(1 * 110px + 1 * 81px); } +#mixer #cha-fx { top:calc(2 * 15px); left:calc(2 * 110px + 1 * 81px); } +#mixer #chb { top:calc(3 * 15px); left:calc(1 * 110px + 1 * 81px); } +#mixer #chb-fx { top:calc(3 * 15px); left:calc(2 * 110px + 1 * 81px); } +#mixer #chc { top:calc(4 * 15px); left:calc(1 * 110px + 1 * 81px); } +#mixer #chc-fx { top:calc(4 * 15px); left:calc(2 * 110px + 1 * 81px); } +#mixer #chd { top:calc(5 * 15px); left:calc(1 * 110px + 1 * 81px); } +#mixer #chd-fx { top:calc(5 * 15px); left:calc(2 * 110px + 1 * 81px); } +#mixer #che { top:calc(6 * 15px); left:calc(1 * 110px + 1 * 81px); } +#mixer #che-fx { top:calc(6 * 15px); left:calc(2 * 110px + 1 * 81px); } +#mixer #chf { top:calc(7 * 15px); left:calc(1 * 110px + 1 * 81px); } +#mixer #chf-fx { top:calc(7 * 15px); left:calc(2 * 110px + 1 * 81px); } + +#mixer #chbit { top:calc(0 * 15px); left:calc(2 * 110px + 2 * 81px); } +#mixer #chdis { top:calc(1 * 15px); left:calc(2 * 110px + 2 * 81px); } +#mixer #chwah { top:calc(2 * 15px); left:calc(2 * 110px + 2 * 81px); } +#mixer #chche { top:calc(3 * 15px); left:calc(2 * 110px + 2 * 81px); } +#mixer #chfee { top:calc(4 * 15px); left:calc(2 * 110px + 2 * 81px); } +#mixer #chdel { top:calc(5 * 15px); left:calc(2 * 110px + 2 * 81px); } +#mixer #chtre { top:calc(6 * 15px); left:calc(2 * 110px + 2 * 81px); } +#mixer #chrev { top:calc(7 * 15px); left:calc(2 * 110px + 2 * 81px); } + +#mixer #chpha { top:calc(0 * 15px); left:calc(2 * 110px + 3 * 81px); } +#mixer #chvib { top:calc(1 * 15px); left:calc(2 * 110px + 3 * 81px); } +#mixer #chcho { top:calc(2 * 15px); left:calc(2 * 110px + 3 * 81px); } +#mixer #chste { top:calc(3 * 15px); left:calc(2 * 110px + 3 * 81px); } +#mixer #chequ { top:calc(4 * 15px); left:calc(2 * 110px + 3 * 81px); } +#mixer #chcom { top:calc(5 * 15px); left:calc(2 * 110px + 3 * 81px); } +#mixer #chvol { top:calc(6 * 15px); left:calc(2 * 110px + 3 * 81px); } +#mixer #chlim { top:calc(7 * 15px); left:calc(2 * 110px + 3 * 81px); } /* Commander */ @@ -60,4 +76,4 @@ body { background:black; padding: 30px; font-family: 'input_mono_regular'; font- @media (max-width: 380px) { #pilot #mixer { columns:1 !important; min-width: 0px } #pilot #mixer > div { max-width: 100%} -} \ No newline at end of file +} diff --git a/desktop/sources/scripts/interface.channel.effect.js b/desktop/sources/scripts/interface.channel.effect.js new file mode 100644 index 0000000..d15de4e --- /dev/null +++ b/desktop/sources/scripts/interface.channel.effect.js @@ -0,0 +1,17 @@ +import EffectInterface from './interface.effect.js' + +export default function ChannelEffectInterface (pilot, channelId, id, node) { + EffectInterface.call(this, pilot, id, node) + + this.el.id = `ch${parseInt(channelId).toString(16)}-fx`; + + this.run = function (msg) { + if (!msg || msg.substr(0, 4).toLowerCase() !== `${parseInt(channelId).toString(16)}${id}`) { + return + } + + if (msg.substr(0, 4).toLowerCase() === `${parseInt(channelId).toString(16)}${id}`) { + this.operate(`${msg.substr(1)}`.substr(3)) + } + } +} diff --git a/desktop/sources/scripts/interface.js b/desktop/sources/scripts/interface.js index 2fc2ab3..9ac53ee 100644 --- a/desktop/sources/scripts/interface.js +++ b/desktop/sources/scripts/interface.js @@ -5,6 +5,7 @@ export default function Interface (pilot, id, node) { this.meter = new Tone.Meter(0.95) this.waveform = new Tone.Waveform(256) + this.effectId = id this.el = document.createElement('div') this.el.id = `ch${id}` this.canvas = document.createElement('canvas') @@ -35,15 +36,29 @@ export default function Interface (pilot, id, node) { host.appendChild(this.el) } + this.uninstall = function () { + this.el.remove() + } + this.start = function () { this.updateAll({}, true) - loop() + this.loop() + } + + this.stop = function () { + cancelAnimationFrame(this.loopId) } this.connect = function (node) { this.node.connect(node) } + this.disconnect = function (node) { + this.node.disconnect() + this.node.dispose() + } + + function draw (level) { if (pilot.animate !== true) { return } if (lastUpdate && performance.now() - lastUpdate < 30) { return } @@ -93,8 +108,8 @@ export default function Interface (pilot, id, node) { context.closePath() } - function loop () { - requestAnimationFrame(loop) + this.loop = function() { + this.loopId = requestAnimationFrame(this.loop.bind(this)) draw() } diff --git a/desktop/sources/scripts/mixer.js b/desktop/sources/scripts/mixer.js index f602adb..6621057 100644 --- a/desktop/sources/scripts/mixer.js +++ b/desktop/sources/scripts/mixer.js @@ -2,12 +2,57 @@ const Tone = require('tone') import ChannelInterface from './interface.channel.js' import EffectInterface from './interface.effect.js' +import ChannelEffectInterface from './interface.channel.effect.js' + +const EFFECTS = ['bit', 'dis', 'wah', 'che', 'fee', 'del', 'tre', 'rev', 'pha', 'vib', 'cho', 'ste', 'equ', 'com', 'vol', 'lim'] + +const getEffect = function(type) { + switch(type) { + case 'bit': + return new Tone.BitCrusher(4) + case 'dis': + return new Tone.Distortion(0.05) + case 'wah': + return new Tone.AutoWah(100, 6, 0) + case 'che': + return new Tone.Chebyshev(50) + case 'fee': + return new Tone.FeedbackDelay(0) + case 'del': + return new Tone.PingPongDelay('4n', 0.2) + case 'tre': + return new Tone.Tremolo() + case 'rev': + return new Tone.JCReverb(0) + case 'pha': + return new Tone.Phaser(0.5, 3, 350) + case 'vib': + return new Tone.Vibrato() + case 'cho': + return new Tone.Chorus(4, 2.5, 0.5) + case 'ste': + return new Tone.StereoWidener(0.5, 3, 350) + case 'equ': + return new Tone.EQ3(5, 0, 5) + case 'com': + return new Tone.Compressor(-6, 4) + case 'vol': + return new Tone.Volume(6) + case 'lim': + return new Tone.Limiter(-2) + } + + console.warn('Unknown effect', type) + + return null +} export default function Mixer (pilot) { this.el = document.createElement('div') this.el.id = 'mixer' this.channels = [] + this.channelsEffects = [] this.effects = {} this.install = function (host) { @@ -37,30 +82,37 @@ export default function Mixer (pilot) { this.channels[14] = new ChannelInterface(pilot, 14, new Tone.MembraneSynth({ 'octaves': 15, 'oscillator': { 'type': 'triangle' } })) this.channels[15] = new ChannelInterface(pilot, 15, new Tone.MembraneSynth({ 'octaves': 20, 'oscillator': { 'type': 'square' } })) + // One FX slot (default rev) for each channel + for (const id in this.channels) { + this.channelsEffects[id] = new ChannelEffectInterface(pilot, id, 'rev', getEffect('rev')) + } + // I - this.effects.bitcrusher = new EffectInterface(pilot, 'bit', new Tone.BitCrusher(4)) - this.effects.distortion = new EffectInterface(pilot, 'dis', new Tone.Distortion(0.05)) - this.effects.autowah = new EffectInterface(pilot, 'wah', new Tone.AutoWah(100, 6, 0)) - this.effects.chebyshev = new EffectInterface(pilot, 'che', new Tone.Chebyshev(50)) + this.effects.bitcrusher = new EffectInterface(pilot, 'bit', getEffect('bit')) + this.effects.distortion = new EffectInterface(pilot, 'dis', getEffect('dis')) + this.effects.autowah = new EffectInterface(pilot, 'wah', getEffect('wah')) + this.effects.chebyshev = new EffectInterface(pilot, 'che', getEffect('che')) // II - this.effects.feedback = new EffectInterface(pilot, 'fee', new Tone.FeedbackDelay(0)) - this.effects.delay = new EffectInterface(pilot, 'del', new Tone.PingPongDelay('4n', 0.2)) - this.effects.tremolo = new EffectInterface(pilot, 'tre', new Tone.Tremolo()) - this.effects.reverb = new EffectInterface(pilot, 'rev', new Tone.JCReverb(0)) + this.effects.feedback = new EffectInterface(pilot, 'fee', getEffect('fee')) + this.effects.delay = new EffectInterface(pilot, 'del', getEffect('del')) + this.effects.tremolo = new EffectInterface(pilot, 'tre', getEffect('tre')) + this.effects.reverb = new EffectInterface(pilot, 'rev', getEffect('rev')) // III - this.effects.phaser = new EffectInterface(pilot, 'pha', new Tone.Phaser(0.5, 3, 350)) - this.effects.vibrato = new EffectInterface(pilot, 'vib', new Tone.Vibrato()) - this.effects.chorus = new EffectInterface(pilot, 'cho', new Tone.Chorus(4, 2.5, 0.5)) - this.effects.widener = new EffectInterface(pilot, 'ste', new Tone.StereoWidener(0.5, 3, 350)) + this.effects.phaser = new EffectInterface(pilot, 'pha', getEffect('pha')) + this.effects.vibrato = new EffectInterface(pilot, 'vib', getEffect('vib')) + this.effects.chorus = new EffectInterface(pilot, 'cho', getEffect('cho')) + this.effects.widener = new EffectInterface(pilot, 'ste', getEffect('ste')) // Mastering - this.effects.equalizer = new EffectInterface(pilot, 'equ', new Tone.EQ3(5, 0, 5)) - this.effects.compressor = new EffectInterface(pilot, 'com', new Tone.Compressor(-6, 4)) - this.effects.volume = new EffectInterface(pilot, 'vol', new Tone.Volume(6)) - this.effects.limiter = new EffectInterface(pilot, 'lim', new Tone.Limiter(-2)) + this.effects.equalizer = new EffectInterface(pilot, 'equ', getEffect('equ')) + this.effects.compressor = new EffectInterface(pilot, 'com', getEffect('com')) + this.effects.volume = new EffectInterface(pilot, 'vol', getEffect('vol')) + this.effects.limiter = new EffectInterface(pilot, 'lim', getEffect('lim')) // Connect for (const id in this.channels) { - this.channels[id].connect(this.effects.bitcrusher.node) + this.channels[id].connect(this.channelsEffects[id].node) + + this.channelsEffects[id].node.connect(this.effects.bitcrusher.node) } this.effects.bitcrusher.connect(this.effects.distortion.node) @@ -85,6 +137,8 @@ export default function Mixer (pilot) { // Add all instruments to dom for (const id in this.channels) { this.channels[id].install(this.el) + + this.channelsEffects[id].install(this.el) } // Add all effects to dom @@ -99,6 +153,7 @@ export default function Mixer (pilot) { console.log('Synthetiser', 'Starting..') for (const id in this.channels) { this.channels[id].start() + this.channelsEffects[id].start() } for (const id in this.effects) { this.effects[id].start() @@ -131,6 +186,36 @@ export default function Mixer (pilot) { // Single for (const id in this.channels) { this.channels[id].run(msg) + + // Channel effect might have changed + if (msg && `${msg}`.substr(0, 1).toLowerCase() === id.toString(16)) { + let effect = `${msg}`.substr(1, 3).toLowerCase() + let channel = this.channels[id] + let channelEffect = this.channelsEffects[id] + + if (channel && channelEffect && + EFFECTS.indexOf(effect) > -1 && effect !== channelEffect.effectId) { + let newEffect = getEffect(effect) + + if (newEffect) { + // Remove current effect + this.channelsEffects[id].stop() + this.channelsEffects[id].uninstall() + this.channelsEffects[id].disconnect() + + // Install new one + this.channelsEffects[id] = new ChannelEffectInterface(pilot, id, effect, newEffect) + this.channelsEffects[id].install(this.el) + this.channelsEffects[id].start() + + // Put back in circuit + this.channels[id].connect(this.channelsEffects[id].node) + this.channelsEffects[id].node.connect(this.effects.bitcrusher.node) + } + } + } + + this.channelsEffects[id].run(msg) } for (const id in this.effects) { this.effects[id].run(msg)