├── pic ├── maudio_xponent.png ├── novation_twitch.png └── korg_nanokontrol2.jpg ├── .github └── FUNDING.yml ├── .gitignore ├── docco ├── public │ ├── fonts │ │ ├── aller-bold.eot │ │ ├── aller-bold.ttf │ │ ├── aller-bold.woff │ │ ├── aller-light.eot │ │ ├── aller-light.ttf │ │ ├── aller-light.woff │ │ ├── novecento-bold.eot │ │ ├── novecento-bold.ttf │ │ └── novecento-bold.woff │ └── stylesheets │ │ └── normalize.css ├── docco.jst └── docco.css ├── .travis.yml ├── shell.nix ├── index.js ├── bin └── mixco.js ├── src ├── console.litcoffee ├── util.litcoffee ├── value.litcoffee ├── transform.litcoffee ├── script.litcoffee ├── control.litcoffee ├── cli.litcoffee └── behaviour.litcoffee ├── package.json ├── test ├── mixco │ ├── value.spec.coffee │ ├── script.spec.coffee │ └── control.spec.coffee ├── mock.litcoffee └── scripts.spec.coffee ├── makefile ├── script ├── korg_nanokontrol2.mixco.litcoffee ├── maudio_xponent.mixco.litcoffee └── novation_twitch.mixco.js └── README.md /pic/maudio_xponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/pic/maudio_xponent.png -------------------------------------------------------------------------------- /pic/novation_twitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/pic/novation_twitch.png -------------------------------------------------------------------------------- /pic/korg_nanokontrol2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/pic/korg_nanokontrol2.jpg -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: arximboldi 2 | patreon: sinusoidal 3 | custom: ["paypal.me/sinusoidal", sinusoid.al] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/ 2 | node_modules/ 3 | out/ 4 | tmp/ 5 | npm-debug.log 6 | lib/ 7 | coverage/ 8 | mixco-output/ 9 | -------------------------------------------------------------------------------- /docco/public/fonts/aller-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/docco/public/fonts/aller-bold.eot -------------------------------------------------------------------------------- /docco/public/fonts/aller-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/docco/public/fonts/aller-bold.ttf -------------------------------------------------------------------------------- /docco/public/fonts/aller-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/docco/public/fonts/aller-bold.woff -------------------------------------------------------------------------------- /docco/public/fonts/aller-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/docco/public/fonts/aller-light.eot -------------------------------------------------------------------------------- /docco/public/fonts/aller-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/docco/public/fonts/aller-light.ttf -------------------------------------------------------------------------------- /docco/public/fonts/aller-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/docco/public/fonts/aller-light.woff -------------------------------------------------------------------------------- /docco/public/fonts/novecento-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/docco/public/fonts/novecento-bold.eot -------------------------------------------------------------------------------- /docco/public/fonts/novecento-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/docco/public/fonts/novecento-bold.ttf -------------------------------------------------------------------------------- /docco/public/fonts/novecento-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arximboldi/mixco/HEAD/docco/public/fonts/novecento-bold.woff -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | script: npm run-script test-coverage 5 | after_script: "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls" 6 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs ? (import {}).fetchFromGitHub { 2 | owner = "NixOS"; 3 | repo = "nixpkgs"; 4 | rev = "afe9649210cace6d3ee9046684d4ea27dc4fd15d"; 5 | sha256 = "19w9cvf8kn369liz3yxab4kam1pbqgn5zlry3832g29w82jwpz2l"; 6 | }}: 7 | 8 | with import nixpkgs {}; 9 | 10 | stdenv.mkDerivation rec { 11 | name = "mixco-dev"; 12 | buildInputs = [ 13 | nodejs-8_x 14 | ]; 15 | shellHook = '' 16 | export REPO_ROOT=`dirname ${toString ./shell.nix}` 17 | addToSearchPath PATH "$REPO_ROOT"/node_modules/.bin 18 | ''; 19 | } 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // mixco 2 | // ===== 3 | 4 | if (process.env.MIXCO_USE_SOURCE) { 5 | require('coffee-script/register') 6 | module.exports = { 7 | behaviour: require('./src/behaviour'), 8 | control: require('./src/control'), 9 | script: require('./src/script'), 10 | transform: require('./src/transform'), 11 | util: require('./src/util'), 12 | value: require('./src/value') 13 | } 14 | } else { 15 | module.exports = { 16 | behaviour: require('./lib/behaviour'), 17 | control: require('./lib/control'), 18 | script: require('./lib/script'), 19 | transform: require('./lib/transform'), 20 | util: require('./lib/util'), 21 | value: require('./lib/value') 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bin/mixco.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // 3 | // mixco main script 4 | // ================= 5 | // 6 | 7 | if (process.env.MIXCO_COVERAGE) { 8 | require('coffee-script/register') 9 | require('coffee-coverage/register-istanbul') 10 | } 11 | 12 | if (process.env.MIXCO_USE_SOURCE) { 13 | require('coffee-script/register') 14 | require('../src/cli').main() 15 | } else { 16 | require('../lib/cli').main() 17 | } 18 | 19 | // > Copyright (C) 2015 Juan Pedro Bolívar Puente 20 | // > 21 | // > This program is free software: you can redistribute it and/or 22 | // > modify it under the terms of the GNU General Public License as 23 | // > published by the Free Software Foundation, either version 3 of the 24 | // > License, or (at your option) any later version. 25 | // > 26 | // > This program is distributed in the hope that it will be useful, 27 | // > but WITHOUT ANY WARRANTY; without even the implied warranty of 28 | // > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 29 | // > GNU General Public License for more details. 30 | // > 31 | // > You should have received a copy of the GNU General Public License 32 | // > along with this program. If not, see . 33 | -------------------------------------------------------------------------------- /src/console.litcoffee: -------------------------------------------------------------------------------- 1 | mixco.console 2 | ============= 3 | 4 | > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | > - **View me [on a static web](http://sinusoid.es/mixco/src/console.html)** 6 | > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/src/console.litcoffee)** 7 | 8 | This module is a lightweight version of 9 | [*console-browserify*](https://npmjs.org/package/console-browserify). 10 | It provides a console object in contexts where there is none, like for 11 | example Mixxx. 12 | 13 | 14 | 15 | {assert} = require './util' 16 | 17 | konsole = this 18 | exports = konsole 19 | 20 | konsole.log ?= -> print "" + arguments 21 | konsole.info ?= konsole.log 22 | konsole.warn ?= konsole.log 23 | konsole.error ?= konsole.log 24 | konsole.time ?= -> assert False, "time not implemented in konsole" 25 | konsole.timeEnd ?= -> assert False, "time not implemented in konsole" 26 | konsole.trace ?= -> 27 | err = new Error() 28 | err.name = "Trace" 29 | err.message = "" + arguments 30 | konsole.error err.stack 31 | konsole.dir ?= -> konsole.log object + "\n" 32 | konsole.assert ?= assert 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mixco", 3 | "description": "Mixco is a framework for making DJ controller scripts for Mixxx.", 4 | "author": "Juan Pedro Bolivar Puente ", 5 | "homepage": "http://sinusoid.es/mixco", 6 | "contributors": [ 7 | { 8 | "name": "Juan Pedro Bolivar Puente", 9 | "email": "raskolnikov@gnu.org" 10 | } 11 | ], 12 | "version": "2.0.3", 13 | "keywords": [ 14 | "mixxx", 15 | "coffee", 16 | "literate" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/arximboldi/mixco" 21 | }, 22 | "engines": { 23 | "node": ">=0.10" 24 | }, 25 | "browserify": { 26 | "transform": "coffeeify" 27 | }, 28 | "dependencies": { 29 | "argp": "^1.0.4", 30 | "browserify": "^10.2.3", 31 | "chai": "^2.3.0", 32 | "chai-spies": "^0.6.0", 33 | "coffee-script": "latest", 34 | "coffeeify": "^1.1.0", 35 | "colors": "^1.1.0", 36 | "globby": "^2.0.0", 37 | "gulp": "^3.9.0", 38 | "gulp-cached": "^1.1.0", 39 | "gulp-changed": "^1.2.1", 40 | "gulp-mocha": "^2.1.0", 41 | "gulp-rename": "^1.2.2", 42 | "heterarchy": "^1.0.5", 43 | "mocha": "^2.2.5", 44 | "node-promise": "^0.5.12", 45 | "sinon": "^1.17.6", 46 | "sinon-chai": "^2.8.0", 47 | "string": "^3.1.3", 48 | "through2": "^0.6.5", 49 | "underscore": "^1.8.3", 50 | "winston": "^1.0.0" 51 | }, 52 | "devDependencies": { 53 | "coffee-coverage": "^0.6.0", 54 | "coveralls": "2.10.0", 55 | "docco": "^0.7.0", 56 | "istanbul": "0.3.5" 57 | }, 58 | "scripts": { 59 | "test": "make test", 60 | "test-coverage": "make test-coverage", 61 | "prepublish": "make" 62 | }, 63 | "files": [ 64 | "bin", 65 | "lib", 66 | "src", 67 | "script", 68 | "test", 69 | "index.js", 70 | "makefile" 71 | ], 72 | "preferGlobal": true, 73 | "main": "index.js", 74 | "bin": { 75 | "mixco": "./bin/mixco.js" 76 | }, 77 | "directories": { 78 | "doc": "./doc", 79 | "man": "./man", 80 | "lib": "./lib", 81 | "bin": "./bin" 82 | }, 83 | "license": "GPL-3.0+" 84 | } 85 | -------------------------------------------------------------------------------- /docco/docco.jst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | <% if (sources.length > 1) { %> 14 | 29 | <% } %> 30 |
    31 | <% if (!hasTitle) { %> 32 |
  • 33 |
    34 |

    <%= title %>

    35 |
    36 |
  • 37 | <% } %> 38 | <% var license = null; %> 39 | <% for (var i=0, l=sections.length; i 40 | <% var section = sections[i]; %> 41 | <% var noCode = section.codeText.replace(/\s/gm, '') == '' %> 42 |
  • 43 | <% bigHeading = section.docsHtml.match(/^\s*(

    )|

    /) %> 44 |
    45 | <% heading = section.docsHtml.match(/^\s*<(h\d)>/) %> 46 |
    47 | 48 |
    49 | <%= section.docsHtml.replace(/--/gm, '—') %> 50 |
    51 | <% if (!noCode) { %> 52 |
    <%= section.codeHtml %>
    53 | <% } %> 54 |

  • 55 | <% } %> 56 |
57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /test/mixco/value.spec.coffee: -------------------------------------------------------------------------------- 1 | # spec.mixco.value 2 | # ================ 3 | 4 | # > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | # > - **View me [on a static web](http://sinusoid.es/mixco/test/mixco/value.spec.html)** 6 | # > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/test/mixco/value.spec.coffee)** 7 | 8 | chai = {expect} = require 'chai' 9 | {stub, spy} = require 'sinon' 10 | chai.use require 'sinon-chai' 11 | 12 | describe 'mixco.value', -> 13 | 14 | {Value, Reduce, Const, transform} = require('mixco').value 15 | 16 | describe 'Value', -> 17 | 18 | it "is initialized to given value", -> 19 | v = new Value initial: 5 20 | expect(v.value).to.eq 5 21 | v = new Value initial: "hello" 22 | expect(v.value).to.eq "hello" 23 | 24 | it "notifies when value changes", -> 25 | callback = spy() 26 | v = new Value 27 | v.on 'value', callback 28 | expect(callback).not.to.have.been.called 29 | v.value = 5 30 | expect(callback).to.have.been.calledWith 5 31 | 32 | it "returns newly set value", -> 33 | v = new Value 34 | v.value = 5 35 | expect(v.value).to.eq 5 36 | 37 | 38 | describe 'Reduce', -> 39 | 40 | v = null 41 | r = null 42 | 43 | beforeEach -> 44 | v = [ 45 | new Value initial: 1 46 | new Value initial: 2 47 | new Value initial: 3 48 | ] 49 | r = new Reduce ((a, b) -> a + b), v... 50 | 51 | it "reduces all given values with binary operation", -> 52 | expect(r.value).to.eq 6 53 | 54 | it "updates when any of the values changes", -> 55 | v[1].value = 0 56 | expect(r.value).to.eq 4 57 | v[0].value = 5 58 | expect(r.value).to.eq 8 59 | 60 | 61 | describe 'transform', -> 62 | 63 | it "applies a nullary operation", -> 64 | r = transform ((a) -> a*4), new Const 2 65 | expect(r.value).to.eq 8 66 | 67 | # License 68 | # ------- 69 | # 70 | # > Copyright (C) 2013, 2015 Juan Pedro Bolívar Puente 71 | # > 72 | # > This program is free software: you can redistribute it and/or 73 | # > modify it under the terms of the GNU General Public License as 74 | # > published by the Free Software Foundation, either version 3 of the 75 | # > License, or (at your option) any later version. 76 | # > 77 | # > This program is distributed in the hope that it will be useful, 78 | # > but WITHOUT ANY WARRANTY; without even the implied warranty of 79 | # > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 80 | # > GNU General Public License for more details. 81 | # > 82 | # > You should have received a copy of the GNU General Public License 83 | # > along with this program. If not, see . 84 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # 2 | # File: makefile 3 | # Author: Juan Pedro Bolívar Puente 4 | # 5 | # Generates proper Mixxx script configuration files from smart 6 | # smart CoffeeScript file. 7 | # 8 | 9 | WITH_PATH = NODE_PATH="$(NODE_PATH):.." 10 | MIXCO = $(WITH_PATH) ./bin/mixco.js 11 | _MIXCO = ./bin/mixco.js 12 | 13 | NODE_BIN = node_modules/.bin 14 | NODEJS = $(WITH_PATH) node 15 | COFFEE = $(WITH_PATH) $(NODE_BIN)/coffee 16 | BROWSERIFY = $(WITH_PATH) $(NODE_BIN)/browserify 17 | DOCCO = $(WITH_PATH) $(NODE_BIN)/docco 18 | MOCHA = $(WITH_PATH) $(NODE_BIN)/mocha 19 | ISTANBUL = $(WITH_PATH) $(NODE_BIN)/istanbul 20 | _MOCHA = $(NODE_BIN)/_mocha 21 | 22 | FRAMEWORK = \ 23 | lib/behaviour.js \ 24 | lib/cli.js \ 25 | lib/console.js \ 26 | lib/control.js \ 27 | lib/script.js \ 28 | lib/transform.js \ 29 | lib/util.js \ 30 | lib/value.js 31 | 32 | DOCS = \ 33 | doc/index.html \ 34 | doc/src/behaviour.html \ 35 | doc/src/cli.html \ 36 | doc/src/control.html \ 37 | doc/src/console.html \ 38 | doc/src/script.html \ 39 | doc/src/transform.html \ 40 | doc/src/util.html \ 41 | doc/src/value.html \ 42 | doc/script/korg_nanokontrol2.mixco.html \ 43 | doc/script/maudio_xponent.mixco.html \ 44 | doc/script/novation_twitch.mixco.html \ 45 | doc/test/mixco/behaviour.spec.html \ 46 | doc/test/mixco/control.spec.html \ 47 | doc/test/mixco/script.spec.html \ 48 | doc/test/mixco/value.spec.html \ 49 | doc/test/mock.html \ 50 | doc/test/scripts.spec.html 51 | 52 | framework: $(FRAMEWORK) 53 | 54 | script: $(FRAMEWORK) 55 | $(MIXCO) --factory 56 | 57 | script-dev: $(FRAMEWORK) 58 | $(MIXCO) --factory -w -o ~/.mixxx/controllers 59 | 60 | doc: $(DOCS) 61 | cp -r ./pic ./doc/ 62 | 63 | install: 64 | npm install 65 | 66 | test: $(FRAMEWORK) 67 | MIXCO_USE_SOURCE=1 $(MIXCO) -tT --factory 68 | 69 | test-coverage: $(FRAMEWORK) 70 | MIXCO_COVERAGE=1 MIXCO_USE_SOURCE=1 \ 71 | $(ISTANBUL) cover $(_MIXCO) -- -tT --factory --fatal-tests 72 | $(ISTANBUL) report text lcov 73 | 74 | upload-doc: doc 75 | ncftpput -R -m -u u48595320 sinusoid.es /mixco doc/* 76 | 77 | copy-doc: doc 78 | cp -rf doc/* ~/public/mixco/ 79 | 80 | clean: 81 | rm -rf ./doc 82 | rm -rf ./out 83 | rm -rf ./tmp 84 | rm -rf ./lib 85 | rm -rf ./coverage 86 | rm -rf ./mixco-output 87 | find . -name "*~" -exec rm -f {} \; 88 | 89 | .SECONDARY: 90 | .PHONY: test script clean 91 | 92 | lib/%.js: src/%.litcoffee 93 | @mkdir -p $(@D) 94 | $(COFFEE) -c -p $< > $@ 95 | lib/%.js: src/%.coffee 96 | @mkdir -p $(@D) 97 | $(COFFEE) -c -p $< > $@ 98 | lib/%.js: src/%.js 99 | @mkdir -p $(@D) 100 | cp -f $< $@ 101 | 102 | doc/index.html: README.md 103 | @mkdir -p $(@D) 104 | $(DOCCO) -t docco/docco.jst -c docco/docco.css -o $(@D) $< 105 | mv $(@D)/README.html $@ 106 | cp -rf docco/public $(@D) 107 | 108 | # $1: input file 109 | # $2: target directory 110 | define GENERATE_DOC 111 | @mkdir -p $2 112 | $(DOCCO) -t docco/docco.jst -c docco/docco.css -o $2 $1 113 | cp -rf docco/public $2 114 | endef 115 | 116 | doc/%.html: %.litcoffee 117 | $(call GENERATE_DOC,$<,$(@D)) 118 | doc/%.html: %.coffee 119 | $(call GENERATE_DOC,$<,$(@D)) 120 | doc/%.html: %.js 121 | $(call GENERATE_DOC,$<,$(@D)) 122 | -------------------------------------------------------------------------------- /test/mock.litcoffee: -------------------------------------------------------------------------------- 1 | spec.mock 2 | ========= 3 | 4 | > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | > - **View me [on a static web](http://sinusoid.es/mixco/test/mock.html)** 6 | > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/test/mock.litcoffee)** 7 | 8 | *Mocks* for objects provided by the Mixxx context. Please *update them 9 | when needed*, reflecting [the official Mixxx documentation]( 10 | http://mixxx.org/wiki/doku.php/midi_scripting). 11 | 12 | mixco = require 'mixco' 13 | script = mixco.script 14 | {assert} = mixco.util 15 | {spy} = require 'sinon' 16 | 17 | Engine 18 | ------ 19 | 20 | exports.engine = -> 21 | values = {} 22 | connections = {} 23 | softTakeover: spy -> # (group, key, enable) 24 | beginTimer: spy -> # (delay, handler) -> id 25 | stopTimer: spy -> # (id) 26 | scratchEnable: spy -> # (int deck, int intervalsPerRev, float rpm, 27 | # alpha, beta, ramp) 28 | scratchTick: spy -> # (int deck, int interval) 29 | scratchDisable: spy -> # (int deck) 30 | isScratching: spy -> # (int deck) 31 | brake: spy -> # (int deck, bool activate[, float factor, rate]) 32 | spinback: spy -> # (int deck, bool activate[, float factor, rate]) 33 | getValue: spy (group, key) -> 34 | values[[group, key]] ? 0 35 | setValue: spy (group, key, value) -> 36 | values[[group, key]] = value 37 | connectControl: spy (group, key, handler, disconnect) -> 38 | id = [group, key, handler] 39 | if disconnect 40 | assert id of connections, 41 | "Disconnect not connect control: #{group}, #{key}" 42 | delete connections[id] 43 | else 44 | assert id not of connections, 45 | "Connect connected control: #{group}, #{key}" 46 | connections[id] = true 47 | 48 | Midi 49 | ---- 50 | 51 | exports.midi = -> 52 | sendShortMsg: spy -> # (status, id, value) 53 | sendSysexMsg: spy -> # (data, length) 54 | 55 | Midi 56 | ---- 57 | 58 | exports.script = -> 59 | pitch: spy (control, value, status) -> 60 | 61 | 62 | Fake mixxx object 63 | ----------------- 64 | 65 | exports.mixxx = -> 66 | engine: exports.engine() 67 | midi: exports.midi() 68 | script: exports.script() 69 | 70 | Script 71 | ------ 72 | 73 | This test script class provides Mixxx object mocks. 74 | 75 | class exports.TestScript extends script.Script 76 | 77 | @property 'mixxx', 78 | get: -> 79 | @_fakeMixxx or= exports.mixxx() 80 | 81 | __registeredName: 'testscript' 82 | 83 | exports.testScript = -> new exports.TestScript arguments... 84 | 85 | 86 | License 87 | ------- 88 | 89 | > Copyright (C) 2013 Juan Pedro Bolívar Puente 90 | > 91 | > This program is free software: you can redistribute it and/or 92 | > modify it under the terms of the GNU General Public License as 93 | > published by the Free Software Foundation, either version 3 of the 94 | > License, or (at your option) any later version. 95 | > 96 | > This program is distributed in the hope that it will be useful, 97 | > but WITHOUT ANY WARRANTY; without even the implied warranty of 98 | > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 99 | > GNU General Public License for more details. 100 | > 101 | > You should have received a copy of the GNU General Public License 102 | > along with this program. If not, see . 103 | -------------------------------------------------------------------------------- /src/util.litcoffee: -------------------------------------------------------------------------------- 1 | mixco.util 2 | ========== 3 | 4 | > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | > - **View me [on a static web](http://sinusoid.es/mixco/src/util.html)** 6 | > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/src/util.litcoffee)** 7 | 8 | This module contains a series of utility functions. 9 | 10 | Monkey patches 11 | -------------- 12 | 13 | ### Function 14 | 15 | We provide a **property** class method to define *properties* -- 16 | i.e. attributes that are accessed via setters and getters. 17 | 18 | Function::property = (prop, desc) -> 19 | if desc instanceof Function 20 | desc = get: desc 21 | Object.defineProperty @prototype, prop, desc 22 | 23 | ### Number 24 | 25 | Number::clamp = (min, max) -> Math.min Math.max(this, min), max 26 | Number::sign = () -> if this < 0 then -1 else 1 27 | 28 | ### Array 29 | 30 | Array::equals = (other) -> 31 | @length is other.length and @every (elem, i) -> elem is other[i] 32 | 33 | 34 | Utilities 35 | --------- 36 | 37 | ### Copy 38 | 39 | Creates a copy of an object into another. 40 | 41 | exports.copy = (a, b) -> 42 | b ?= {} 43 | for k, v of a 44 | b[k] = v 45 | b 46 | 47 | ### Error management 48 | 49 | This can be used to guard against any possible exception, printing an 50 | error on the console when it happens. This is specially useful in the 51 | context of Mixxx. 52 | 53 | exports.catching = (f) -> -> 54 | try 55 | f.apply @, arguments 56 | catch err 57 | console.log "ERROR: #{err}" 58 | 59 | Throws an error if *value* is false. The *error* can be a custom string. 60 | 61 | exports.assert = (value, error=undefined) -> 62 | if not value 63 | throw new Error(if error? then error else "Assertion failed") 64 | 65 | ### String utilities 66 | 67 | This tries to scape a string to be valid XML. 68 | 69 | exports.xmlEscape = (str) -> 70 | str?.replace('&', '&') 71 | .replace('"', '"') 72 | .replace('>', '>') 73 | .replace('<', '<') ? '' 74 | 75 | Generates a string that contains *depth* number of spaces. 76 | 77 | exports.indent = (depth) -> 78 | Array(depth*4).join(" ") 79 | 80 | Generates a string with a C-style hexadecimal representation of a 81 | number. 82 | 83 | exports.hexStr = (number) -> 84 | "0x#{number.toString 16}" 85 | 86 | 87 | Generates a XML tag with the passed in value or nothing. 88 | 89 | exports.xmlTag = (str, value, indent=0) -> 90 | if value? 91 | "#{exports.indent indent}<#{str}>#{value}" 92 | else 93 | "" 94 | 95 | Joins several lines, removing empty ones. 96 | 97 | exports.joinLn = (lines) -> 98 | lines.filter((x) -> x).join('\n') 99 | 100 | 101 | ### Factories 102 | 103 | Generates a function that constructs an object of type *Klass*, 104 | forwarding all its parameters to the constructor. I hate using the 105 | `new` operator, so this will be used on most exported classes. 106 | 107 | exports.factory = (Klass) -> -> new Klass arguments... 108 | 109 | 110 | License 111 | ------- 112 | 113 | > Copyright (C) 2013 Juan Pedro Bolívar Puente 114 | > 115 | > This program is free software: you can redistribute it and/or 116 | > modify it under the terms of the GNU General Public License as 117 | > published by the Free Software Foundation, either version 3 of the 118 | > License, or (at your option) any later version. 119 | > 120 | > This program is distributed in the hope that it will be useful, 121 | > but WITHOUT ANY WARRANTY; without even the implied warranty of 122 | > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 123 | > GNU General Public License for more details. 124 | > 125 | > You should have received a copy of the GNU General Public License 126 | > along with this program. If not, see . 127 | -------------------------------------------------------------------------------- /test/mixco/script.spec.coffee: -------------------------------------------------------------------------------- 1 | # spec.mixco.value 2 | # ================ 3 | 4 | # > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | # > - **View me [on a static web](http://sinusoid.es/mixco/test/mixco/script.spec.html)** 6 | # > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/test/mixco/script.spec.coffee)** 7 | 8 | chai = {expect} = require 'chai' 9 | {stub, spy} = require 'sinon' 10 | chai.use require 'sinon-chai' 11 | 12 | describe 'mixco.script', -> 13 | 14 | {isinstance} = require 'heterarchy' 15 | mixco = require 'mixco' 16 | {Script, register} = mixco.script 17 | control = mixco.control 18 | 19 | class TestScript extends Script 20 | __registeredName: 'testscript' 21 | 22 | describe 'Script', -> 23 | 24 | script = null 25 | 26 | beforeEach -> 27 | script = new TestScript 28 | 29 | it 'configures controller id to be de script name', -> 30 | expect(script.config()) 31 | .to.match /[^$]*<\/controller>/ 32 | 33 | it 'can generate configuration with partial metadata', -> 34 | delete script.info.wiki 35 | expect(script.config()) 36 | .not.to.contain "undefined" 37 | 38 | describe 'register', -> 39 | 40 | testModule = null 41 | beforeEach -> 42 | testModule = 43 | exports: {} 44 | filename: 'testscript.mixco.js' 45 | 46 | it 'registers a class in the given NodeJs module', -> 47 | register testModule, TestScript 48 | expect(isinstance testModule.exports.testscript, TestScript) 49 | .to.be.true 50 | 51 | it 'can generate a script type from a definition', -> 52 | spier = stub 53 | constructor: spy() 54 | preinit: -> 55 | init: -> 56 | shutdown: -> 57 | postshutdown: -> 58 | register testModule, 59 | constructor: -> spier.constructor() 60 | preinit: -> 61 | spier.preinit() 62 | expect(@_isInit).not.to.exist 63 | init: -> spier.init() 64 | postshutdown: -> 65 | spier.postshutdown() 66 | expect(@_isInit).not.to.exist 67 | shutdown: -> spier.shutdown() 68 | info: author: 'Jimmy Jazz' 69 | 70 | script = testModule.exports.testscript 71 | expect(script.name).to.be.eq 'testscript' 72 | expect(script.info.author).to.be.eq 'Jimmy Jazz' 73 | expect(spier.constructor).to.have.been.called 74 | 75 | script.init() 76 | expect(spier.preinit).to.have.been.called 77 | expect(spier.init).to.have.been.called 78 | 79 | script.shutdown() 80 | expect(spier.shutdown).to.have.been.called 81 | expect(spier.postshutdown).to.have.been.called 82 | 83 | it 'controls created during construction are registered autoamtically', -> 84 | expectedControls = [] 85 | register testModule, 86 | constructor: -> 87 | expectedControls.push control.input() 88 | expectedControls.push control.control() 89 | 90 | expect(expectedControls.length) 91 | .to.be.eq 2 92 | expect(testModule.exports.testscript.controls) 93 | .to.eql expectedControls 94 | 95 | # License 96 | # ------- 97 | # 98 | # > Copyright (C) 2013 Juan Pedro Bolívar Puente 99 | # > 100 | # > This program is free software: you can redistribute it and/or 101 | # > modify it under the terms of the GNU General Public License as 102 | # > published by the Free Software Foundation, either version 3 of the 103 | # > License, or (at your option) any later version. 104 | # > 105 | # > This program is distributed in the hope that it will be useful, 106 | # > but WITHOUT ANY WARRANTY; without even the implied warranty of 107 | # > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 108 | # > GNU General Public License for more details. 109 | # > 110 | # > You should have received a copy of the GNU General Public License 111 | # > along with this program. If not, see . 112 | -------------------------------------------------------------------------------- /src/value.litcoffee: -------------------------------------------------------------------------------- 1 | mixco.value 2 | =========== 3 | 4 | > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | > - **View me [on a static web](http://sinusoid.es/mixco/src/value.html)** 6 | > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/src/value.litcoffee)** 7 | 8 | Module that provides a series of expressions that re-evaluate 9 | automatically whenever one of the leaf nodes of the expression tree 10 | changes. 11 | 12 | events = require 'events' 13 | util = require './util' 14 | factory = util.factory 15 | 16 | 17 | Value 18 | ----- 19 | 20 | The **Value** instances represent an active value that changes with 21 | time. The actual value can be accessed via the `value` property. 22 | Whenever the value changes, a `value` event is notified, using the 23 | standard [**node.js** *events*](http://nodejs.org/api/events.html) 24 | system. To register a listener callback that is called whenever the 25 | value changes, use the `on` method from the `events.EventEmitter` 26 | interface. 27 | 28 | class exports.Value extends events.EventEmitter 29 | 30 | constructor: ({initial}={}) -> 31 | super 32 | @setMaxListeners 0 33 | if initial? 34 | @value = initial 35 | 36 | @property 'value', 37 | get: -> @_value 38 | set: (newValue) -> 39 | @setValue newValue 40 | 41 | setValue: (newValue) -> 42 | if @_value != newValue 43 | @_value = newValue 44 | @emit 'value', newValue 45 | @_value 46 | 47 | exports.value = factory exports.Value 48 | 49 | 50 | ### Constants 51 | 52 | Constants are lightweight objects that behave like a exports.Value, 53 | but can not be modified -- at least, they will not trigger a 54 | modification when modified. 55 | 56 | class exports.Const 57 | 58 | value: undefined 59 | 60 | constructor: (initial=undefined) -> 61 | @value = initial 62 | 63 | It has to mock the events.EventEmitter interface. 64 | 65 | on: -> 66 | addListener: -> 67 | removeListener: -> 68 | listeners: -> [] 69 | 70 | exports.const = factory exports.Const 71 | 72 | 73 | High-order values 74 | ----------------- 75 | 76 | Higher order values take a function as a parameter and a set of 77 | other values. 78 | 79 | A **Reduce** value combines N values applying a reduction (i.e. fold) 80 | operation on them. It updates whenever one of them changes. 81 | 82 | class exports.Reduce extends exports.Value 83 | 84 | constructor: (@reducer, @reduced...) -> 85 | super() 86 | for v in @reduced 87 | v.on 'value', => @update() 88 | @update() 89 | 90 | update: -> 91 | @value = @reduced 92 | .reduce((a, b) => exports.const @reducer a.value, b.value) 93 | .value 94 | 95 | exports.reduce = factory exports.Reduce 96 | exports.and = -> exports.reduce ((a, b) -> a and b), arguments... 97 | exports.or = -> exports.reduce ((a, b) -> a or b), arguments... 98 | 99 | 100 | A **Transform** value holds a transformation of some other value by a 101 | unary function. 102 | 103 | class exports.Transform extends exports.Value 104 | 105 | constructor: (@transformer, @transformed) -> 106 | super() 107 | @transformed.on 'value', => @update() 108 | @update() 109 | 110 | update: -> 111 | @value = @transformer @transformed.value 112 | 113 | exports.transform = factory exports.Transform 114 | exports.not = -> exports.transform ((a) -> not a), arguments... 115 | 116 | License 117 | ------- 118 | 119 | > Copyright (C) 2013 Juan Pedro Bolívar Puente 120 | > 121 | > This program is free software: you can redistribute it and/or 122 | > modify it under the terms of the GNU General Public License as 123 | > published by the Free Software Foundation, either version 3 of the 124 | > License, or (at your option) any later version. 125 | > 126 | > This program is distributed in the hope that it will be useful, 127 | > but WITHOUT ANY WARRANTY; without even the implied warranty of 128 | > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 129 | > GNU General Public License for more details. 130 | > 131 | > You should have received a copy of the GNU General Public License 132 | > along with this program. If not, see . 133 | -------------------------------------------------------------------------------- /test/scripts.spec.coffee: -------------------------------------------------------------------------------- 1 | # spec.scripts 2 | # ============ 3 | # 4 | # > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | # > - **View me [on a static web](http://sinusoid.es/mixco/test/scripts.spec.html)** 6 | # > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/test/scripts.spec.coffee)** 7 | # 8 | # General tests for some of the Mixco based scripts. This will test 9 | # every available script in the 'script' folder at the root of the 10 | # Mixco distribution. 11 | 12 | {expect} = require 'chai' 13 | globby = require 'globby' 14 | 15 | describe 'scripts', -> 16 | 17 | fs = require 'fs' 18 | path = require 'path' 19 | mock = require './mock' 20 | mixco = require 'mixco' 21 | {assert} = mixco.util 22 | 23 | MIXCO_EXT_GLOBS = [ 24 | "*.mixco.js" 25 | "*.mixco.coffee" 26 | "*.mixco.litcoffee" 27 | ] 28 | 29 | # One may use the MIXCO_TEST_INPUTS environment variable to pass 30 | # which scripts to test. This variable should contain a 31 | # `:`-separated list of globby globs. 32 | 33 | MIXCO_TEST_INPUTS = process.env.MIXCO_TEST_INPUTS?.split ':' 34 | .map (input) -> path.resolve process.cwd(), input 35 | MIXCO_TEST_INPUTS ?= MIXCO_EXT_GLOBS.map (ext) -> 36 | path.join __dirname, "..", "script", "**", ext 37 | 38 | # We should let exception get all the way down to the test 39 | # framework so trivial errors are detected. The **unrequire** 40 | # function will cause a module to be unloaded. We patch the 41 | # *catching* decorator after unloading all modules so exceptions 42 | # reach the test system. 43 | 44 | unrequire = (name) -> 45 | fullName = require.resolve name 46 | if fullName of require.cache 47 | delete require.cache[fullName] 48 | 49 | globEach = (globs, fn) -> 50 | # Registering tests asynchronously confuses Mocha 51 | globby.sync globs 52 | .forEach fn 53 | 54 | do monkeypatchCatching = -> 55 | unrequire 'heterarchy' 56 | unrequire 'mixco' 57 | globEach MIXCO_TEST_INPUTS, (fname) -> 58 | unrequire fname 59 | globEach ['../lib/*.js', '../src/*.litcoffee'], (fname) -> 60 | unrequire fname 61 | 62 | require 'mixco' 63 | ['../src/util', '../lib/util'].forEach (mname) -> 64 | module = require.cache[require.resolve mname] 65 | module?.exports.catching = (f) -> f 66 | 67 | # Tests 68 | # ----- 69 | # 70 | # For every possible script we find, we generate some tests that 71 | # will check for stupid JavaScript programming mistakes -- like 72 | # propagating `undefined` values -- or other potential errors, 73 | # like exceptions reaching Mixxx, or whatever. 74 | 75 | globEach MIXCO_TEST_INPUTS, (fname) -> 76 | scriptName = path.basename fname, path.extname fname 77 | scriptName = path.basename scriptName, ".mixco" 78 | 79 | describe "#{scriptName}", -> 80 | script = null 81 | 82 | beforeEach -> 83 | @timeout 1 << 14 84 | unrequire fname 85 | module = require fname 86 | script = module[scriptName] 87 | script.mixxx = mock.mixxx() 88 | 89 | it "generates configuration without undefined values", -> 90 | expect(script.config()) 91 | .not.to.match /undefined/ 92 | expect(script.config()) 93 | .not.to.match /NaN/ 94 | 95 | it "is not empty", -> 96 | expect(script.controls.length) 97 | .not.to.equal 0 98 | 99 | it "initializes and shutsdown without launching exceptions", -> 100 | script.init() 101 | script.shutdown() 102 | 103 | # The next is specially useful. Missing entries in the 104 | # `mixco.transform` table are often found by these, among 105 | # other trivial problems in the user scripts. 106 | # 107 | # We simulate here that we send values to all controls 108 | # that are script mapped. We run through the controls in 109 | # different orders, increasing the likelihood of executing 110 | # behaviours that lie under modifiers. Note the check 111 | # `ev.value == value` after creating the event -- this way 112 | # we prevent sending the *note off* message of some 113 | # buttons when we do not intend to. 114 | 115 | it "does not break when receiving MIDI", -> 116 | @timeout 1 << 14 117 | control = require '../src/control' 118 | sendValues = (values, order=1) -> 119 | for c in script.controls by order 120 | if c.needsHandler?() 121 | for id in c.ids 122 | for value in values 123 | ev = control.event \ 124 | id.channel, 125 | null, 126 | value, 127 | id.status(), 128 | null 129 | if ev.value == value 130 | c.emit 'event', ev 131 | script.init() 132 | sendValues [127, 63, 0] 133 | sendValues [127, 63, 0], -1 134 | sendValues [127] 135 | sendValues [127], -1 136 | sendValues [0], -1 137 | sendValues [0] 138 | script.shutdown() 139 | 140 | # License 141 | # ------- 142 | # 143 | # > Copyright (C) 2013 Juan Pedro Bolívar Puente 144 | # > 145 | # > This program is free software: you can redistribute it and/or 146 | # > modify it under the terms of the GNU General Public License as 147 | # > published by the Free Software Foundation, either version 3 of the 148 | # > License, or (at your option) any later version. 149 | # > 150 | # > This program is distributed in the hope that it will be useful, 151 | # > but WITHOUT ANY WARRANTY; without even the implied warranty of 152 | # > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 153 | # > GNU General Public License for more details. 154 | # > 155 | # > You should have received a copy of the GNU General Public License 156 | # > along with this program. If not, see . 157 | -------------------------------------------------------------------------------- /docco/public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects `inline-block` display not defined in IE 8/9. 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | } 34 | 35 | /* 36 | * Prevents modern browsers from displaying `audio` without controls. 37 | * Remove excess height in iOS 5 devices. 38 | */ 39 | 40 | audio:not([controls]) { 41 | display: none; 42 | height: 0; 43 | } 44 | 45 | /* 46 | * Addresses styling for `hidden` attribute not present in IE 8/9. 47 | */ 48 | 49 | [hidden] { 50 | display: none; 51 | } 52 | 53 | /* ========================================================================== 54 | Base 55 | ========================================================================== */ 56 | 57 | /* 58 | * 1. Sets default font family to sans-serif. 59 | * 2. Prevents iOS text size adjust after orientation change, without disabling 60 | * user zoom. 61 | */ 62 | 63 | html { 64 | font-family: sans-serif; /* 1 */ 65 | -webkit-text-size-adjust: 100%; /* 2 */ 66 | -ms-text-size-adjust: 100%; /* 2 */ 67 | } 68 | 69 | /* 70 | * Removes default margin. 71 | */ 72 | 73 | body { 74 | margin: 0; 75 | } 76 | 77 | /* ========================================================================== 78 | Links 79 | ========================================================================== */ 80 | 81 | /* 82 | * Addresses `outline` inconsistency between Chrome and other browsers. 83 | */ 84 | 85 | a:focus { 86 | outline: thin dotted; 87 | } 88 | 89 | /* 90 | * Improves readability when focused and also mouse hovered in all browsers. 91 | */ 92 | 93 | a:active, 94 | a:hover { 95 | outline: 0; 96 | } 97 | 98 | /* ========================================================================== 99 | Typography 100 | ========================================================================== */ 101 | 102 | /* 103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, 104 | * Safari 5, and Chrome. 105 | */ 106 | 107 | h1 { 108 | font-size: 2em; 109 | } 110 | 111 | /* 112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: 1px dotted; 117 | } 118 | 119 | /* 120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: bold; 126 | } 127 | 128 | /* 129 | * Addresses styling not present in Safari 5 and Chrome. 130 | */ 131 | 132 | dfn { 133 | font-style: italic; 134 | } 135 | 136 | /* 137 | * Addresses styling not present in IE 8/9. 138 | */ 139 | 140 | mark { 141 | background: #ff0; 142 | color: #000; 143 | } 144 | 145 | 146 | /* 147 | * Corrects font family set oddly in Safari 5 and Chrome. 148 | */ 149 | 150 | code, 151 | kbd, 152 | pre, 153 | samp { 154 | font-family: monospace, serif; 155 | font-size: 1em; 156 | } 157 | 158 | /* 159 | * Improves readability of pre-formatted text in all browsers. 160 | */ 161 | 162 | pre { 163 | white-space: pre; 164 | white-space: pre-wrap; 165 | word-wrap: break-word; 166 | } 167 | 168 | /* 169 | * Sets consistent quote types. 170 | */ 171 | 172 | q { 173 | quotes: "\201C" "\201D" "\2018" "\2019"; 174 | } 175 | 176 | /* 177 | * Addresses inconsistent and variable font size in all browsers. 178 | */ 179 | 180 | small { 181 | font-size: 80%; 182 | } 183 | 184 | /* 185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers. 186 | */ 187 | 188 | sub, 189 | sup { 190 | font-size: 75%; 191 | line-height: 0; 192 | position: relative; 193 | vertical-align: baseline; 194 | } 195 | 196 | sup { 197 | top: -0.5em; 198 | } 199 | 200 | sub { 201 | bottom: -0.25em; 202 | } 203 | 204 | /* ========================================================================== 205 | Embedded content 206 | ========================================================================== */ 207 | 208 | /* 209 | * Removes border when inside `a` element in IE 8/9. 210 | */ 211 | 212 | img { 213 | border: 0; 214 | } 215 | 216 | /* 217 | * Corrects overflow displayed oddly in IE 9. 218 | */ 219 | 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* ========================================================================== 225 | Figures 226 | ========================================================================== */ 227 | 228 | /* 229 | * Addresses margin not present in IE 8/9 and Safari 5. 230 | */ 231 | 232 | figure { 233 | margin: 0; 234 | } 235 | 236 | /* ========================================================================== 237 | Forms 238 | ========================================================================== */ 239 | 240 | /* 241 | * Define consistent border, margin, and padding. 242 | */ 243 | 244 | fieldset { 245 | border: 1px solid #c0c0c0; 246 | margin: 0 2px; 247 | padding: 0.35em 0.625em 0.75em; 248 | } 249 | 250 | /* 251 | * 1. Corrects color not being inherited in IE 8/9. 252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 253 | */ 254 | 255 | legend { 256 | border: 0; /* 1 */ 257 | padding: 0; /* 2 */ 258 | } 259 | 260 | /* 261 | * 1. Corrects font family not being inherited in all browsers. 262 | * 2. Corrects font size not being inherited in all browsers. 263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome 264 | */ 265 | 266 | button, 267 | input, 268 | select, 269 | textarea { 270 | font-family: inherit; /* 1 */ 271 | font-size: 100%; /* 2 */ 272 | margin: 0; /* 3 */ 273 | } 274 | 275 | /* 276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in 277 | * the UA stylesheet. 278 | */ 279 | 280 | button, 281 | input { 282 | line-height: normal; 283 | } 284 | 285 | /* 286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 287 | * and `video` controls. 288 | * 2. Corrects inability to style clickable `input` types in iOS. 289 | * 3. Improves usability and consistency of cursor style between image-type 290 | * `input` and others. 291 | */ 292 | 293 | button, 294 | html input[type="button"], /* 1 */ 295 | input[type="reset"], 296 | input[type="submit"] { 297 | -webkit-appearance: button; /* 2 */ 298 | cursor: pointer; /* 3 */ 299 | } 300 | 301 | /* 302 | * Re-set default cursor for disabled elements. 303 | */ 304 | 305 | button[disabled], 306 | input[disabled] { 307 | cursor: default; 308 | } 309 | 310 | /* 311 | * 1. Addresses box sizing set to `content-box` in IE 8/9. 312 | * 2. Removes excess padding in IE 8/9. 313 | */ 314 | 315 | input[type="checkbox"], 316 | input[type="radio"] { 317 | box-sizing: border-box; /* 1 */ 318 | padding: 0; /* 2 */ 319 | } 320 | 321 | /* 322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. 323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome 324 | * (include `-moz` to future-proof). 325 | */ 326 | 327 | input[type="search"] { 328 | -webkit-appearance: textfield; /* 1 */ 329 | -moz-box-sizing: content-box; 330 | -webkit-box-sizing: content-box; /* 2 */ 331 | box-sizing: content-box; 332 | } 333 | 334 | /* 335 | * Removes inner padding and search cancel button in Safari 5 and Chrome 336 | * on OS X. 337 | */ 338 | 339 | input[type="search"]::-webkit-search-cancel-button, 340 | input[type="search"]::-webkit-search-decoration { 341 | -webkit-appearance: none; 342 | } 343 | 344 | /* 345 | * Removes inner padding and border in Firefox 4+. 346 | */ 347 | 348 | button::-moz-focus-inner, 349 | input::-moz-focus-inner { 350 | border: 0; 351 | padding: 0; 352 | } 353 | 354 | /* 355 | * 1. Removes default vertical scrollbar in IE 8/9. 356 | * 2. Improves readability and alignment in all browsers. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; /* 1 */ 361 | vertical-align: top; /* 2 */ 362 | } 363 | 364 | /* ========================================================================== 365 | Tables 366 | ========================================================================== */ 367 | 368 | /* 369 | * Remove most spacing between table cells. 370 | */ 371 | 372 | table { 373 | border-collapse: collapse; 374 | border-spacing: 0; 375 | } -------------------------------------------------------------------------------- /src/transform.litcoffee: -------------------------------------------------------------------------------- 1 | mixco.transform 2 | =============== 3 | 4 | Methods to transform MIDI values to Mixxx control values. 5 | 6 | Utilities 7 | --------- 8 | 9 | exports.identity = (v) -> v 10 | exports.identity.inverse = exports.identity 11 | exports.momentary = (v) -> if v > 0 then 1 else 0 12 | exports.momentary.inverse = (v) -> if v > 0 then 1 else 0 13 | exports.binary = (v, oldv) -> if v > 0 then not oldv else null 14 | exports.binary.inverse = (v) -> if v > 0 then 1 else 0 15 | exports.linear = (v, min, max) -> min + v * (max - min) 16 | exports.linear.inverse = (v, min, max) -> (v - min) / (max - min) 17 | exports.centered = (v, min, center, max) -> 18 | if v < .5 19 | then linear v*2, min, center 20 | else linear (v-.5)*2, center, max 21 | exports.centered.inverse = (v, min, center, max) -> 22 | if v < center 23 | then 0.5 * linear.inverse v, min, center 24 | else 0.5 + 0.5 * linear.inverse v, center, max 25 | 26 | exports.transform = (f, args...) -> 27 | result = (ev, oldv) -> f ev.value / 127.0, args..., oldv 28 | result.inverse = (v) -> 127 * f.inverse(v, args...) 29 | result 30 | 31 | exports.transformI = (f, args...) -> 32 | result = (ev, oldv) -> f ev.value, args..., oldv 33 | result.inverse = (v) -> f.inverse(v, args...) 34 | result 35 | 36 | exports.transformB = (f, args...) -> 37 | result = (ev, oldv) -> f ev.pressed / 127.0, args..., oldv 38 | result.inverse = (v) -> 127 * f.inverse(v, args...) 39 | result 40 | 41 | exports.identityT = identityT = exports.transformI exports.identity 42 | exports.momentaryT = momentaryT = exports.transformB exports.momentary 43 | exports.binaryT = binaryT = exports.transformB exports.binary 44 | exports.linearT = linearT = -> exports.transform exports.linear, arguments... 45 | exports.centeredT = centeredT = -> exports.transform exports.centered, arguments... 46 | exports.defaultT = defaultT = linearT 0.0, 1.0 47 | 48 | Mappings 49 | -------- 50 | 51 | The **mappings** table defines a set of functions that convert a MIDI 52 | value to the value ranges that Mixxx controls expect. Extend as 53 | needed. Please make sure to keep it in sync with the official 54 | [Mixxx controls documentation][mixxxcontrols]. 55 | 56 | [mixxxcontrols]: http://www.mixxx.org/wiki/doku.php/mixxxcontrols 57 | 58 | exports.mappings = 59 | "beatloop_0.0625_activate": momentaryT 60 | "beatloop_0.0625_toggle": momentaryT 61 | "beatloop_0.125_activate": momentaryT 62 | "beatloop_0.125_toggle": momentaryT 63 | "beatloop_0.5_activate": momentaryT 64 | "beatloop_0.5_toggle": momentaryT 65 | "beatlooproll_0.0625_activate": momentaryT 66 | "beatlooproll_0.125_activate": momentaryT 67 | "beatlooproll_0.5_activate": momentaryT 68 | back: momentaryT 69 | balance: linearT -1.0, 1.0 70 | beatloop_16_activate: momentaryT 71 | beatloop_16_toggle: momentaryT 72 | beatloop_1_activate: momentaryT 73 | beatloop_1_toggle: momentaryT 74 | beatloop_2_activate: momentaryT 75 | beatloop_2_toggle: momentaryT 76 | beatloop_32_activate: momentaryT 77 | beatloop_32_toggle: momentaryT 78 | beatloop_4_activate: momentaryT 79 | beatloop_4_toggle: momentaryT 80 | beatloop_8_activate: momentaryT 81 | beatloop_8_toggle: momentaryT 82 | beatlooproll_16_activate: momentaryT 83 | beatlooproll_1_activate: momentaryT 84 | beatlooproll_2_activate: momentaryT 85 | beatlooproll_32_activate: momentaryT 86 | beatlooproll_4_activate: momentaryT 87 | beatlooproll_8_activate: momentaryT 88 | beatloop_double: momentaryT 89 | beatloop_halve: momentaryT 90 | beatjump_4_forward: momentaryT 91 | beatjump_4_backward: momentaryT 92 | beatjump_1_forward: momentaryT 93 | beatjump_1_backward: momentaryT 94 | beats_translate_curpos: momentaryT 95 | beatsync: momentaryT 96 | beatsync_tempo: momentaryT 97 | crossfader: linearT -1.0, 1.0 98 | cue_default: momentaryT 99 | eject: momentaryT 100 | enabled: binaryT 101 | filterHigh: centeredT 0.0, 1.0, 4.0 102 | filterHighKill: binaryT 103 | filterLow: centeredT 0.0, 1.0, 4.0 104 | filterLowKill: binaryT 105 | filterMid: centeredT 0.0, 1.0, 4.0 106 | filterMidKill: binaryT 107 | fwd: momentaryT 108 | headMix: centeredT -1.0, 1.0 109 | headVolume: centeredT 0.0, 1.0, 5.0 110 | hotcue_1_activate: momentaryT 111 | hotcue_1_clear: momentaryT 112 | hotcue_2_activate: momentaryT 113 | hotcue_2_clear: momentaryT 114 | hotcue_3_activate: momentaryT 115 | hotcue_3_clear: momentaryT 116 | hotcue_4_activate: momentaryT 117 | hotcue_4_clear: momentaryT 118 | hotcue_5_activate: momentaryT 119 | hotcue_5_clear: momentaryT 120 | hotcue_6_activate: momentaryT 121 | hotcue_6_clear: momentaryT 122 | hotcue_7_activate: momentaryT 123 | hotcue_7_clear: momentaryT 124 | jog: identityT 125 | keylock: binaryT 126 | lfoDelay: linearT 50.0, 10000.0 127 | lfoDepth: defaultT 128 | lfoPeriod: linearT 50000.0, 2000000.0 129 | LoadSelectedTrack: momentaryT 130 | loop_double: momentaryT 131 | loop_enabled: binaryT 132 | loop_end_position: linearT 133 | loop_halve: momentaryT 134 | loop_in: momentaryT 135 | loop_out: momentaryT 136 | loop_start_position: linearT 137 | play: binaryT 138 | playposition: linearT 0.0, 1.0 139 | plf: binaryT 140 | pregain: centeredT 0.0, 1.0, 4.0 141 | pregain_toggle: binaryT 142 | rate: linearT -1.0, 1.0 143 | rate_temp_down: momentaryT 144 | rate_temp_down_small: momentaryT 145 | rate_temp_up: momentaryT 146 | rate_temp_up_small: momentaryT 147 | reverse: binaryT 148 | scratch2: linearT -3.0, 3.0 149 | scratch2_enable: binaryT 150 | SelectNextPlaylist: momentaryT 151 | SelectNextTrack: momentaryT 152 | SelectPrevPlaylist: momentaryT 153 | SelectPrevTrack: momentaryT 154 | SelectTrackKnob: identityT 155 | slip_enabled: binaryT 156 | super1: linearT 0.0, 10.0 157 | talkover: binaryT 158 | ToggleSelectedSidebarItem: momentaryT 159 | volume: defaultT 160 | VuMeter: defaultT 161 | VuMeterL: defaultT 162 | VuMeterR: defaultT 163 | wheel: linearT -3.0, 3.0 164 | 165 | License 166 | ------- 167 | 168 | > Copyright (C) 2013 Juan Pedro Bolívar Puente 169 | > 170 | > This program is free software: you can redistribute it and/or 171 | > modify it under the terms of the GNU General Public License as 172 | > published by the Free Software Foundation, either version 3 of the 173 | > License, or (at your option) any later version. 174 | > 175 | > This program is distributed in the hope that it will be useful, 176 | > but WITHOUT ANY WARRANTY; without even the implied warranty of 177 | > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 178 | > GNU General Public License for more details. 179 | > 180 | > You should have received a copy of the GNU General Public License 181 | > along with this program. If not, see . 182 | -------------------------------------------------------------------------------- /test/mixco/control.spec.coffee: -------------------------------------------------------------------------------- 1 | # spec.mixco.behaviour 2 | # ==================== 3 | 4 | # > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | # > - **View me [on a static web](http://sinusoid.es/mixco/test/mixco/control.spec.html)** 6 | # > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/test/mixco/control.spec.coffee)** 7 | 8 | chai = {expect} = require 'chai' 9 | {spy, mock} = require 'sinon' 10 | chai.use require 'sinon-chai' 11 | 12 | describe 'mixco.control', -> 13 | 14 | {union} = require 'underscore' 15 | mocks = require '../mock' 16 | mixco = require 'mixco' 17 | c = {MIDI_CC, Control, InControl, OutControl} = mixco.control 18 | {Behaviour} = behaviour = mixco.behaviour 19 | 20 | describe 'Control', -> 21 | 22 | control = null 23 | 24 | beforeEach -> 25 | control = new Control 26 | 27 | it "exposes script when initialized", -> 28 | script = new mocks.TestScript "script" 29 | 30 | control.init script 31 | expect(control.script).to.eq script 32 | 33 | control.shutdown script 34 | expect(control.script).not.to.exist 35 | 36 | it "converts number or pair in constructor to CC midi id", -> 37 | control = new Control 32 38 | expect(control.ids.length).to.eq 1 39 | {message, midino, channel} = control.ids[0] 40 | expect([message, midino, channel]).to.eql [MIDI_CC, 32, 0] 41 | 42 | control = new Control 64, 8 43 | expect(control.ids.length).to.eq 1 44 | {message, midino, channel} = control.ids[0] 45 | expect([message, midino, channel]).to.eql [MIDI_CC, 64, 8] 46 | 47 | 48 | describe 'InControl', -> 49 | 50 | control = null 51 | 52 | beforeEach -> 53 | control = new InControl 54 | 55 | it "propagates options to its behaviours that are registered", -> 56 | beh1 = new Behaviour 57 | control.does beh1 58 | control.option behaviour.option.invert 59 | expect(beh1._options).to.eql [behaviour.option.invert] 60 | 61 | it "propagates options to its new behaviours", -> 62 | beh1 = new Behaviour 63 | control.option behaviour.option.invert 64 | control.does beh1 65 | expect(beh1._options).to.eql [behaviour.option.invert] 66 | 67 | it "propagates options to its conditional behaviours", -> 68 | beh1 = new Behaviour 69 | beh2 = new Behaviour 70 | beh3 = new Behaviour 71 | control.option behaviour.option.invert 72 | 73 | control.when new Behaviour, beh1 74 | expect(beh1._options).to.eql [behaviour.option.invert] 75 | 76 | control.else.when new Behaviour, beh2 77 | expect(beh2._options).to.eql [behaviour.option.invert] 78 | 79 | control.else beh3 80 | expect(beh3._options).to.eql [behaviour.option.invert] 81 | 82 | it "configures the options of its behaviour when it can", -> 83 | beh1 = new Behaviour 84 | beh1.directInMapping = -> 85 | group: "[master]" 86 | key: "crossfader" 87 | beh1.option behaviour.option.invert 88 | beh1.options.softTakeover 89 | control.does beh1 90 | 91 | expect(control.configInputs 0) 92 | .to.match /// 93 | \s* 94 | \s* 95 | \s* 96 | \s* 97 | /// 98 | 99 | it "configures as script binding when no direct input mapping", -> 100 | beh1 = new Behaviour 101 | beh1.option behaviour.option.invert 102 | beh1.option behaviour.option.softTakeover 103 | control.does beh1 104 | 105 | expect(control.configInputs 0, mocks.testScript()) 106 | .to.match /// 107 | \s* 108 | \s* 109 | \s* 110 | /// 111 | 112 | it "configures as script binding when too many behaviours", -> 113 | beh1 = new Behaviour 114 | beh1.directInMapping = -> 115 | group: "[master]" 116 | key: "crossfader" 117 | beh2 = new Behaviour 118 | beh2.directInMapping = beh1.directInMapping 119 | 120 | beh1.option behaviour.option.invert 121 | beh1.options.softTakeover 122 | control.does beh1 123 | control.does beh2 124 | 125 | expect(control.configInputs 0, mocks.testScript()) 126 | .to.match /// 127 | \s* 128 | \s* 129 | \s* 130 | /// 131 | 132 | it "configures as script binding when option without name", -> 133 | beh1 = new Behaviour 134 | beh1.directInMapping = -> 135 | group: "[master]" 136 | key: "crossfader" 137 | control.does beh1.option {} 138 | 139 | expect(control.configInputs 0, mocks.testScript()) 140 | .to.match /// 141 | \s* 142 | \s* 143 | \s* 144 | /// 145 | 146 | it "configures as normal when no options", -> 147 | beh1 = new Behaviour 148 | beh1.directInMapping = -> 149 | group: "[master]" 150 | key: "crossfader" 151 | control.does beh1 152 | 153 | expect(control.configInputs 0, mocks.testScript()) 154 | .to.match /// 155 | \s* 156 | \s* 157 | \s* 158 | /// 159 | 160 | 161 | describe 'OutControl', -> 162 | 163 | it "configures minimum and maximum from the behaviour mapping", -> 164 | control = new OutControl 165 | behave = new Behaviour 166 | control.does behave 167 | 168 | behave.directOutMapping = -> 169 | minimum: 1 170 | maximum: 2 171 | config = control.configOutputs 0 172 | expect(config).to.contain("1") 173 | expect(config).to.contain("2") 174 | 175 | behave.directOutMapping = -> {} 176 | config = control.configOutputs 0 177 | expect(config).not.to.contain("") 178 | expect(config).not.to.contain("") 179 | 180 | it "configures an output for every midi id that is not a note off", -> 181 | control = new OutControl union c.ccIds(0x42), c.noteIds(0x33) 182 | behave = new Behaviour 183 | control.does behave 184 | 185 | behave.directOutMapping = -> {} 186 | config = control.configOutputs 0 187 | expect(config).to.match /// 188 | \s*0xb0 189 | \s*0x42 190 | /// 191 | expect(config).to.match /// 192 | \s*0x90 193 | \s*0x33 194 | /// 195 | expect(config).not.to.match /// 196 | \s*0x80 197 | \s*0x33 198 | /// 199 | 200 | it "turns off completely the controls on shut down", -> 201 | script = new mocks.TestScript "script" 202 | control = new OutControl 203 | spy(control, 'doSend') 204 | 205 | control.init script 206 | expect(control.doSend).not.to.have.been.calledWith 'disable' 207 | control.shutdown script 208 | expect(control.doSend).to.have.been.calledWith 'disable' 209 | 210 | it "sends midi to all outputs that are not a note off", -> 211 | script = new mocks.TestScript "script" 212 | control = new OutControl union c.ccIds(0x42), c.noteIds(0x33) 213 | 214 | control.init script 215 | control.doSend 'on' 216 | 217 | expect(script.mixxx.midi.sendShortMsg) 218 | .to.have.been.calledWith control.ids[0].status(), 219 | control.ids[0].midino, 220 | 0x7f 221 | expect(script.mixxx.midi.sendShortMsg) 222 | .to.have.been.calledWith control.ids[1].status(), 223 | control.ids[1].midino, 224 | 0x7f 225 | expect(script.mixxx.midi.sendShortMsg) 226 | .not 227 | .to.have.been.calledWith control.ids[2].status(), 228 | control.ids[2].midino, 229 | 0x7f 230 | 231 | # License 232 | # ------- 233 | # 234 | # > Copyright (C) 2013 Juan Pedro Bolívar Puente 235 | # > 236 | # > This program is free software: you can redistribute it and/or 237 | # > modify it under the terms of the GNU General Public License as 238 | # > published by the Free Software Foundation, either version 3 of the 239 | # > License, or (at your option) any later version. 240 | # > 241 | # > This program is distributed in the hope that it will be useful, 242 | # > but WITHOUT ANY WARRANTY; without even the implied warranty of 243 | # > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 244 | # > GNU General Public License for more details. 245 | # > 246 | # > You should have received a copy of the GNU General Public License 247 | # > along with this program. If not, see . 248 | -------------------------------------------------------------------------------- /src/script.litcoffee: -------------------------------------------------------------------------------- 1 | mixco.script 2 | ============ 3 | 4 | > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | > - **View me [on a static web](http://sinusoid.es/mixco/src/script.html)** 6 | > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/src/script.litcoffee)** 7 | 8 | This module contains the main interface for defining custom Mixxx 9 | scripts. 10 | 11 | {flatten, bind} = require 'underscore' 12 | {issubclass, mro} = require 'heterarchy' 13 | {Control} = require './control' 14 | {indent, xmlEscape, catching, assert} = require './util' 15 | require './console' 16 | 17 | {basename} = require 'path' 18 | S = require 'string' 19 | _ = require 'underscore' 20 | 21 | Script 22 | ------ 23 | 24 | First, the **register** function registers a instance of the class 25 | `scriptTypeOrDefinition` instance into the given module. 26 | `scriptTypeOrDefinition` can be either a `Script` subclass or an 27 | object defining overrides for `name`, `constructor` and optionally 28 | `init` and `shutdown`. The script instance will be exported as 29 | `Script.name`, and if the parent module is main, it will be executed. 30 | 31 | exports.nameFromFilename = (fname) -> 32 | extensions = [ 33 | ".mixco.coffee", 34 | ".mixco.litcoffee", 35 | ".mixco.js", 36 | ".mixco" # must be last 37 | ] 38 | fname = basename fname 39 | assert (_.some extensions, (x) -> S(fname).endsWith x), 40 | "Script file name: #{fname} must end in one of: #{extensions}" 41 | name = extensions.reduce ((fname, ext) -> fname.replace ext, ""), fname 42 | assert name.match /^[a-zA-Z_$][0-9a-zA-Z_$].*$/, 43 | "Script name must be a valid JavaScript identifier" 44 | name 45 | 46 | exports.register = (targetModule, scriptTypeOrDefinition) -> 47 | name = 48 | if targetModule.filename? 49 | # running inside Node 50 | exports.nameFromFilename targetModule.filename 51 | else if MIXCO_SCRIPT_FILENAME? 52 | # running inside Mixxx 53 | exports.nameFromFilename MIXCO_SCRIPT_FILENAME 54 | else 55 | assert false, "Invalid script" 56 | scriptType = 57 | if issubclass scriptTypeOrDefinition, exports.Script 58 | scriptTypeOrDefinition 59 | else 60 | exports.create scriptTypeOrDefinition 61 | 62 | instance = new scriptType 63 | instance.__registeredName = name 64 | targetModule.exports[name] = instance 65 | 66 | if targetModule == require.main 67 | instance.main() 68 | 69 | exports.create = (scriptDefinition) -> 70 | assert scriptDefinition.constructor?, 71 | "Script definition must have a constructor" 72 | 73 | {constructor, init, shutdown} = 74 | scriptDefinition 75 | 76 | class NewScript extends exports.Script 77 | 78 | constructor: -> 79 | super 80 | try 81 | Control::setRegistry bind @add, @ 82 | constructor.apply @, arguments 83 | finally 84 | Control::setRegistry null 85 | this 86 | 87 | init: -> 88 | @preinit?.apply @, arguments 89 | super 90 | init?.apply @, arguments 91 | 92 | shutdown: -> 93 | shutdown?.apply @, arguments 94 | super 95 | @postshutdown?.apply @, arguments 96 | 97 | special = ['name', 'constructor', 'init', 'shutdown'] 98 | for k, v of scriptDefinition 99 | if k not in special 100 | NewScript::[k] = v 101 | 102 | NewScript 103 | 104 | 105 | Then, inherit from the **Script** class to define your own controller 106 | mappings. These scripts can be used to both generate de XML 107 | configuration file for Mixxx and also as the script itself, when 108 | properly compiled to Javascript. 109 | 110 | To work properly, the script file name must the the same as the class 111 | name but in lowercase, and it must be registered using the 112 | **register** function -- i.e. if you have a script called 113 | `MyGreatController`, it should be in a file called 114 | `mygreatcontroller.litcoffee`, and this file should contain a line 115 | like: 116 | 117 | > ```coffee 118 | > script.register module, MyGreatController 119 | > ``` 120 | 121 | The `module` variable is [defined automatically by *node.js*]( 122 | http://nodejs.org/api/modules.html#modules_the_module_object), you do 123 | not have to care about it. 124 | 125 | class exports.Script 126 | 127 | ### Properties 128 | 129 | This is the metadata that is displayed in the Mixxx preferences 130 | panel. Override it with your details. 131 | 132 | info: 133 | name: "[mixco] Generic Script" 134 | author: "Juan Pedro Bolivar Puente " 135 | description: "" 136 | forums: "" 137 | wiki: "" 138 | 139 | 140 | The **name** property returns the name of the script, which is the 141 | name of the script file minus the extensions. It is set up 142 | automatically during registration. 143 | 144 | @property 'name', 145 | get: -> 146 | assert @__registeredName, "Script must be registered" 147 | @__registeredName 148 | 149 | Use **add** to add controls to your script instance. 150 | 151 | add: (controls...) -> 152 | assert not @_isInit, "Can only add controls in constructor" 153 | @controls.push flatten(controls)... 154 | 155 | ### Mixxx protocol 156 | 157 | These methods are called by Mixxx when the script is loaded or 158 | unloaded. 159 | 160 | init: catching -> 161 | @_isInit = true 162 | for control in @controls 163 | control.init this 164 | 165 | shutdown: catching -> 166 | for control in @controls 167 | control.shutdown this 168 | delete @_isInit 169 | 170 | ### Constructor 171 | 172 | constructor: -> 173 | @controls = [] 174 | 175 | ### Mixxx environment 176 | 177 | In general, controls, behaviours and other entities using the Mixxx 178 | environment --the global variables like *engine* or *midi*-- should 179 | access it via this property instead. This improves testability. 180 | 181 | mixxx: 182 | engine: (engine if engine?) 183 | midi: (midi if midi?) 184 | script: (script if script?) 185 | 186 | ### Standalone execution 187 | 188 | The following methods are executed implicitly by **register** when the 189 | script is executed as a standalone application. It can generate the 190 | XML file and display some help. 191 | 192 | main: -> 193 | for arg in process.argv 194 | if arg in ['-h', '--help'] 195 | console.info @help() 196 | break 197 | if arg in ['-g', '--generate-config'] 198 | console.info @config() 199 | break 200 | 201 | help: -> 202 | """ 203 | Mixxx Controller Script 204 | ======================= 205 | 206 | Name: #{@info.name} 207 | Author: #{@info.author} 208 | Description: #{@info.description} 209 | Forums: #{@info.description} 210 | 211 | Usage 212 | ----- 213 | 1. Generate Mixxx config: 214 | coffee #{@name}.coffee -g > #{@name}.xml 215 | 216 | 2. Generate Mixxx script: 217 | coffee -c #{@name}.coffee 218 | """ 219 | 220 | config: -> 221 | """ 222 | 223 | 224 | #{indent 1} 225 | #{indent 2}#{xmlEscape(@info.name)} 226 | #{indent 2}#{xmlEscape(@info.author)} 227 | #{indent 2}#{xmlEscape(@info.description)} 228 | #{indent 2}#{xmlEscape(@info.wiki)} 229 | #{indent 2}#{xmlEscape(@info.forums)} 230 | #{indent 1} 231 | #{indent 1} 232 | #{indent 2} 233 | #{indent 3} 235 | #{indent 2} 236 | #{indent 2} 237 | #{@configInputs 3} 238 | #{indent 2} 239 | #{indent 2} 240 | #{@configOutputs 3} 241 | #{indent 2} 242 | #{indent 1} 243 | 244 | """ 245 | 246 | ### Implementationd details 247 | 248 | configInputs: (depth) -> 249 | (control.configInputs depth, this for control in @controls) 250 | .filter((x) -> x) 251 | .join('\n') 252 | 253 | configOutputs: (depth) -> 254 | (control.configOutputs depth, this for control in @controls) 255 | .filter((x) -> x) 256 | .join('\n') 257 | 258 | The **registerHandler** method is called during initialization by the 259 | controls to register a handler callback in the script. If `id` is not 260 | passed, one is generated for them. When `id` is passed, the handler 261 | key is constant and can be queried even before registering the 262 | handler, using the **handlerKey** method. Otherwise, the handler can 263 | still be known from the return value of `registerHandler`. 264 | 265 | _nextCallbackId: 1 266 | registerHandler: (callback, id=undefined) -> 267 | id or= @_nextCallbackId++ 268 | handlerName = "__handle_#{id}" 269 | assert not this[handlerName], 270 | "Handlers can be registered only once (#{handlerName})" 271 | 272 | this[handlerName] = callback 273 | return @handlerKey id 274 | 275 | handlerKey: (id=undefined) -> 276 | if not id? 277 | id = @_nextCallbackId - 1 278 | "#{@name}.__handle_#{id}" 279 | 280 | 281 | License 282 | ------- 283 | 284 | > Copyright (C) 2013 Juan Pedro Bolívar Puente 285 | > 286 | > This program is free software: you can redistribute it and/or 287 | > modify it under the terms of the GNU General Public License as 288 | > published by the Free Software Foundation, either version 3 of the 289 | > License, or (at your option) any later version. 290 | > 291 | > This program is distributed in the hope that it will be useful, 292 | > but WITHOUT ANY WARRANTY; without even the implied warranty of 293 | > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 294 | > GNU General Public License for more details. 295 | > 296 | > You should have received a copy of the GNU General Public License 297 | > along with this program. If not, see . 298 | -------------------------------------------------------------------------------- /script/korg_nanokontrol2.mixco.litcoffee: -------------------------------------------------------------------------------- 1 | script.nanokontrol2 2 | =================== 3 | 4 | > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | > - **View me [on a static web](http://sinusoid.es/mixco/script/korg_nanokontrol2.mixco.html)** 6 | > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/script/korg_nanokontrol2.mixco.litcoffee)** 7 | 8 | Mixxx script file for the **Korg NanoKontrol2** controller. 9 | 10 | This script description is a bit more verbose than others, at it tries 11 | to serve as **tutorial** on how to write your own controller scripts. 12 | People just interested in the functionality of the mapping can find 13 | this in indented bullet points. 14 | 15 | ![NanoKontrol2 Layout](http://sinusoid.es/mixco/pic/korg_nanokontrol2.jpg) 16 | 17 | Dependencies 18 | ------------ 19 | 20 | First, we have to import he *Mixco* modules that we are going to use, 21 | using the [NodeJS, 22 | **require**](http://nodejs.org/api/modules.html#modules_module_require_id) 23 | function. Also, lets define these couple of shortcuts to save typing. 24 | 25 | mixco = require 'mixco' 26 | {assert} = mixco.util 27 | c = mixco.control 28 | b = mixco.behaviour 29 | v = mixco.value 30 | 31 | The script 32 | ---------- 33 | 34 | ### Declaration 35 | 36 | We define the script with the **script.register** function, which will 37 | create the script instance. As first parameter we pass the current 38 | module, that is contained in the special *NodeJS* variable 39 | `module`. The second parameter is an object with the actual script. 40 | 41 | mixco.script.register module, 42 | 43 | ### Metadata 44 | 45 | Then we fill out the metadata. This will be shown to the user in the 46 | preferences window in Mixxx when he selects the script. 47 | 48 | info: 49 | name: '[mixco] Korg Nanokontrol 2' 50 | author: 'Juan Pedro Bolivar Puente ' 51 | wiki: 'https://sinusoid.es/mixco/script/korg_nanokontrol2.mixco.html' 52 | forums: 'https://github.com/arximboldi/mixco/issues' 53 | description: 54 | """ 55 | Controller mapping for Korg Nanokontrol 2. Unlike 56 | other scripts that are oriented as a secondary 57 | controller, this provides basic deck controls, being 58 | usable as primary interface. 59 | """ 60 | 61 | ### Constructor 62 | 63 | All the actual interesting stuff happens in the *constructor* of the 64 | script. Here we will create the controls and add them to the script 65 | and define their behaviour. 66 | 67 | constructor: -> 68 | 69 | #### Master section 70 | 71 | In this section we describe controls that have effect on the master 72 | channel and main outputs. 73 | 74 | ##### Transport section 75 | 76 | All the buttons on the left side of the controllers is what we call 77 | the *transport section*. These are global buttons 78 | 79 | * The *cycle* button will be used as modifier. 80 | 81 | @cycle = b.modifier() 82 | c.control(0x2e).does @cycle 83 | 84 | Most of the transport controls will have their behaviour defined 85 | per-deck. We define them here and add the behaviours later. 86 | 87 | @backButton = c.control 0x3a 88 | @fwdButton = c.control 0x3b 89 | @nudgeDownButton = c.control 0x2b 90 | @nudgeUpButton = c.control 0x2c 91 | 92 | * The *marker* section can be used to browse the library. The left 93 | and right arrows browse the playlist up and down. When *set* is 94 | pressed, they move the sidebar. 95 | 96 | @marker = b.modifier() 97 | c.control(0x3C).does @marker 98 | 99 | g = "[Playlist]" 100 | c.control(0x3D) 101 | .when(@marker, g, "SelectPrevPlaylist") 102 | .else g, "SelectPrevTrack" 103 | c.control(0x3E) 104 | .when(@marker, g, "SelectNextPlaylist") 105 | .else g, "SelectNextTrack" 106 | 107 | @loadTrack = c.control(0x2a) 108 | 109 | Here are some more controls that get their actual functionality 110 | defined later. 111 | 112 | @sync = c.control(0x29) 113 | @syncTempo = c.control(0x2d) 114 | 115 | ##### Channel sections 116 | 117 | And finally, some of the master functionality are mapped to the 118 | channel sliders. 119 | 120 | * The *headphone gain* is mapped to the 7th slider. 121 | 122 | g = "[Master]" 123 | c.input(0x06).does g, "headVolume" 124 | 125 | * The *headphone mix* is mapped to the 6th slider. 126 | 127 | c.input(0x05).does g, "headMix" 128 | 129 | * The *crossfader* is mapped to the 2nd slider. 130 | 131 | c.input(0x02).does b.soft g, "crossfader" 132 | 133 | * The main *balance* is mapped to the 1st slider. 134 | 135 | c.input(0x01).does b.soft g, "balance" 136 | 137 | 138 | #### Deck controls 139 | 140 | Then, we create a chooser object over the *pfl* (headphone) parameter, 141 | so we will have only one deck with PFL activated at a time. 142 | Also, this will let us change the behaviour of some *transport* 143 | controls depending on which deck is *selected* -- i.e, has PFL 144 | enabled. 145 | 146 | @decks = b.chooser() 147 | 148 | Finally we add the per-deck controls, that are defined in `addDeck`. 149 | 150 | @addDeck 0 151 | @addDeck 1 152 | 153 | addDeck: (i) -> 154 | assert i in [0, 1] 155 | 156 | g = "[Channel#{i+1}]" 157 | offset = if i == 0 then [3, 2, 1, 0] else [4, 5, 6, 7] 158 | 159 | ##### Channel sections 160 | 161 | * The top 8 knobs are mapped to the two decks mixer filter section 162 | (low, mid, high, gain). They spread out from the center, i.e. the 163 | 4th and 5th knob control the low EQ filter, the 3rd and 6th knob 164 | control the mid EQ filter, and so on. 165 | 166 | c.input(0x10 + offset[0]).does g, "filterLow" 167 | c.input(0x10 + offset[1]).does g, "filterMid" 168 | c.input(0x10 + offset[2]).does g, "filterHigh" 169 | c.input(0x10 + offset[3]).does b.soft g, "pregain" 170 | 171 | * Then two central channel sections (4th and 5th) control the 172 | following parameters of the left and right deck: 173 | 174 | * S: Selects the deck *PFL*. 175 | * M: *Cue* button for the deck. 176 | * R: *Play* button for the deck. 177 | * The fader controls the *volume* of the deck. 178 | 179 | c.control(0x20 + offset[0]).does @decks.add g, "pfl" 180 | c.control(0x30 + offset[0]).does g, "cue_default", g, "cue_indicator" 181 | c.control(0x40 + offset[0]).does g, "play" 182 | c.input(0x00 + offset[0]).does g, "volume" 183 | 184 | * The two furthest channel sections (1st and 8th) control the pitch 185 | related stuff of the two decks. 186 | 187 | * S: *Bpm tap*, also shows the speed. 188 | * M: Toggles *key lock*. 189 | * R: Sets the *beat grid* to match the current playhead position. 190 | * The fader controls the *pitch* of the deck. 191 | 192 | c.control(0x20 + offset[3]).does g, "bpm_tap", g, "beat_active" 193 | c.control(0x30 + offset[3]).does g, "keylock" 194 | c.control(0x40 + offset[3]).does g, "beats_translate_curpos" 195 | c.input(0x00 + offset[3]).does b.soft g, "rate" 196 | 197 | Then, we have some looping related buttons in the middle. Also, these 198 | are the hotcue trigger and clear with the *cycle* and *marker* 199 | modifiers. 200 | 201 | * The *S, M, R* buttons of the central channels (2nd, 3rd, 6th, 7th) 202 | have different functionality, depending on the modifiers. 203 | * Normally, they *toggle loops* of size 1, 2, 4 and 8 beats. The *R* 204 | buttons of these sections control *loop double* and *halve.* 205 | * When the *cycle* button is held, they *launch hot-cues*. 206 | * When the *set* button is held, they *clear hot-cues*. 207 | 208 | c.control(0x20 + offset[1]) 209 | .when(@cycle, g, "hotcue_1_activate", g, "hotcue_1_enabled") 210 | .else.when(@marker, g, "hotcue_1_clear", g, "hotcue_1_enabled") 211 | .else g, "beatloop_2_toggle", g, "beatloop_2_enabled" 212 | c.control(0x20 + offset[2]) 213 | .when(@cycle, g, "hotcue_2_activate", g, "hotcue_2_enabled") 214 | .else.when(@marker, g, "hotcue_2_clear", g, "hotcue_2_enabled") 215 | .else g, "beatloop_4_toggle", g, "beatloop_4_enabled" 216 | c.control(0x30 + offset[1]) 217 | .when(@cycle, g, "hotcue_3_activate", g, "hotcue_3_enabled") 218 | .else.when(@marker, g, "hotcue_3_clear", g, "hotcue_3_enabled") 219 | .else g, "beatloop_8_toggle", g, "beatloop_8_enabled" 220 | c.control(0x30 + offset[2]) 221 | .when(@cycle, g, "hotcue_4_activate", g, "hotcue_4_enabled") 222 | .else.when(@marker, g, "hotcue_4_clear", g, "hotcue_4_enabled") 223 | .else g, "beatloop_16_toggle", g, "beatloop_16_enabled" 224 | c.control(0x40 + offset[1]) 225 | .when(@cycle, g, "hotcue_5_activate", g, "hotcue_5_enabled") 226 | .else.when(@marker, g, "hotcue_5_clear", g, "hotcue_5_enabled") 227 | .else g, "loop_halve" 228 | c.control(0x40 + offset[2]) 229 | .when(@cycle, g, "hotcue_6_activate", g, "hotcue_6_enabled") 230 | .else.when(@marker, g, "hotcue_6_clear", g, "hotcue_6_enabled") 231 | .else g, "loop_double" 232 | 233 | ##### Transport section 234 | 235 | These per-deck controls of the transport section have effect on the 236 | *selected track*. We consider the current PFL track to be the 237 | selected track. 238 | 239 | The *<<* and *>>* buttons are a bit more complicated. We want them 240 | to behave as *nudge* buttons for the selected track, but we want the 241 | *cycle* modifier to change the nudge speed. 242 | 243 | See how we use the `behaviour.and` condition combinator to mix the 244 | conditions. We also use `control.else.when` to simplify the negative 245 | condition. 246 | 247 | * The *<<* and *>>* buttons nudge and scroll over the selected track. 248 | 249 | chooseCycle = v.and @cycle, @decks.activator i 250 | @nudgeUpButton 251 | .when(chooseCycle, b.toggle 0, 0.5, g, "wheel") 252 | .else.when @decks.activator(i), b.toggle 0, 0.1, g, "wheel" 253 | @nudgeDownButton 254 | .when(chooseCycle, b.toggle 0, -0.5, g, "wheel") 255 | .else.when @decks.activator(i), b.toggle 0, -0.1, g, "wheel" 256 | 257 | * The *track<* and *track>* buttons control the selected track *fast 258 | forward* and *fast rewind*. 259 | 260 | @fwdButton.when @decks.activator(i), g, "fwd" 261 | @backButton.when @decks.activator(i), g, "back" 262 | 263 | * Load *stop* button loads the selected track in the selected deck. 264 | 265 | @loadTrack.when @decks.activator(i), g, "LoadSelectedTrack" 266 | 267 | * The *play* and *record* buttons synchronize to the other track. The 268 | *play* button can be held to enable master synchronization for the deck. 269 | 270 | @sync.when @decks.activator(i), g, "sync_enabled" 271 | @syncTempo.when @decks.activator(i), g, "beatsync" 272 | 273 | ### Initialization 274 | 275 | The **init** method is called by Mixxx when the script is loaded. Here 276 | we can initialize the state of Mixxx. In our case, we select the first 277 | deck, such that all transport buttons are directly functional. 278 | 279 | init: -> 280 | @decks.activate 0 281 | 282 | License 283 | ------- 284 | 285 | > Copyright (C) 2013 Juan Pedro Bolívar Puente 286 | > 287 | > This program is free software: you can redistribute it and/or 288 | > modify it under the terms of the GNU General Public License as 289 | > published by the Free Software Foundation, either version 3 of the 290 | > License, or (at your option) any later version. 291 | > 292 | > This program is distributed in the hope that it will be useful, 293 | > but WITHOUT ANY WARRANTY; without even the implied warranty of 294 | > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 295 | > GNU General Public License for more details. 296 | > 297 | > You should have received a copy of the GNU General Public License 298 | > along with this program. If not, see . 299 | -------------------------------------------------------------------------------- /src/control.litcoffee: -------------------------------------------------------------------------------- 1 | mixco.control 2 | ============= 3 | 4 | > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | > - **View me [on a static web](http://sinusoid.es/mixco/src/control.html)** 6 | > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/src/control.litcoffee)** 7 | 8 | Defines different hardware controls. 9 | 10 | {multi} = require 'heterarchy' 11 | {indent, hexStr, assert, factory, xmlTag, joinLn} = require './util' 12 | behaviour = require './behaviour' 13 | {some, extend} = require 'underscore' 14 | 15 | Constants 16 | --------- 17 | 18 | exports.MIDI_NOTE_ON = MIDI_NOTE_ON = 0x9 19 | exports.MIDI_NOTE_OFF = MIDI_NOTE_OFF = 0x8 20 | exports.MIDI_CC = MIDI_CC = 0xB 21 | exports.MIDI_PITCHBEND = MIDI_PITCHBEND = 0xE 22 | 23 | Utilities 24 | --------- 25 | 26 | The **midi** function returns an object representing a MIDI identifier 27 | for a control. 28 | 29 | midiId = (message = MIDI_CC, midino = 0, channel = 0) -> 30 | message: message 31 | midino: midino 32 | channel: channel 33 | status: -> (@message << 4) | @channel 34 | configMidi: (depth) -> 35 | """ 36 | #{indent depth}#{hexStr @status()} 37 | #{indent depth}#{hexStr @midino} 38 | """ 39 | exports.midiId = midiId 40 | 41 | The **noteIds** and **ccIds** returns a list with the MIDI messages 42 | needed to identify a control based on notes or control signals. 43 | 44 | pbIds = -> [ midiId(MIDI_PITCHBEND, 0, arguments...) ] 45 | noteOnIds = -> [ midiId(MIDI_NOTE_ON, arguments...) ] 46 | noteIds = -> [ midiId(MIDI_NOTE_ON, arguments...) 47 | , midiId(MIDI_NOTE_OFF, arguments...) ] 48 | ccIds = -> [ midiId(MIDI_CC, arguments...) ] 49 | 50 | exports.pbIds = pbIds 51 | exports.noteOnIds = noteOnIds 52 | exports.noteIds = noteIds 53 | exports.ccIds = ccIds 54 | 55 | The **event** function returns an object representing an script event 56 | coming from Mixxx. 57 | 58 | exports.event = event = (channel, control, value, status, group) -> 59 | channel: channel 60 | control: control 61 | value: switch status >> 4 62 | when MIDI_PITCHBEND then (value * 128.0 + control) / 128.0 63 | else value 64 | status: status 65 | group: group 66 | message: -> @status >> 4 67 | pressed: status >> 4 != MIDI_NOTE_OFF && value 68 | 69 | Controls 70 | -------- 71 | 72 | Base class for all control types. 73 | 74 | class exports.Control extends behaviour.Actor 75 | 76 | constructor: (@ids = [midiId()], args...) -> 77 | @else = => @_else arguments... 78 | @else.when = => @_elseWhen arguments... 79 | @else_ = @else 80 | super() 81 | if not (@ids instanceof Array) 82 | @ids = ccIds @ids, args... 83 | @_behaviours = [] 84 | @_controlRegistry?(@) 85 | 86 | The following set of methods define the behaviour of the control. A 87 | control can have several behaviours at the same time. Note that when 88 | passing behaviours to these methods (which is always the last 89 | parameter) you can either pass a *Behaviour* object or a *key* and 90 | *group* strings that will be puto directly into a `behaviour.map`. 91 | 92 | Thera are three kinds of behaviours we can associate to the control: 93 | 94 | * With **does** we associate behaviours that are always *active*, 95 | unconditionally. 96 | 97 | * With **when** we associate behaviours that are only *active* when some 98 | condition is met. This `condition` is a boolean `behaviour.Value` 99 | object. 100 | 101 | * With **else** we associate behaviours that are only active when no 102 | other *when* behaviour is *active*. 103 | 104 | does: (args...) -> 105 | assert not @_isInit 106 | @_behaviours.push @registerBehaviour behaviour.toBehaviour args... 107 | this 108 | 109 | when: (args...) -> 110 | assert not @_isInit 111 | @_lastWhen = behaviour.when args... 112 | @_behaviours.push @registerBehaviour @_lastWhen 113 | this 114 | 115 | _elseWhen: (args...) -> 116 | assert @_lastWhen?, 117 | "'elseWhen' must be preceded by 'when' or 'elseWhen'" 118 | @_lastWhen = @_lastWhen.else.when args... 119 | @_behaviours.push @registerBehaviour @_lastWhen 120 | this 121 | 122 | _else: (args...) -> 123 | assert @_lastWhen?, 124 | "'else' must be preceded by 'when' or 'elseWhen'" 125 | @_lastWhen = @_lastWhen.else args... 126 | @_behaviours.push @registerBehaviour @_lastWhen 127 | @_lastWhen = undefined 128 | this 129 | 130 | init: (script) -> 131 | @script = script 132 | assert not @_isInit 133 | for b in @_behaviours 134 | b.enable script, this 135 | @_isInit = true 136 | 137 | shutdown: (script) -> 138 | assert script == @script 139 | assert @_isInit 140 | for b in @_behaviours 141 | b.disable script, this 142 | @_isInit = false 143 | delete @script 144 | 145 | registerBehaviour: (b) -> b 146 | configInputs: (depth, script) -> 147 | configOutputs: (depth, script) -> 148 | 149 | The **setRegistry** is a class method that is called by the scripts to 150 | automate the registration of the controls. It is called directly by 151 | the scripts base class. 152 | 153 | setRegistry: (registry) -> 154 | assert not @_controlRegistry? or not registry? 155 | @_controlRegistry = registry 156 | 157 | 158 | ### Input 159 | 160 | An *input control* can process inputs from the hardware. 161 | 162 | class exports.InControl extends exports.Control 163 | 164 | init: (script) -> 165 | super 166 | if @needsHandler() 167 | script.registerHandler \ 168 | ((args...) => @emit 'event', event args...), 169 | @handlerId() 170 | 171 | A input control can be configured with the same type of *options* that 172 | behaviours can. These are documented in the `mixco.behaviour` 173 | module. An *options chooser* syntax is also available. 174 | 175 | option: (options...) -> 176 | (@_options ?= []).push options... 177 | for beh in @_behaviours 178 | beh.option options... 179 | this 180 | 181 | @property 'options', -> behaviour.makeOptionsChooser @ 182 | 183 | registerBehaviour: (beh) -> 184 | if @_options? 185 | beh.option @_options... 186 | beh 187 | 188 | The control will listen to the --via a *handler*-- only when the 189 | behaviours need it. If there is only one behaviour in the control and 190 | this can be directly mapped, the midi messages will be connected 191 | directly in the XML file. Otherwise, the control will request to 192 | process the MIDI messages via the script, and it will emit a `event` 193 | signal when they are received. 194 | 195 | needsHandler: -> 196 | @_behaviours.length != 1 or 197 | not @_behaviours[0].directInMapping() or 198 | some @_behaviours[0]._options, (opt) -> not opt.name 199 | 200 | handlerId: -> 201 | "x#{@ids[0].status().toString(16)}_x#{@ids[0].midino.toString(16)}" 202 | 203 | configInputs: (depth, script) -> 204 | if @needsHandler() 205 | mapping = 206 | group: "[Master]" 207 | key: script.handlerKey @handlerId() 208 | else 209 | mapping = @_behaviours[0].directInMapping() 210 | joinLn(@configInMapping depth, mapping, id for id in @ids) 211 | 212 | configInMapping: (depth, mapping, id) -> 213 | """ 214 | #{indent depth} 215 | #{indent depth+1}#{mapping.group} 216 | #{indent depth+1}#{mapping.key} 217 | #{id.configMidi depth+1} 218 | #{indent depth+1} 219 | #{@configOptions depth+2} 220 | #{indent depth+1} 221 | #{indent depth} 222 | """ 223 | 224 | configOptions: (depth) -> 225 | if @needsHandler() 226 | "#{indent depth}" 227 | else if @_behaviours[0]._options?.length > 0 228 | joinLn( 229 | for opt in @_behaviours[0]._options 230 | if opt.name? 231 | "#{indent depth}<#{opt.name}/>") 232 | else 233 | "#{indent depth}" 234 | 235 | ### Output 236 | 237 | An *output control* can send data to the hardware. 238 | 239 | class exports.OutControl extends exports.Control 240 | 241 | constructor: -> 242 | super 243 | @_states = 244 | on: 0x7f 245 | off: 0x00 246 | disable: 0x00 247 | 248 | send: (state) -> 249 | @doSend state 250 | 251 | states: (states) -> 252 | extend @_states, states 253 | @ 254 | 255 | doSend: (state) -> 256 | for id in @ids 257 | if id.message != MIDI_NOTE_OFF 258 | if state of @_states 259 | @script.mixxx.midi.sendShortMsg \ 260 | id.status(), id.midino, @_states[state] 261 | else 262 | @script.mixxx.midi.sendShortMsg \ 263 | id.status(), id.midino, state 264 | 265 | init: -> 266 | 267 | We should remove the send function before enabling behaviours. 268 | 269 | if not @needsSend() 270 | @send = undefined 271 | super 272 | 273 | shutdown: -> 274 | @doSend 'disable' 275 | super 276 | 277 | needsSend: -> 278 | @_behaviours.length != 1 or 279 | not @_behaviours[0].directOutMapping() 280 | 281 | configOutputs: (depth, script) -> 282 | mapping = not @needsSend() and @_behaviours[0].directOutMapping() 283 | if mapping 284 | joinLn(@configOutMapping depth, mapping, id for id in @ids) 285 | 286 | configOutMapping: (depth, mapping, id) -> 287 | if id.message != MIDI_NOTE_OFF 288 | options = joinLn [ 289 | xmlTag 'minimum', mapping.minimum, depth+1 290 | xmlTag 'maximum', mapping.maximum, depth+1 291 | ] 292 | """ 293 | #{indent depth} 294 | #{indent depth+1}#{mapping.group} 295 | #{indent depth+1}#{mapping.key} 296 | #{id.configMidi depth+1} 297 | #{indent depth+1}#{hexStr @_states['on']} 298 | #{indent depth+1}#{hexStr @_states['off']} 299 | #{options} 300 | #{indent depth} 301 | """ 302 | 303 | ### Concrete controls 304 | 305 | #### Factories 306 | 307 | Lets provide a series of factories to make scripts read more declarative. 308 | 309 | exports.input = factory exports.InControl 310 | exports.output = factory exports.OutControl 311 | 312 | #### Input and output 313 | 314 | Represents a hardware control that can do both input and output. This 315 | is often the case for buttons that have a LED. 316 | 317 | class exports.InOutControl extends multi exports.InControl, 318 | exports.OutControl 319 | 320 | exports.control = factory exports.InOutControl 321 | 322 | 323 | License 324 | ------- 325 | 326 | > Copyright (C) 2013 Juan Pedro Bolívar Puente 327 | > 328 | > This program is free software: you can redistribute it and/or 329 | > modify it under the terms of the GNU General Public License as 330 | > published by the Free Software Foundation, either version 3 of the 331 | > License, or (at your option) any later version. 332 | > 333 | > This program is distributed in the hope that it will be useful, 334 | > but WITHOUT ANY WARRANTY; without even the implied warranty of 335 | > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 336 | > GNU General Public License for more details. 337 | > 338 | > You should have received a copy of the GNU General Public License 339 | > along with this program. If not, see . 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mixco 2 | ===== 3 | 4 | [**Mixco**][mixco] is a framework for creating hardware 5 | [controller scripts][scripts] for the amazing [Mixxx][mixxx] DJ 6 | software. It makes the process **easier** and **faster**, and 7 | resulting scripts are often more **robust**, ready to be rock big 8 | parties. 9 | 10 | And remember, this is [Free Software][gnu]. 11 | 12 | [scripts]: http://mixxx.org/wiki/doku.php/midi_scripting 13 | [gnu]: http://www.gnu.org/philosophy/free-sw.html 14 | [mixxx]: http://www.mixxx.org 15 | [lcs]: http://coffeescript.org/#literate 16 | [mixco]: http://sinusoid.es/mixco 17 | 18 | 19 | 20 | 21 | 22 | Installation 23 | ------------ 24 | 25 | Mixco is based on the [NodeJS][nodejs] JavaScript development 26 | environment so, first, you need to [install it][nodedown]. Then, just 27 | run in the command line: 28 | 29 | > ``` 30 | > npm install -g mixco 31 | > ``` 32 | 33 | You can also [browse the code on Github][github]. 34 | 35 | [github]: http://github.com/arximboldi/mixco 36 | [nodejs]: http://nodejs.org/ 37 | [nodedown]: http://nodejs.org/download 38 | 39 | Examples 40 | -------- 41 | 42 | Mixco comes with a series of factory controller scripts. They are 43 | well documented and their code serves as good tutorial on how to use 44 | the framework. 45 | 46 | - [Novation Twitch][script.novation_twitch] 47 | - [Korg Nanokontrol 2][script.korg_nanokontrol2] 48 | - [M-Audio Xponent][script.maudio_xponent] 49 | 50 | To **install** these, run on the command line: 51 | 52 | > ``` 53 | > mixco --factory 54 | > ``` 55 | 56 | [script.korg_nanokontrol2]: http://sinusoid.es/mixco/script/korg_nanokontrol2.mixco.html 57 | [script.maudio_xponent]: http://sinusoid.es/mixco/script/maudio_xponent.mixco.html 58 | [script.novation_twitch]: http://sinusoid.es/mixco/script/novation_twitch.mixco.html 59 | 60 | Features 61 | -------- 62 | 63 | #### Write more high level code 64 | 65 | The programming interface is very [fluent][fluent] and 66 | [declarative][declarative], allowing you to write more high level 67 | code. For example, imagine this feature: *when the sync button aligns 68 | the phase or not depending on whether the shift button is pressed*. 69 | While normally this would involve quite a few lines of code detecting 70 | whether shift is pressed, controlling the lights of the buttons, and 71 | so on, With Mixco this can be written simply as: 72 | 73 | ```js 74 | var mixco = require('mixco') 75 | var c = mixco.controls 76 | var b = mixco.behaviours 77 | 78 | // ... in your script constructor ... 79 | var shift = b.modifier() 80 | c.control(c.noteIds(0x01, 0)) 81 | .does (shift) 82 | c.control(c.noteIds(0x02, 0)) 83 | .when (shift, "[Channel1]", "beatsync_tempo") 84 | .else_( "[Channel1]", "beatsync") 85 | ``` 86 | 87 | #### No editing XML files 88 | 89 | Normally, Mixxx requires that you describe every MIDI message that 90 | your controller can receive in [a verbose XML file][xmlspec]. Mixco 91 | generates this file for you from your JavaScript file, so you can 92 | focus on adding cool features to your mapping. 93 | 94 | [xmlspec]: http://mixxx.org/wiki/doku.php/midi_controller_mapping_file_format 95 | 96 | #### No duplicate per-deck code 97 | 98 | Most DJ oriented MIDI controllers are mostly symmetric, with controls 99 | duplicated per deck. Since we don't need a XML mapping, you can avoid 100 | duplicating the code: just write a function that defines the 101 | functionality for one deck, and call it several times. For an 102 | example, look at the `addDeck()` function of 103 | [this tutorial script][script.novation_twitch]. 104 | 105 | #### Use external libraries and modularize your code 106 | 107 | If your script is big and complicated, you can split it into multiple 108 | files to make it easier to maintain, by using the `require()` 109 | function. Even cooler, most libraries installed with `npm`, the 110 | [NodeJS package manager][npm], work out of the box. Mixco will 111 | compile your script into a single bundle that Mixxx can use and is 112 | easy to redistribute. For example: 113 | 114 | ```js 115 | // file: my-utils.js 116 | exports.doSomething = function() { ... } 117 | ``` 118 | 119 | ```js 120 | // file: my-script.mixco.js 121 | // importing the framework 122 | var mixco = require('mixco') 123 | // using a external library: https://www.npmjs.com/package/underscore 124 | var _ = require('underscore') 125 | // Using custom module 126 | var utils = require('./my-utils') 127 | utils.doSomething() 128 | ``` 129 | 130 | #### Automatically test your code 131 | 132 | In JavaScript, it is easy to make tiny mistake that break your code. 133 | Mixco can run some basic tests on your scripts, so some simple 134 | problems can be found before even loading it into Mixxx. 135 | 136 | Also, Mixco be run in **watch mode**: whenever you change your script, 137 | it will re-run the tests and, when successful, recompile the script so 138 | it's reloaded inside Mixxx. 139 | 140 | #### Use languages different from JavaScript 141 | 142 | If you are like me, you don't like JavaScript so much. Mixco supports 143 | [CoffeeScript][coffee], a nice language with syntax inspired by Python 144 | and Ruby that polishes some of the rough corners of JavaScript. Mixco 145 | can automatically compile CoffeeScript script to JavaScript and, in 146 | the future, other languages too. 147 | 148 | #### Generate beautiful documentation 149 | 150 | Documenting a script is hard but important: otherwise your users are 151 | clue-less about what each button of the controller does. Mixco 152 | encourages a style of programming known as 153 | [literate programming][litprog], which mixes code with documentation 154 | about what it does. If you code in that style, it can generate 155 | beautiful [web pages like this][script.novation_twitch], that teach 156 | your users not only what the script does, but also what code they 157 | should is creating that functionality, encouraging people to improve 158 | the scripts and create their own mods. 159 | 160 | [npm]: https://www.npmjs.com/ 161 | [litprog]: https://en.wikipedia.org/wiki/Literate_programming 162 | [declarative]: http://en.wikipedia.org/wiki/Declarative_programming 163 | [fluent]: http://en.wikipedia.org/wiki/Fluent_interface 164 | [lcs]: http://coffeescript.org/#literate 165 | [coffee]: http://coffeescript.org/ 166 | [script.korg_nanokontrol2]: http://sinusoid.es/mixco/script/korg_nanokontrol2.mixco.html 167 | [script.maudio_xponent]: http://sinusoid.es/mixco/script/maudio_xponent.mixco.html 168 | [script.novation_twitch]: http://sinusoid.es/mixco/script/novation_twitch.mixco.html 169 | 170 | Usage 171 | ----- 172 | 173 | Mixco comes with a program called, ehem, **mixco**, that compiles all 174 | the scripts in the current directory to a form that can be used inside 175 | Mixxx. Try this by creating a file `my_script.mixco.js` and run this 176 | in the same folder: 177 | 178 | > ``` 179 | > mixco 180 | > ``` 181 | 182 | > ``` 183 | > info: inputs: . 184 | > info: output directory: mixco-output 185 | > info: generated: <...>/mixco-output/my_script.mixco.output.js 186 | > info: generated: <...>/mixco-output/my_script.mixco.output.midi.xml 187 | > ``` 188 | 189 | Mixco can `watch` the filesystem so you don't need to re-run the 190 | command whenever you change the script. It can also automatically run 191 | tests on it and copy the script to some location, so Mixxx can see it. 192 | For example, if you are on Linux, you might want to run the command 193 | like this: 194 | 195 | > ``` 196 | > mixco --watch --test -o /usr/share/mixxx/controllers 197 | > ``` 198 | 199 | The **mixco** command can do much more: 200 | 201 | > ``` 202 | > mixco --help 203 | > 204 | > Usage: 205 | > mixco [options] [...] 206 | > 207 | > Mixco is a framework for making DJ controller scripts for Mixxx. 208 | > 209 | > This program can compile all the Mixco scripts into .js and .xml files 210 | > that can be used inside Mixxx. Mixco scripts have one of the following 211 | > extensions: *.mixco.js, *.mixco.coffee, *.mixco.litcoffee. When no is 212 | > passed, it will compile all scripts in the current directory. When an is 213 | > a directory, all scripts found in it will be compiled. 214 | > 215 | > Options: 216 | > -o, --output=PATH Directory where to put the generated files 217 | > Default: mixco-output 218 | > -r, --recursive Recursively look for scripts in input directories 219 | > -w, --watch Watch scripts for changes and recompile them 220 | > -T, --self-test Test the framework before compilation 221 | > -t, --test Test the input scripts before compilation 222 | > --factory Compile the scripts that come with Mixco 223 | > -h, --help Display this help message and exit 224 | > -V, --verbose Print more output 225 | > -v, --version Output version information and exit 226 | > 227 | > More info and bug reports at: 228 | > ``` 229 | 230 | Documentation 231 | ------------- 232 | 233 | ### Scripts 234 | 235 | * [script.korg_nanokontrol2][script.korg_nanokontrol2] 236 | * [script.maudio_xponent][script.maudio_xponent] 237 | * [script.novation_twitch][script.novation_twitch] 238 | 239 | ### API 240 | 241 | * [mixco.behaviour][mixco.behaviour] 242 | * [mixco.cli][mixco.cli] 243 | * [mixco.control][mixco.control] 244 | * [mixco.console][mixco.console] 245 | * [mixco.script][mixco.script] 246 | * [mixco.transform][mixco.transform] 247 | * [mixco.util][mixco.util] 248 | * [mixco.value][mixco.value] 249 | 250 | ### Tests 251 | 252 | * [spec.mixco.behaviour][spec.mixco.behaviour] 253 | * [spec.mixco.control][spec.mixco.control] 254 | * [spec.mixco.script][spec.mixco.script] 255 | * [spec.mixco.value][spec.mixco.value] 256 | * [spec.mock][spec.mock] 257 | * [spec.scripts][spec.scripts] 258 | 259 | [script.korg_nanokontrol2]: http://sinusoid.es/mixco/script/korg_nanokontrol2.mixco.html 260 | [script.maudio_xponent]: http://sinusoid.es/mixco/script/maudio_xponent.mixco.html 261 | [script.novation_twitch]: http://sinusoid.es/mixco/script/novation_twitch.mixco.html 262 | 263 | [mixco.behaviour]: http://sinusoid.es/mixco/src/behaviour.html 264 | [mixco.cli]: http://sinusoid.es/mixco/src/cli.html 265 | [mixco.control]: http://sinusoid.es/mixco/src/control.html 266 | [mixco.console]: http://sinusoid.es/mixco/src/console.html 267 | [mixco.script]: http://sinusoid.es/mixco/src/script.html 268 | [mixco.transform]: http://sinusoid.es/mixco/src/transform.html 269 | [mixco.util]: http://sinusoid.es/mixco/src/util.html 270 | [mixco.value]: http://sinusoid.es/mixco/src/value.html 271 | 272 | [spec.mixco.behaviour]: http://sinusoid.es/mixco/spec/mixco/behaviour.spec.html 273 | [spec.mixco.control]: http://sinusoid.es/mixco/spec/mixco/control.spec.html 274 | [spec.mixco.script]: http://sinusoid.es/mixco/spec/mixco/script.spec.html 275 | [spec.mixco.value]: http://sinusoid.es/mixco/spec/mixco/value.spec.html 276 | [spec.mock]: http://sinusoid.es/mixco/spec/mock.html 277 | [spec.scripts]: http://sinusoid.es/mixco/spec/scripts.spec.html 278 | 279 | Contributing 280 | ------------ 281 | 282 | Please, log **bugs, questions or feature requests** in the 283 | [Github issue tracker][issues]. 284 | 285 | We are also **happy to accept contributions**, either improvements to the 286 | frameworks, new factory scripts or documentation 287 | enhancements. [Fork us on GitHub][github] or by running: 288 | 289 | > ``` 290 | > git clone https://github.com/arximboldi/mixco.git 291 | > ``` 292 | 293 | You can also **contact me** by email at: `raskolnikov@gnu.org`. 294 | 295 | [github]: http://github.com/arximboldi/mixco 296 | [issues]: http://github.com/arximboldi/mixco/issues 297 | 298 | License 299 | ------- 300 | 301 | > Copyright (C) 2013, 2015 Juan Pedro Bolívar Puente 302 | > 303 | > This program is free software: you can redistribute it and/or 304 | > modify it under the terms of the GNU General Public License as 305 | > published by the Free Software Foundation, either version 3 of the 306 | > License, or (at your option) any later version. 307 | > 308 | > This program is distributed in the hope that it will be useful, 309 | > but WITHOUT ANY WARRANTY; without even the implied warranty of 310 | > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 311 | > GNU General Public License for more details. 312 | > 313 | > You should have received a copy of the GNU General Public License 314 | > along with this program. If not, see . 315 | -------------------------------------------------------------------------------- /src/cli.litcoffee: -------------------------------------------------------------------------------- 1 | mixco.cli 2 | ========= 3 | 4 | > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | > - **View me [on a static web](http://sinusoid.es/mixco/src/cli.html)** 6 | > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/src/cli.litcoffee)** 7 | 8 | This module implements the meat of the `mixco` script that takes Mixco 9 | scripts and compiles them such that they can be used inside Mixxx. 10 | 11 | require 'coffee-script/register' 12 | _ = require 'underscore' 13 | path = require 'path' 14 | fs = require 'fs' 15 | logger = require 'winston' 16 | stream = require 'stream' 17 | promise = require 'node-promise' 18 | {inspect} = require 'util' 19 | 20 | First, we find out what is the name of the script that we are running. 21 | 22 | MIXCO = path.basename process.argv[1] 23 | 24 | We use the `package.json` data to get the script metadata. 25 | 26 | packageJsonPath = path.join __dirname, "..", "package.json" 27 | package_ = JSON.parse fs.readFileSync packageJsonPath 28 | MIXCO_VERSION = package_.version 29 | MIXCO_AUTHOR = package_.author 30 | MIXCO_DESCRIPTION = package_.description 31 | MIXCO_HOMEPAGE = package_.homepage 32 | 33 | And then we define some defaults. 34 | 35 | MIXCO_DEFAULT_OUTPUT_DIR = path.join ".", "mixco-output" 36 | MIXCO_DEFAULT_INPUTS = [ "." ] 37 | MIXCO_EXT_GLOBS = [ 38 | "*.mixco.js" 39 | "*.mixco.coffee" 40 | "*.mixco.litcoffee" 41 | ] 42 | 43 | The `colors` library allows us print colored output. We shall not 44 | name colors explicitly through our script, but instead only used the 45 | theme names defined here. 46 | 47 | colors = require 'colors/safe' 48 | colors.setTheme 49 | data: 'yellow' 50 | 51 | The **args** function parses the command line arguments and returns an 52 | object containing the options and arguments, as parsed. It will also 53 | output and exit when passed `--help`, `--version`, etc... 54 | 55 | args = -> 56 | _.defaults (require "argp" 57 | .createParser once: true 58 | .allowUndefinedArguments() 59 | .usages [ "", "#{MIXCO} [options] [...]" ] 60 | .on "argument", (argv, argument) -> 61 | argv.inputs ?= [] 62 | argv.inputs.push argument 63 | .body() 64 | .text MIXCO_DESCRIPTION 65 | .text "\n 66 | This program can compile all the Mixco scripts 67 | into .js and .xml files that can be used inside Mixxx. 68 | Mixco scripts have one of the following extensions: 69 | #{MIXCO_EXT_GLOBS.join ', '}. When no is 70 | passed, it will compile all scripts in the current 71 | directory. When an is a directory, all scripts 72 | found in it will be compiled." 73 | .text() 74 | .text "Options:" 75 | .option 76 | short: "o" 77 | long: "output" 78 | description: "Directory where to put the generated files 79 | Default: #{MIXCO_DEFAULT_OUTPUT_DIR}" 80 | metavar: "PATH" 81 | default: MIXCO_DEFAULT_OUTPUT_DIR 82 | .option 83 | short: "r" 84 | long: "recursive" 85 | description: "Recursively look for scripts in input directories" 86 | .option 87 | short: "w" 88 | long: "watch" 89 | description: "Watch scripts for changes and recompile them" 90 | .option 91 | short: "T" 92 | long: "self-test" 93 | description: "Test the framework before compilation" 94 | .option 95 | short: "t" 96 | long: "test" 97 | description: "Test the input scripts before compilation" 98 | .option 99 | long: "factory" 100 | description: "Compile the scripts that come with Mixco" 101 | .option 102 | long: "fatal-tests" 103 | description: "Make process fail when tests fail" 104 | .help() 105 | .option 106 | short: "V" 107 | long: "verbose" 108 | description: "Print more output" 109 | .version(MIXCO_VERSION) 110 | .text "\nMore info and bug reports at: <#{MIXCO_HOMEPAGE}>" 111 | .argv()), 112 | inputs: MIXCO_DEFAULT_INPUTS 113 | 114 | The **sources** function takes a list of inputs, as passed by the 115 | user, and returns a list of `gulp` enabled globs that can be passed to 116 | `gulp.src` 117 | 118 | sources = (inputs, recursive) -> 119 | _.flatten inputs.map (input) -> 120 | stat = fs.statSync input 121 | if stat.isDirectory() 122 | MIXCO_EXT_GLOBS.map (glob) -> 123 | if recursive 124 | path.join input, "**", glob 125 | else 126 | path.join input, glob 127 | else 128 | [ input ] 129 | 130 | The **tasks** function will, given the gulp sources and an output 131 | directory, define all the `gulp` tasks. It returns the *gulp* module 132 | itself. 133 | 134 | tasks = (sources, output, opts={}) -> 135 | gulp = require 'gulp' 136 | cached = require 'gulp-cached' 137 | rename = require 'gulp-rename' 138 | 139 | testError = -> 140 | logger.error arguments... 141 | if opts.fatal_tests 142 | process.exit 1 143 | 144 | gulp.task 'self-test', -> 145 | if opts.self_test 146 | mocha = require 'gulp-mocha' 147 | specs = path.join __dirname, '..', 'test', 'mixco', '*.spec.coffee' 148 | logger.info "testing framework:", colors.data specs 149 | gulp.src specs, read: false 150 | .pipe mocha() 151 | .on 'error', testError 152 | 153 | gulp.task 'test', ['self-test'], -> 154 | if opts.test 155 | mocha = require 'gulp-mocha' 156 | specs = path.join __dirname, '..', 'test', 'scripts.spec.coffee' 157 | logger.info "testing input scripts:", colors.data specs 158 | process.env.MIXCO_TEST_INPUTS = sources.join ':' 159 | gulp.src specs, read: false 160 | .pipe mocha() 161 | .on 'error', -> testError 162 | 163 | gulp.task 'scripts', ['test'], -> 164 | ext = ".output.js" 165 | gulp.src sources 166 | .pipe cached 'scripts' 167 | .pipe changed output, ext 168 | .pipe browserified() 169 | .pipe rename extname: ext 170 | .pipe gulp.dest output 171 | .pipe logging "generated" 172 | 173 | gulp.task 'mappings', ['test'], -> 174 | ext = ".output.midi.xml" 175 | gulp.src sources 176 | .pipe cached 'sources' 177 | .pipe changed output, ext 178 | .pipe xmlMapped() 179 | .pipe rename extname: ext 180 | .pipe gulp.dest output 181 | .pipe logging "generated" 182 | 183 | gulp.task 'build', [ 'scripts', 'mappings' ] 184 | gulp.task 'watch', ['build'], -> gulp.watch sources, [ 'build' ] 185 | gulp 186 | 187 | We define a couple of helpers to extract parts of a path pointing to a 188 | Mixco script file. 189 | 190 | moduleName = (scriptPath) -> 191 | path.join (path.dirname scriptPath), 192 | path.basename scriptPath, path.extname scriptPath 193 | 194 | scriptName = (scriptPath) -> 195 | path.basename (moduleName scriptPath), ".mixco" 196 | 197 | logging = (str) -> 198 | through = require 'through2' 199 | through.obj (file, enc, next) -> 200 | logger.info "#{str}:", colors.data file.path 201 | next null, file 202 | 203 | changed = (dest, ext) -> 204 | changed_ = require 'gulp-changed' 205 | changed_ dest, 206 | extension: ext 207 | hasChanged: (stream, next, file, path) -> 208 | fs.stat path, (err, stat) -> 209 | if err or file.stat.mtime > stat.mtime 210 | stream.push file 211 | else 212 | logger.info "up to date:", colors.data path 213 | do next 214 | 215 | The **xmlMapped** gulpy plugin generates the `.midi.xml` Mixxx 216 | controller mapping files. 217 | 218 | consume = (stream) -> 219 | result = new promise.Promise 220 | chunks = [] 221 | stream.on 'data', (chunk) -> 222 | chunks.push chunk 223 | stream.on 'end', -> 224 | result.resolve Buffer.concat chunks 225 | stream.on 'error', (err) -> 226 | result.reject error 227 | result 228 | 229 | fork_ = (what, args) -> 230 | childp = require 'child_process' 231 | proc = childp.fork what, args, silent: true 232 | stdoutResult = consume proc.stdout 233 | exitResult = new promise.Promise 234 | proc.on 'error', (err) -> 235 | logger.error err 236 | next err, null 237 | proc.on 'exit', (code) -> 238 | if code == 0 239 | exitResult.resolve code 240 | else 241 | exitResult.reject new Error "Exit code: #{code}" 242 | promise.allOrNone stdoutResult, exitResult 243 | 244 | xmlMapped = -> 245 | through = require 'through2' 246 | through.obj (file, enc, next) -> 247 | moduleName_ = moduleName file.path 248 | scriptName_ = scriptName file.path 249 | logger.debug "compiling mapping for:", colors.data moduleName_ 250 | logger.debug " module:", colors.data moduleName_ 251 | logger.debug " script:", colors.data scriptName_ 252 | fork_ file.path, [ "-g" ] 253 | .then ([data, _]) -> 254 | file.contents = data 255 | next null, file 256 | , (err) -> 257 | logger.error "Error while generating mapping from:", 258 | colors.data file.path 259 | logger.error err 260 | 261 | The **browserified()** function returns a gulpy plugin that compiles a 262 | Mixco script (which is a NodeJS script) into a standalone bundle that 263 | can be loaded inside Mixxx. It also transforms it from Coffee-Script 264 | to JavaScript if necessary, and packages dependencies transparently 265 | (e.g underscore). This means that a Mixco script can be split across 266 | multiple files. It is recommended to only use the `.mixco.*` 267 | extension for the main script, where `mixco.script.register` is called. 268 | 269 | browserified = -> 270 | browserify = require 'browserify' 271 | through = require 'through2' 272 | globby = require 'globby' 273 | thisdir = path.dirname module.filename 274 | exclude = globby.sync [ path.join thisdir, "*.litcoffee" ] 275 | 276 | through.obj (file, enc, next) -> 277 | moduleName_ = moduleName file.path 278 | scriptName_ = scriptName file.path 279 | logger.debug "compiling script for:", colors.data file.path 280 | logger.debug " module:", colors.data moduleName_ 281 | logger.debug " script:", colors.data scriptName_ 282 | 283 | prepend = new Buffer """ 284 | /* 285 | * File generated with Mixco framework version: #{MIXCO_VERSION} 286 | * More info at: <#{MIXCO_HOMEPAGE}> 287 | */ 288 | \nMIXCO_SCRIPT_FILENAME = '#{file.path}';\n\n 289 | """ 290 | append = new Buffer """ 291 | \n#{scriptName_} = require('#{moduleName_}').#{scriptName_}; 292 | /* End of Mixco generated script */ 293 | """ 294 | finish = (err, res) -> 295 | if err 296 | logger.error 'Error while generating script for:', 297 | colors.data file.path 298 | logger.error err 299 | else 300 | file.contents = Buffer.concat [ 301 | prepend, res, append ] 302 | next err, file 303 | 304 | bundler = browserify (toStream "require('#{file.path}');"), 305 | extensions: [ ".js", ".coffee", ".litcoffee"] 306 | exclude.reduce ((b, fname) -> b.exclude fname), bundler 307 | .exclude 'coffee-script/register' 308 | .require moduleName_ 309 | .bundle finish 310 | 311 | The **main** function finally implements the meat of the command line 312 | script. It parses the arguments, sets up the logger and starts the 313 | appropriate task. 314 | 315 | exports.main = -> 316 | argv = args() 317 | logger.cli() 318 | logger.level = if argv.verbose then 'debug' else 'info' 319 | logger.debug "console arguments:\n", colors.data inspect argv 320 | logger.info "inputs:", colors.data argv['inputs'] 321 | logger.info "output directory:", colors.data argv['output'] 322 | 323 | if argv['factory'] 324 | argv['inputs'].push path.join __dirname, '..', 'script' 325 | srcs = sources argv['inputs'], argv['recursive'] 326 | logger.debug "gulp sources:", colors.data srcs 327 | 328 | gulp = tasks srcs, argv.output, 329 | self_test: argv['self-test'] 330 | test: argv['test'] 331 | fatal_tests: argv['fatal-tests'] 332 | 333 | task = if argv['watch'] then 'watch' else 'build' 334 | gulp.start task 335 | 336 | Oh, and there is this utility to create a stream from a plain string. 337 | 338 | class StringStream extends stream.Readable 339 | constructor: (@str) -> 340 | super 341 | _read: (size) -> 342 | @push @str 343 | @push null 344 | 345 | toStream = (str) -> new StringStream str 346 | -------------------------------------------------------------------------------- /script/maudio_xponent.mixco.litcoffee: -------------------------------------------------------------------------------- 1 | script.xponent 2 | ============== 3 | 4 | > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | > - **View me [on a static web](http://sinusoid.es/mixco/script/maudio_xponent.mixco.html)** 6 | > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/script/maudio_xponent.mixco.litcoffee)** 7 | 8 | Mixxx script file for the **M-Audio Xponent** controller. The numbers 9 | in the following picture will be used in the script to describe the 10 | functionality of the controls. 11 | 12 | ![Xponent Layout](http://sinusoid.es/mixco/pic/maudio_xponent.png) 13 | 14 | mixco = require 'mixco' 15 | {assert} = mixco.util 16 | c = mixco.control 17 | b = mixco.behaviour 18 | v = mixco.value 19 | 20 | mixco.script.register module, 21 | 22 | info: 23 | name: '[mixco] M-Audio Xponent' 24 | author: 'Juan Pedro Bolivar Puente ' 25 | wiki: 'https://sinusoid.es/mixco/script/maudio_xponent.mixco.html' 26 | forums: 'https://github.com/arximboldi/mixco/issues' 27 | description: 28 | """ 29 | Controller mapping for the M-Audio Xponent DJ controller. 30 | """ 31 | 32 | constructor: -> 33 | @decks = b.chooser() 34 | @addMaster 0 35 | @addMaster 1 36 | @addDeck 0 37 | @addDeck 1 38 | @addDeck 2 39 | @addDeck 3 40 | 41 | Global section 42 | -------------- 43 | 44 | Controls that do not have a per-deck functionality. 45 | 46 | addMaster: (bank) -> 47 | assert bank in [0, 1] 48 | 49 | offset = bank * 5 50 | ccId = (cc) -> c.ccIds cc, 2 + offset 51 | g = "[Master]" 52 | 53 | * **27.** Headphone mix. 54 | 55 | c.input(ccId 0x0D).does g, "headMix" 56 | 57 | * **39.** Crossfader. 58 | 59 | c.input(ccId 0x07).does g, "crossfader" 60 | 61 | ### Effects 62 | 63 | Most of knobs and buttons in **24** and **25** are dedicated to 64 | effects. Some of them are mapped per-deck --see later-- but some are 65 | mapped globally: 66 | 67 | * The *first and second knobs* of the *left deck* control the *super* 68 | and *mix* of the first effect unit. In the *right deck*, they do 69 | likewise for the second effect unit. 70 | 71 | c.input(0x0c, 0x00 + offset).does \ 72 | b.soft "[EffectRack1_EffectUnit1]", "super1" 73 | c.input(0x0d, 0x00 + offset).does \ 74 | b.soft "[EffectRack1_EffectUnit1]", "mix" 75 | 76 | c.input(0x0c, 0x01 + offset).does \ 77 | b.soft "[EffectRack1_EffectUnit2]", "super1" 78 | c.input(0x0d, 0x01 + offset).does \ 79 | b.soft "[EffectRack1_EffectUnit2]", "mix" 80 | 81 | * **16.** When in *MIDI mode*, the touch pad can be used to control 82 | the first two parameters of the first effect. *MIDI mode* can be 83 | enabled with the button labeled `MIDI` below the touch pad. 84 | Otherwise, the touch pad is used to control the computer mouse. 85 | 86 | c.control(0x09, 0x02 + offset) 87 | .does "[EffectRack1_EffectUnit1_Effect1]", "parameter1" 88 | c.control(0x08, 0x02 + offset) 89 | .does "[EffectRack1_EffectUnit1_Effect1]", "parameter2" 90 | 91 | * **17.** and **18.** When in *MIDI mode*, the touch pad buttons *toggle 92 | the first effect unit* for the first and second deck respectively. 93 | 94 | c.input(c.noteOnIds 0x00, 0x02 + offset) 95 | .does "[EffectRack1_EffectUnit1]", "group_[Channel1]_enable" 96 | c.input(c.noteOnIds 0x01, 0x02 + offset) 97 | .does "[EffectRack1_EffectUnit1]", "group_[Channel2]_enable" 98 | 99 | Per deck controls 100 | ----------------- 101 | 102 | We add the two decks with the `addDeck(idx)` function. In the 103 | *Xponent*, each MIDI message is repeated per-deck on a different 104 | channel. 105 | 106 | addDeck: (i) -> 107 | assert i in [0, 1, 2, 3] 108 | g = "[Channel#{i+1}]" 109 | bank = if i in [0, 1] then 0 else 1 110 | lr = if i in [0, 2] then 0 else 1 111 | offset = bank * 5 112 | ccId = (cc) -> c.ccIds cc, i + bank * 3 113 | outCcId = (cc) -> c.ccIds cc + lr, 3 + offset 114 | noteId = (note) -> c.noteIds note, i + bank * 3 115 | noteOnId = (note) -> c.noteOnIds note, i + bank * 3 116 | 117 | * **15.** Shift. It changes the behaviour of some controls. Note 118 | that there is a shift button per-deck, which only affects the 119 | controls of that deck. 120 | 121 | shift = b.modifier() 122 | c.control(noteId 0x2C).does shift 123 | 124 | * **12.** Pre-Fade Listen. Select which deck goes to the headphones. 125 | 126 | c.control(noteOnId 0x14).does @decks.add g, "pfl" 127 | 128 | ### The mixer 129 | 130 | 131 | * **20.** Filter and gain kills. 132 | 133 | c.control(noteId 0x08).does g, "filterLowKill" 134 | c.control(noteId 0x09).does g, "filterMidKill" 135 | c.control(noteId 0x0A).does g, "filterHighKill" 136 | c.control(noteId 0x0B).does g, "pregain_toggle" 137 | 138 | * **22.** Mixer EQ and gain. 139 | 140 | c.input(ccId 0x08).does g, "filterLow" 141 | c.input(ccId 0x09).does g, "filterMid" 142 | c.input(ccId 0x0A).does g, "filterHigh" 143 | c.input(ccId 0x0B).does b.soft g, "pregain" 144 | 145 | * **23.** Per deck volume meters. 146 | 147 | c.output(outCcId 0x12) 148 | .does b.mapOut(g, "VuMeter").meter() 149 | 150 | * **34.** Sync button. Like the button in the UI, it can be held 151 | pressed to enable the deck in the *master sync* group. 152 | 153 | c.control(noteId 0x02).does g, "sync_enabled" 154 | 155 | * **33.** Deck volume. 156 | 157 | c.input(ccId 0x07).does b.soft g, "volume" 158 | 159 | * **38.** Punch-in/transform. While pressed, lets this track be heard 160 | overriding the crossfader. 161 | 162 | c.control(noteId 0x07).does b.punchIn (0.5-i) 163 | 164 | ### The transport section 165 | 166 | * **29.** Song progress indication. When it approaches the end of the 167 | playing song it starts blinking. 168 | 169 | c.output(outCcId 0x14).does b.playhead g 170 | 171 | * **30.** Back and forward. 172 | 173 | c.control(noteId 0x21).does g, "back" 174 | c.control(noteId 0x22).does g, "fwd" 175 | 176 | * **31.** Includes several buttons... 177 | 178 | - The top buttons with numbers are the *hotcues*. On first press, 179 | sets the hotcue. On second press, jumps to hotcue. When *shift* is 180 | held, deletes the hotcue point. 181 | 182 | for idx in [0..4] 183 | c.control(noteId(0x17 + idx)) 184 | .when(shift, 185 | g, "hotcue_#{idx+1}_clear", 186 | g, "hotcue_#{idx+1}_enabled") 187 | .else g, "hotcue_#{idx+1}_activate", 188 | g, "hotcue_#{idx+1}_enabled" 189 | 190 | - The little arrow buttons do *beatjump* -- jump forward or back by 191 | 4 beats. When *shift* is pressed they jump by one beat. 192 | 193 | c.control(noteId 0x1C) 194 | .when shift, g, "beatjump_1_backward" 195 | .else g, "beatjump_4_backward" 196 | c.control(noteId 0x1D) 197 | .when shift, g, "beatjump_1_forward" 198 | .else g, "beatjump_4_forward" 199 | 200 | - The *lock* button does *key lock* -- i.e. makes tempo changes 201 | independent of pitch. When *shift* is pressed, it expands/collapses 202 | the selected browser item. 203 | 204 | c.control(noteId 0x1E) 205 | .when(shift, "[Playlist]", "ToggleSelectedSidebarItem") 206 | .else g, "keylock" 207 | 208 | - The *plus* (+) button moves the beat grid to align with the current 209 | play position. 210 | 211 | c.control(noteId 0x1F).does g, "beats_translate_curpos" 212 | 213 | - The *minus* (-) button plays the track in reverse. 214 | 215 | c.control(noteId 0x20).does g, "reverse" 216 | 217 | * **35.** Cue button. 218 | 219 | c.control(noteId 0x23).does g, "cue_default", g, "cue_indicator" 220 | 221 | * **37.** Play/pause button. 222 | 223 | c.control(noteOnId 0x24).does g, "play" 224 | 225 | 226 | ### The looping section 227 | 228 | * **36.** This includes several controls to manage loops... 229 | 230 | - The *in* and *out* buttons set the loop start and end to the 231 | current playing position. When *shift* is pressed, they halve and 232 | double the current loop size respectively. 233 | 234 | c.control(noteId 0x29) 235 | .when(shift, g, "loop_halve") 236 | .else g, "loop_in" 237 | c.control(noteId 0x2B) 238 | .when(shift, g, "loop_double") 239 | .else g, "loop_out" 240 | 241 | - The *loop* toggles the current loop on/off whenever there is a loop 242 | selected. 243 | 244 | c.control(noteOnId 0x2A) 245 | .does g, "reloop_exit", g, "loop_enabled" 246 | 247 | - The numbers set and trigger a loop of 4, 8, 16 and 32 beats 248 | respectively. When *shift*, they set loops of 1/8, 1/2, 1 or 2 249 | beats long. 250 | 251 | c.control(noteId 0x25) 252 | .when(shift, g, "beatloop_0.125_activate", 253 | g, "beatloop_0.125_enabled") 254 | .else g, "beatloop_4_activate", 255 | g, "beatloop_4_enabled" 256 | c.control(noteId 0x26) 257 | .when(shift, g, "beatloop_0.5_activate", 258 | g, "beatloop_0.5_enabled") 259 | .else g, "beatloop_8_activate", 260 | g, "beatloop_8_enabled" 261 | c.control(noteId 0x27) 262 | .when(shift, g, "beatloop_1_activate", 263 | g, "beatloop_1_enabled") 264 | .else g, "beatloop_16_activate", 265 | g, "beatloop_16_enabled" 266 | c.control(noteId 0x28) 267 | .when(shift, g, "beatloop_2_activate", 268 | g, "beatloop_2_enabled") 269 | .else g, "beatloop_32_activate", 270 | g, "beatloop_32_enabled" 271 | 272 | ### Effects 273 | 274 | * In the **24** group, the *first* and *second* buttons enable the 275 | first or second effect units for this deck. 276 | 277 | c.control(noteOnId 0x0c) 278 | .does "[EffectRack1_EffectUnit1]", "group_#{g}_enable" 279 | c.control(noteOnId 0x0d) 280 | .does "[EffectRack1_EffectUnit2]", "group_#{g}_enable" 281 | 282 | * The *third knob and button* in **24** and **25** enable a *beat 283 | roll* effect, similar to those of the looping section but with 284 | resolution controllable with a knob for more drastic effects, and 285 | the play position is restored when turned off. 286 | 287 | beatloop = b.beatEffect g, 'roll' 288 | c.input(ccId 0x0e).does beatloop.selector() 289 | c.control(noteId 0x0e).does beatloop 290 | 291 | * The *fourth knob and button* in **24** and **25** enable the quick 292 | effect knob -- by default mapped to a filter sweep. 293 | 294 | c.input(ccId 0x0f) 295 | .does "[QuickEffectRack1_#{g}]", 'super1' 296 | c.control(noteId 0x0f) 297 | .does "[QuickEffectRack1_#{g}]", 'enabled' 298 | 299 | ### The wheel and pitch section 300 | 301 | * **10.** Toggles *scratch* mode. 302 | 303 | scratchMode = b.switch() 304 | c.control(noteOnId 0x15).does scratchMode 305 | 306 | * **11.** The wheel does different functions... 307 | 308 | - When the deck is stopped, it moves the play position. 309 | 310 | - When *scratch* mode is on, it will stop the song when touched on 311 | top and control the track play like a vinyl when moved. 312 | 313 | - Otherwise, it can be used to *nudge* the playing speed up or down 314 | to sync the phase of tracks when the track is playing. 315 | 316 | - When *shift* is pressed, it will scroll through the current list 317 | of tracks in the browser. 318 | 319 | selectTrackKnobTransform = do -> 320 | toggle = 1 321 | (ev) -> 322 | val = ev.value - 64 323 | toggle -= 1 324 | if toggle < 0 then toggle = 3 325 | if toggle == 0 then val.sign() else null 326 | 327 | c.control(noteId 0x16) 328 | .when v.and(v.not(shift), scratchMode), b.scratchEnable i+1 329 | 330 | c.input(ccId 0x16) 331 | .when(shift, b.map("[Playlist]", "SelectTrackKnob") 332 | .transform selectTrackKnobTransform) 333 | .else.when(scratchMode, 334 | b.scratchTick(i+1).options.spread64) 335 | .else b.map(g, "jog").transform (ev) -> (ev.value - 64) / 8 336 | 337 | * **26.** Temporarily nudges the pitch down or up. When **shift**, 338 | they do it in a smaller amount. 339 | 340 | c.control(noteId 0x10) 341 | .when(shift, g, "rate_temp_down_small") 342 | .else g, "rate_temp_down" 343 | c.control(noteId 0x11) 344 | .when(shift, g, "rate_temp_up_small") 345 | .else g, "rate_temp_up" 346 | 347 | * **32.** Pitch slider, adjusts playing speed. 348 | 349 | c.input(c.pbIds i).does b.soft g, "rate" 350 | 351 | * **21.** Custom effects that include... 352 | 353 | - The *big cross* (X) button simulates a *brake* effect as if the 354 | turntable was turned off suddenly. On *shift*, it ejects the track 355 | from the deck. 356 | 357 | c.control(noteId 0x12) 358 | .when(shift, g, "eject") 359 | .else b.brake i+1 360 | 361 | - The *big minus* (--) button simulates a *backspin* effect as if the 362 | vinyl was launched backwards. On *shift*, it loads the selected 363 | track in the browser into the deck. 364 | 365 | c.control(noteId 0x13) 366 | .when(shift, g, "LoadSelectedTrack") 367 | .else b.spinback i+1 368 | 369 | Initialization 370 | -------------- 371 | 372 | Unlike old Mixxx versions, this script initializes the device properly 373 | for light feedback. The trick of holding the two and key button on 374 | initialization are no longer required. 375 | 376 | preinit: -> 377 | msg = [0xF0, 0x00, 0x20, 0x08, 0x00, 0x00, 0x63, 0x0E, 378 | 0x16, 0x40, 0x00, 0x01, 0xF7] 379 | @mixxx.midi.sendSysexMsg msg, msg.length 380 | 381 | init: -> 382 | @decks.activate 0 383 | 384 | postshutdown: -> 385 | msg = [0xF0, 0x00, 0x20, 0x08, 0x00, 0x00, 0x63, 0x0E, 386 | 0x16, 0x40, 0x00, 0x00, 0xF7] 387 | @mixxx.midi.sendSysexMsg msg, msg.length 388 | 389 | License 390 | ------- 391 | 392 | > Copyright (C) 2013 Juan Pedro Bolívar Puente 393 | > 394 | > This program is free software: you can redistribute it and/or 395 | > modify it under the terms of the GNU General Public License as 396 | > published by the Free Software Foundation, either version 3 of the 397 | > License, or (at your option) any later version. 398 | > 399 | > This program is distributed in the hope that it will be useful, 400 | > but WITHOUT ANY WARRANTY; without even the implied warranty of 401 | > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 402 | > GNU General Public License for more details. 403 | > 404 | > You should have received a copy of the GNU General Public License 405 | > along with this program. If not, see . 406 | -------------------------------------------------------------------------------- /docco/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Typography ----------------------------*/ 2 | 3 | @font-face { 4 | font-family: 'aller-light'; 5 | src: url('public/fonts/aller-light.eot'); 6 | src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'), 7 | url('public/fonts/aller-light.woff') format('woff'), 8 | url('public/fonts/aller-light.ttf') format('truetype'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: 'aller-bold'; 15 | src: url('public/fonts/aller-bold.eot'); 16 | src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'), 17 | url('public/fonts/aller-bold.woff') format('woff'), 18 | url('public/fonts/aller-bold.ttf') format('truetype'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: 'novecento-bold'; 25 | src: url('public/fonts/novecento-bold.eot'); 26 | src: url('public/fonts/novecento-bold.eot?#iefix') format('embedded-opentype'), 27 | url('public/fonts/novecento-bold.woff') format('woff'), 28 | url('public/fonts/novecento-bold.ttf') format('truetype'); 29 | font-weight: normal; 30 | font-style: normal; 31 | } 32 | 33 | /*--------------------- Layout ----------------------------*/ 34 | html { height: 100%; } 35 | body { 36 | font-family: "aller-light"; 37 | font-size: 14px; 38 | line-height: 18px; 39 | color: #333; 40 | margin: 0; padding: 0; 41 | height:100%; 42 | } 43 | #container { min-height: 100%; } 44 | 45 | a { 46 | color: #000; 47 | } 48 | 49 | b, strong { 50 | font-weight: normal; 51 | font-family: "aller-bold"; 52 | } 53 | 54 | p, ul, ol { 55 | margin: 15px 0 0px; 56 | } 57 | 58 | h1, h2 { 59 | margin: 30px 0 15px 0; 60 | } 61 | 62 | h3, h4, h5, h6 { 63 | margin: 15px 0 15px 0; 64 | } 65 | 66 | 67 | h1, h2, h3 { 68 | text-transform: uppercase; 69 | font-family: "novecento-bold"; 70 | font-weight: normal; 71 | } 72 | 73 | h4, h5, h6 { 74 | font-weight: bold; 75 | font-size: 120%; 76 | } 77 | 78 | h1, h2, h3, h4, h5, h6 { 79 | color: #000; 80 | line-height: 1em; 81 | } 82 | 83 | h1 { 84 | margin-top: 60px; 85 | border-bottom: 6px solid #000; 86 | font-size: 250%; 87 | width: 120%; 88 | } 89 | 90 | h2 { font-size: 175%; } 91 | 92 | 93 | hr { 94 | border: 0; 95 | background: 1px solid #ddd; 96 | height: 1px; 97 | margin: 20px 0; 98 | } 99 | 100 | pre, tt, code { 101 | font-size: 12px; line-height: 16px; 102 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 103 | margin: 0; padding: 0; color: #aaa; 104 | } 105 | 106 | .annotation pre { 107 | display: block; 108 | margin: 0; 109 | padding: 7px 10px; 110 | overflow-x: auto; 111 | } 112 | 113 | .annotation pre code { 114 | border: 0; 115 | padding: 0; 116 | background: transparent; 117 | } 118 | 119 | 120 | blockquote { 121 | border-left: 5px solid #ccc; 122 | margin: 10px 0 10px; 123 | padding: 1px 0 1px 1em; 124 | } 125 | .sections blockquote p { 126 | font-family: Menlo, Consolas, Monaco, monospace; 127 | font-size: 12px; line-height: 16px; 128 | color: #999; 129 | margin: 10px 0 10px; 130 | white-space: pre-wrap; 131 | } 132 | 133 | ul.sections { 134 | list-style: none; 135 | padding:0 0 5px 0;; 136 | margin:0; 137 | } 138 | 139 | /* 140 | Force border-box so that % widths fit the parent 141 | container without overlap because of margin/padding. 142 | 143 | More Info : http://www.quirksmode.org/css/box.html 144 | */ 145 | ul.sections > li > div { 146 | -moz-box-sizing: border-box; /* firefox */ 147 | -ms-box-sizing: border-box; /* ie */ 148 | -webkit-box-sizing: border-box; /* webkit */ 149 | -khtml-box-sizing: border-box; /* konqueror */ 150 | box-sizing: border-box; /* css3 */ 151 | } 152 | 153 | 154 | /*---------------------- Jump Page -----------------------------*/ 155 | #jump_to, #jump_page { 156 | margin: 0; 157 | background: white; 158 | font: 16px Arial; 159 | cursor: pointer; 160 | text-align: right; 161 | list-style: none; 162 | } 163 | 164 | #jump_to a { 165 | text-decoration: none; 166 | } 167 | 168 | #jump_to a.large { 169 | display: none; 170 | } 171 | #jump_to a.small { 172 | font-size: 22px; 173 | font-weight: bold; 174 | color: #676767; 175 | } 176 | 177 | #jump_to, #jump_wrapper { 178 | position: fixed; 179 | right: 0; top: 0; 180 | padding: 10px 15px; 181 | margin:0; 182 | } 183 | 184 | #jump_wrapper { 185 | display: none; 186 | padding:0; 187 | } 188 | 189 | #jump_to:hover #jump_wrapper { 190 | display: block; 191 | } 192 | 193 | #jump_page { 194 | padding: 5px 0 3px; 195 | margin: 0 0 25px 25px; 196 | } 197 | 198 | #jump_page .source { 199 | display: block; 200 | padding: 15px; 201 | text-decoration: none; 202 | border-top: 1px solid #eee; 203 | } 204 | 205 | #jump_page .source:hover { 206 | background: #f5f5ff; 207 | } 208 | 209 | #jump_page .source:first-child { 210 | } 211 | 212 | /*---------------------- Low resolutions (> 320px) ---------------------*/ 213 | 214 | #container { 215 | overflow-x: hidden; 216 | } 217 | 218 | .pilwrap { display: none; } 219 | 220 | ul.sections > li > div { 221 | display: block; 222 | padding:5px 10px 0 10px; 223 | } 224 | 225 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 226 | padding-left: 30px; 227 | } 228 | 229 | ul.sections > li > div.content { 230 | background: #000; 231 | overflow-x:auto; 232 | border: 0; 233 | margin:5px 10px 5px 10px; 234 | padding-bottom: 5px; 235 | } 236 | 237 | ul.sections.hide-code > li > div.content { 238 | width: 0; 239 | height: 0; 240 | display: none; 241 | } 242 | 243 | ul.sections > li > div.annotation pre { 244 | margin: 7px 0 7px; 245 | padding-left: 15px; 246 | } 247 | 248 | ul.sections > li > div.annotation p tt, .annotation code { 249 | background: #f8f8ff; 250 | border: 0; 251 | font-size: 12px; 252 | padding: 0 0.2em; 253 | color: #000; 254 | } 255 | 256 | 257 | /*---------------------- (> 481px) ---------------------*/ 258 | 259 | @media only screen and (min-width: 681px) { 260 | #container { 261 | position: relative; 262 | overflow-x: inherit; 263 | } 264 | body { 265 | background-color: #000; 266 | font-size: 15px; 267 | line-height: 21px; 268 | } 269 | pre, tt, code { 270 | line-height: 18px; 271 | } 272 | p, ul, ol { 273 | margin: 0 0 15px; 274 | } 275 | 276 | #jump_to { 277 | padding: 5px 10px; 278 | } 279 | #jump_wrapper { 280 | padding: 0; 281 | } 282 | #jump_to, #jump_page { 283 | font: 10px Arial; 284 | text-transform: uppercase; 285 | } 286 | #jump_page .source { 287 | padding: 5px 10px; 288 | } 289 | #jump_to a.large { 290 | display: inline-block; 291 | } 292 | #jump_to a.small { 293 | display: none; 294 | } 295 | 296 | #background { 297 | position: absolute; 298 | top: 0; bottom: 0; 299 | width: 350px; 300 | background: #fff; 301 | border-right: 0px solid #000; 302 | z-index: -1; 303 | } 304 | 305 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 306 | padding-left: 40px; 307 | } 308 | 309 | ul.sections > li { 310 | white-space: nowrap; 311 | } 312 | 313 | ul.sections > li > div { 314 | display: inline-block; 315 | } 316 | 317 | ul.sections > li > div.annotation { 318 | border-top: 3px dashed #ddd; 319 | max-width: 350px; 320 | min-width: 350px; 321 | min-height: 5px; 322 | padding: 13px; 323 | overflow-x: hidden; 324 | white-space: normal; 325 | vertical-align: top; 326 | text-align: left; 327 | } 328 | ul.sections.hide-code > li > div.annotation { 329 | border-top: 0; 330 | } 331 | ul.sections > li > div.annotation.header, 332 | ul.sections > li#section-1 > div.annotation, 333 | ul.sections > li#section-2 > div.annotation { 334 | border-top: 0; 335 | } 336 | 337 | ul.sections > li > div.annotation pre { 338 | margin: 15px 0 15px; 339 | padding-left: 15px; 340 | } 341 | 342 | ul.sections > li > div.content { 343 | border-top: 3px dashed #222; 344 | padding: 0px; 345 | min-width: calc(100% - 640px); 346 | margin: 0; 347 | vertical-align: top; 348 | background: #000; 349 | } 350 | ul.sections.hide-code > li > div.content { 351 | border-top: 0; 352 | height: 0; 353 | width: 0; 354 | display: none; 355 | } 356 | ul.sections > li#section-1 > div.content, 357 | ul.sections > li#section-2 > div.content { 358 | border-top: 0; 359 | } 360 | 361 | .pilwrap { 362 | position: relative; 363 | display: inline; 364 | } 365 | 366 | .pilcrow { 367 | font: 12px Arial; 368 | text-decoration: none; 369 | color: #454545; 370 | position: absolute; 371 | top: 3px; left: -20px; 372 | padding: 1px 2px; 373 | opacity: 0; 374 | -webkit-transition: opacity 0.2s linear; 375 | } 376 | .for-h1 .pilcrow { 377 | top: 47px; 378 | } 379 | .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { 380 | top: 35px; 381 | } 382 | 383 | ul.sections > li > div.annotation:hover .pilcrow { 384 | opacity: 1; 385 | } 386 | 387 | body { 388 | font-size: 16px; 389 | line-height: 24px; 390 | } 391 | 392 | #background { 393 | width: 625px; 394 | } 395 | 396 | ul.sections > li > div.annotation { 397 | max-width: 625px; 398 | min-width: 625px; 399 | padding: 10px 25px 1px 50px; 400 | } 401 | 402 | ul.sections > li > div.content { 403 | padding: 9px 15px 16px 25px; 404 | } 405 | 406 | } 407 | 408 | /*---------------------- Syntax Highlighting -----------------------------*/ 409 | 410 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 411 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 412 | /* 413 | 414 | github.com style (c) Vasily Polovnyov 415 | 416 | */ 417 | 418 | pre code { 419 | display: block; padding: 0.5em; 420 | color: #ddd; 421 | background: #000 422 | } 423 | 424 | pre .comment, 425 | pre .template_comment, 426 | pre .diff .header, 427 | pre .javadoc { 428 | color: #408080; 429 | font-style: italic 430 | } 431 | 432 | pre .keyword, 433 | pre .assignment, 434 | pre .literal, 435 | pre .css .rule .keyword, 436 | pre .winutils, 437 | pre .javascript .title, 438 | pre .lisp .title, 439 | pre .subst { 440 | color: #C57151; 441 | /*font-weight: bold*/ 442 | } 443 | 444 | pre .number, 445 | pre .hexcolor { 446 | color: #40a070 447 | } 448 | 449 | pre .string, 450 | pre .tag .value, 451 | pre .phpdoc, 452 | pre .tex .formula { 453 | color: #219161; 454 | } 455 | 456 | pre .title, 457 | pre .id { 458 | color: #4986CD; 459 | } 460 | pre .params { 461 | color: #00F; 462 | } 463 | 464 | pre .javascript .title, 465 | pre .lisp .title, 466 | pre .subst { 467 | font-weight: normal 468 | } 469 | 470 | pre .class .title, 471 | pre .haskell .label, 472 | pre .tex .command { 473 | color: #4986CD; 474 | font-weight: bold 475 | } 476 | 477 | pre .tag, 478 | pre .tag .title, 479 | pre .rules .property, 480 | pre .django .tag .keyword { 481 | color: #000080; 482 | font-weight: normal 483 | } 484 | 485 | pre .attribute, 486 | pre .variable, 487 | pre .instancevar, 488 | pre .lisp .body { 489 | color: #ccc 490 | } 491 | 492 | pre .regexp { 493 | color: #B68 494 | } 495 | 496 | pre .class { 497 | color: #ccc; 498 | font-weight: bold 499 | } 500 | 501 | pre .symbol, 502 | pre .ruby .symbol .string, 503 | pre .ruby .symbol .keyword, 504 | pre .ruby .symbol .keymethods, 505 | pre .lisp .keyword, 506 | pre .tex .special, 507 | pre .input_number { 508 | color: #990073 509 | } 510 | 511 | pre .builtin, 512 | pre .constructor, 513 | pre .built_in, 514 | pre .lisp .title { 515 | color: #0086b3 516 | } 517 | 518 | pre .preprocessor, 519 | pre .pi, 520 | pre .doctype, 521 | pre .shebang, 522 | pre .cdata { 523 | color: #999; 524 | font-weight: bold 525 | } 526 | 527 | pre .deletion { 528 | background: #300 529 | } 530 | 531 | pre .addition { 532 | background: #030 533 | } 534 | 535 | pre .diff .change { 536 | background: #003 537 | } 538 | 539 | pre .chunk { 540 | color: #aaa 541 | } 542 | 543 | pre .tex .formula { 544 | opacity: 0.5; 545 | } 546 | 547 | /* 548 | 549 | Railscasts-like style (c) Visoft, Inc. (Damien White) 550 | 551 | */ 552 | 553 | .hljs { 554 | display: block; 555 | overflow-x: auto; 556 | padding: 0.5em; 557 | background: #232323; 558 | color: #e6e1dc; 559 | -webkit-text-size-adjust: none; 560 | } 561 | 562 | .hljs-comment, 563 | .hljs-javadoc, 564 | .hljs-shebang { 565 | color: #bc9458; 566 | font-style: italic; 567 | } 568 | 569 | .hljs-keyword, 570 | .ruby .hljs-function .hljs-keyword, 571 | .hljs-request, 572 | .hljs-status, 573 | .nginx .hljs-title, 574 | .method, 575 | .hljs-list .hljs-title { 576 | color: #c26230; 577 | } 578 | 579 | .hljs-string, 580 | .hljs-number, 581 | .hljs-regexp, 582 | .hljs-tag .hljs-value, 583 | .hljs-cdata, 584 | .hljs-filter .hljs-argument, 585 | .hljs-attr_selector, 586 | .apache .hljs-cbracket, 587 | .hljs-date, 588 | .tex .hljs-command, 589 | .asciidoc .hljs-link_label, 590 | .markdown .hljs-link_label { 591 | color: #a5c261; 592 | } 593 | 594 | .hljs-subst { 595 | color: #519f50; 596 | } 597 | 598 | .hljs-tag, 599 | .hljs-tag .hljs-keyword, 600 | .hljs-tag .hljs-title, 601 | .hljs-doctype, 602 | .hljs-sub .hljs-identifier, 603 | .hljs-pi, 604 | .input_number { 605 | color: #e8bf6a; 606 | } 607 | 608 | .hljs-identifier { 609 | color: #d0d0ff; 610 | } 611 | 612 | .hljs-class .hljs-title, 613 | .hljs-type, 614 | .smalltalk .hljs-class, 615 | .hljs-javadoctag, 616 | .hljs-yardoctag, 617 | .hljs-phpdoc, 618 | .hljs-dartdoc { 619 | text-decoration: none; 620 | } 621 | 622 | .hljs-constant, 623 | .hljs-name { 624 | color: #da4939; 625 | } 626 | 627 | 628 | .hljs-symbol, 629 | .hljs-built_in, 630 | .ruby .hljs-symbol .hljs-string, 631 | .ruby .hljs-symbol .hljs-identifier, 632 | .asciidoc .hljs-link_url, 633 | .markdown .hljs-link_url, 634 | .hljs-attribute { 635 | color: #6d9cbe; 636 | } 637 | 638 | .asciidoc .hljs-link_url, 639 | .markdown .hljs-link_url { 640 | text-decoration: underline; 641 | } 642 | 643 | 644 | 645 | .hljs-params, 646 | .hljs-variable, 647 | .clojure .hljs-attribute { 648 | color: #d0d0ff; 649 | } 650 | 651 | .css .hljs-tag, 652 | .hljs-rule .hljs-property, 653 | .hljs-pseudo, 654 | .tex .hljs-special { 655 | color: #cda869; 656 | } 657 | 658 | .css .hljs-class { 659 | color: #9b703f; 660 | } 661 | 662 | .hljs-rule .hljs-keyword { 663 | color: #c5af75; 664 | } 665 | 666 | .hljs-rule .hljs-value { 667 | color: #cf6a4c; 668 | } 669 | 670 | .css .hljs-id { 671 | color: #8b98ab; 672 | } 673 | 674 | .hljs-annotation, 675 | .apache .hljs-sqbracket, 676 | .nginx .hljs-built_in { 677 | color: #9b859d; 678 | } 679 | 680 | .hljs-preprocessor, 681 | .hljs-preprocessor *, 682 | .hljs-pragma { 683 | color: #8996a8 !important; 684 | } 685 | 686 | .hljs-hexcolor, 687 | .css .hljs-value .hljs-number { 688 | color: #a5c261; 689 | } 690 | 691 | .hljs-title, 692 | .hljs-decorator, 693 | .css .hljs-function { 694 | color: #ffc66d; 695 | } 696 | 697 | .diff .hljs-header, 698 | .hljs-chunk { 699 | background-color: #2f33ab; 700 | color: #e6e1dc; 701 | display: inline-block; 702 | width: 100%; 703 | } 704 | 705 | .diff .hljs-change { 706 | background-color: #4a410d; 707 | color: #f8f8f8; 708 | display: inline-block; 709 | width: 100%; 710 | } 711 | 712 | .hljs-addition { 713 | background-color: #144212; 714 | color: #e6e1dc; 715 | display: inline-block; 716 | width: 100%; 717 | } 718 | 719 | .hljs-deletion { 720 | background-color: #600; 721 | color: #e6e1dc; 722 | display: inline-block; 723 | width: 100%; 724 | } 725 | 726 | .coffeescript .javascript, 727 | .javascript .xml, 728 | .tex .hljs-formula, 729 | .xml .javascript, 730 | .xml .vbscript, 731 | .xml .css, 732 | .xml .hljs-cdata { 733 | opacity: 0.7; 734 | } 735 | -------------------------------------------------------------------------------- /script/novation_twitch.mixco.js: -------------------------------------------------------------------------------- 1 | // script.twitch 2 | // ============= 3 | // 4 | // > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | // > - **View me [on a static web](http://sinusoid.es/mixco/script/novation_twitch.mixco.html)** 6 | // > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/script/novation_twitch.mixco.litcoffee)** 7 | // 8 | // Mixxx script file for the **Novation Twitch** controller. 9 | // 10 | // This script serves as **tutorial** for creating scripts using the 11 | // *Mixco* framework, but programming directly in JavaScript. Still, 12 | // we recommend you to try CoffeeScript, since it is a bit of a nicer 13 | // language. 14 | // 15 | // If you want to modify this script, you may want to read the 16 | // [Novation Twitch Programmer Guide](https://us.novationmusic.com/support/downloads/twitch-programmers-reference-guide) 17 | // 18 | // ### Note for Linux Users 19 | // 20 | // The Linux Kernel version 3.10 is required to get Novation Twitch 21 | // detected as soundcard or MIDI device. 22 | // 23 | // ![Novation Twitch Layout](http://sinusoid.es/mixco/pic/novation_twitch.png) 24 | // 25 | // Dependencies 26 | // ------------ 27 | // 28 | // First, we have to import the modules from the framework. We use 29 | // that the *NodeJS* `require` function. Note that all other NodeJS 30 | // modules are usable too when writing your script with the *Mixco* 31 | // framework. 32 | 33 | var _ = require('underscore') 34 | var mixco = require('mixco') 35 | var c = mixco.control 36 | var b = mixco.behaviour 37 | var v = mixco.value 38 | 39 | // The script 40 | // ---------- 41 | // 42 | // When writing a controller script we use the `script.register` 43 | // function to generate and install a script instance in the current 44 | // module. The first parameter is the current module as defined by 45 | // *NodeJS*, the second parameter is the JavaScript object with all 46 | // the functions and information about our script. 47 | 48 | mixco.script.register(module, { 49 | 50 | // ### Metadata 51 | // 52 | // Then the `info` object contains the meta-data that is displayed 53 | // to the user in the MIDI mapping chooser of Mixxx. 54 | 55 | info: { 56 | name: "[mixco] Novation Twitch", 57 | author: "Juan Pedro Bolivar Puente ", 58 | forums: 'https://github.com/arximboldi/mixco/issues', 59 | wiki: 'https://sinusoid.es/mixco/script/korg_nanokontrol2.mixco.html', 60 | description: "Controller mapping for Novation Twitch (in basic mode).", 61 | }, 62 | 63 | // ### Constructor 64 | // 65 | // The constructor contains the definition of the MIDI mapping. 66 | // Here we create all the different control objects and add them 67 | // to the script instance. 68 | 69 | constructor: function () { 70 | 71 | // #### Master section 72 | // 73 | // Many of the master controls of the that the *headphone 74 | // volume*, *headphone mix*, *booth volume* and *master volume* 75 | // knobs are handled directly by the integrated soundcard of 76 | // the controller. We map the rest here. 77 | // 78 | // * *Crossfader* slider. 79 | 80 | c.input(0x08, 0x07).does("[Master]", "crossfader") 81 | 82 | // #### Mic/aux and effects 83 | // 84 | // Sadly, the buttons *Aux*, *Deck A*, *Deck B* and *PFL* 85 | // of the effects and microphone sections are controlled by 86 | // the hardware in a bit of a useless way, so they do nothing 87 | // -- other than light up when they are pressed. 88 | 89 | var ccIdFxBanks = function (index) { 90 | banks = 5 91 | params = 4 92 | ids = [] 93 | for (var j = 0; j < banks; ++j) 94 | ids.push.apply(ids, c.ccIds(index + params * j, 0xB)) 95 | return ids 96 | } 97 | 98 | // * Microphone *volume* control and *on/off* button. 99 | 100 | c.input(ccIdFxBanks(0x3)).does(b.soft("[Microphone]", "pregain")) 101 | c.control(c.noteIds(0x23, 0xB)).does("[Microphone]", "talkover") 102 | 103 | // * The the *Depth* and *Mix* knobs in the *Master FX* 104 | // section are mapped to *mix* and *super* of the first 105 | // effect unit. 106 | 107 | c.input(ccIdFxBanks(0x0)) 108 | .does("[EffectRack1_EffectUnit1]", "mix") 109 | c.input(ccIdFxBanks(0x1)) 110 | .option(scaledDiff(1/2)) 111 | .does("[EffectRack1_EffectUnit1]", "super1") 112 | 113 | // * The *beats* knob can be used to change the selected effect. 114 | 115 | c.input(ccIdFxBanks(0x2)) 116 | .does("[EffectRack1_EffectUnit1]", "chain_selector") 117 | 118 | // * The *on/off* button of the FX section completely toggles 119 | // the first effect unit. 120 | 121 | c.control(c.noteIds(0x22, 0xB)) 122 | .does("[EffectRack1_EffectUnit1]", "enabled") 123 | 124 | // #### Browse 125 | // 126 | // * The *back* and *fwd* can be used to scroll the sidebar. 127 | 128 | c.input(c.noteIds(0x54, 0x7)).does( 129 | "[Playlist]", "SelectPrevPlaylist") 130 | c.input(c.noteIds(0x56, 0x7)).does( 131 | "[Playlist]", "SelectNextPlaylist") 132 | 133 | // * The *scroll* encoder scrolls the current view. When 134 | // pressed it moves faster. 135 | 136 | scrollFaster = b.modifier() 137 | 138 | c.input(c.noteIds(0x55, 0x7)).does(scrollFaster) 139 | c.input(0x55, 0x7) 140 | .when (scrollFaster, 141 | b.map("[Playlist]", "SelectTrackKnob") 142 | .option(scaledSelectKnob(8))) 143 | .else_(b.map("[Playlist]", "SelectTrackKnob") 144 | .options.selectknob) 145 | 146 | // * The *area* toggles the maximized library. 147 | 148 | c.control(c.noteIds(0x50, 0x7)).does( 149 | "[Master]", "maximize_library") 150 | 151 | // * The *view* button in the *browser* section lets you tap 152 | // the tempo for the track that is currently on PFL. 153 | 154 | this.viewButton = c.control(c.noteIds(0x51, 0x7)) 155 | 156 | // ### Per deck controls 157 | // 158 | // We use a `behaviou.chooser` for the PFL selection. This 159 | // will make sure that only one deck's PFL is selected at 160 | // a time for greater convenience. Then, we define a `addDeck` 161 | // function that will add the actual controls for each of the 162 | // decks. 163 | 164 | this.decks = b.chooser() 165 | this.addDeck(0) 166 | this.addDeck(1) 167 | 168 | }, 169 | 170 | addDeck: function (i) { 171 | var g = "[Channel" + (i+1) + "]" 172 | var ccId = function (cc) { return c.ccIds(cc, 0x07+i) } 173 | var ccIdShift = function (cc) { return c.ccIds(cc, 0x09+i) } 174 | var ccIdAll = function (cc) { return _.union(ccId(cc), 175 | ccIdShift(cc)) } 176 | var noteId = function (note) { return c.noteIds(note, 0x07+i) } 177 | var noteIdShift = function (note) { return c.noteIds(note, 0x09+i) } 178 | var noteIdAll = function (cc) { return _.union(noteId(cc), 179 | noteIdShift(cc)) } 180 | 181 | // #### Mixer section 182 | // 183 | // * PFL deck selection. 184 | 185 | c.control(noteIdAll(0x0A)).does(this.decks.add(g, "pfl")) 186 | 187 | this.viewButton.when(this.decks.activator(i), 188 | g, "bpm_tap", g, "beat_active") 189 | 190 | // * *Volume* fader and *low*, *mid*, *high* and *trim* knobs. 191 | // *Trim* is the deck *gain* knob in Mixxx. 192 | 193 | c.input(ccIdAll(0x07)).does(g, "volume") 194 | c.input(ccIdAll(0x46)).does(g, "filterLow") 195 | c.input(ccIdAll(0x47)).does(g, "filterMid") 196 | c.input(ccIdAll(0x48)).does(g, "filterHigh") 197 | c.input(ccIdAll(0x09)).does(g, "pregain") 198 | 199 | // * *Volume* meters for each channel. 200 | 201 | c.output(noteIdAll(0x5f)).does(b.mapOut(g, "VuMeter").meter()) 202 | 203 | // * The **fader FX** we use to control the quick filter. The 204 | // **on/off** button below can be used to toggle it. 205 | // Likewise, pressing the knob momentarily toggles it. 206 | 207 | c.input(ccIdAll(0x06)) 208 | .option(scaledDiff(1/2)) 209 | .does("[QuickEffectRack1_"+g+"]", 'super1') 210 | c.control(noteIdAll(0x06)) 211 | .does("[QuickEffectRack1_"+g+"]", 'enabled') 212 | c.control(noteIdAll(0x0D)) 213 | .does("[QuickEffectRack1_"+g+"]", 'enabled') 214 | 215 | // #### Effects 216 | // 217 | // * In the *Master FX* section, the *FX Select* left and 218 | // right enable the first effect unit on the deck in the 219 | // direction of the arrow. 220 | 221 | c.control(c.noteIds(0x20+i, 0xB)) 222 | .does("[EffectRack1_EffectUnit1]", "group_"+g+"_enable") 223 | 224 | // #### Browse 225 | // 226 | // * The *load A* or *load B* buttons load the selected track 227 | // to the given deck. 228 | 229 | c.control(c.noteIds(0x52+i, 0x7)).does(g, "LoadSelectedTrack") 230 | 231 | // #### Deck transport 232 | // 233 | // * The *play* and *cue* buttons work as expected. On 234 | // *shift*, the cue button does a reverse effect. 235 | 236 | var redLed = 0x00 237 | var amberLed = 0x40 238 | var greenLed = 0x70 239 | var pad = function (ids, color) { 240 | return c.control(ids).states({ 241 | on: color + 0xf, 242 | off: color + 0x1 243 | }) 244 | } 245 | 246 | pad(noteIdAll(0x17), greenLed).does(g, "play") 247 | pad(noteId(0x16), redLed).does(g, "cue_default", g, "cue_indicator") 248 | pad(noteIdShift(0x16), amberLed).does(g, "reverse") 249 | 250 | // * The *keylock* button toggles the pitch-independent time 251 | // stretching. On *shift*, it toggles *slip mode*, in which 252 | // loops and scratching continue playback on the background 253 | // thus returning the playhead to where the track would have 254 | // been. 255 | 256 | slipMode = b.switch_() 257 | c.control(noteId(0x12)).does(g, "keylock") 258 | c.control(noteIdShift(0x12)).does(slipMode) 259 | 260 | // * The *sync* button. Like the button in the UI, it can be 261 | // held pressed to enable the deck in the *master sync* 262 | // group. 263 | 264 | c.control(noteIdAll(0x13)).does(g, "sync_enabled") 265 | 266 | // #### Beat grid 267 | // 268 | // * The *adjust* button *aligns the beatgrid* to the current 269 | // play position. 270 | 271 | c.control(noteIdAll(0x11)).does(g, "beats_translate_curpos") 272 | 273 | // * The *set* button toggles loop and hot-cue *quantization* 274 | // on or off. 275 | 276 | c.control(noteIdAll(0x10)).does(g, "quantize") 277 | 278 | // #### Pitch and transport bar 279 | // 280 | // * The *pitch* encoder moves the pitch slider up and 281 | // down. When it is pressed, it moves it pitch faster. 282 | 283 | var coarseRateFactor = 1/10 284 | var coarseRateOn = b.modifier() 285 | 286 | c.input(noteIdAll(0x03)).does(coarseRateOn) 287 | c.input(ccIdAll(0x03)) 288 | .when (coarseRateOn, 289 | b.map(g, "rate").option(scaledDiff(2))) 290 | .else_(b.map(g, "rate").option(scaledDiff(1/12))) 291 | 292 | // * In *drop* mode, the touch strip scrolls through the song. 293 | 294 | c.input(ccId(0x34)).does(g, "playposition") 295 | 296 | // * In *swipe* mode, the touch strip nudges the pitch up and 297 | // down. When *shift* is held it simulates scratching. 298 | 299 | c.input(ccId(0x35)).does(g, "jog") 300 | .option(scaledSelectKnob(-1/3)) 301 | c.input(ccIdShift(0x35)).does(b.scratchTick(i+1)) 302 | .options.selectknob, 303 | c.input(noteIdShift(0x47)) 304 | .does(b.scratchEnable(i+1, 128)) 305 | .when(slipMode, b.map(g, "slip_enabled").options.switch_) 306 | 307 | // #### Performance modes 308 | // 309 | // ##### Hot cues 310 | // 311 | // * In *hot-cues* mode, the performance buttons control the 312 | // hot cues. One may *clear* hot-cues with *shift*. 313 | 314 | for (var j = 0; j < 8; ++j) { 315 | pad(noteId(0x60+j), amberLed).does( 316 | g, "hotcue_" + (j+1) + "_activate", 317 | g, "hotcue_" + (j+1) + "_enabled") 318 | pad(noteIdShift(0x60+j), amberLed).does( 319 | g, "hotcue_" + (j+1) + "_clear", 320 | g, "hotcue_" + (j+1) + "_enabled") 321 | } 322 | 323 | // ##### Slicer 324 | // 325 | // There is no functionality like a *slicer* in Mixxx, but we 326 | // reuse these pads for various purposes in this mode. 327 | // 328 | // * The buttons *1 to 4* trigger the first four samplers. 329 | // The sample plays as long as the button is held. 330 | 331 | for (var j = 0; j < 4; ++j) 332 | pad(noteIdAll(0x68+j), redLed).does( 333 | "[Sampler" + (j+1) + "]", "cue_preview") 334 | 335 | // * The buttons *5 and 6* trigger a *spinback* and *brake* 336 | // effect respectively. 337 | 338 | pad(noteIdAll(0x6C), greenLed).does(b.spinback(i+1)) 339 | pad(noteIdAll(0x6D), greenLed).does(b.brake(i+1)) 340 | 341 | // * The buttons *7 and 8* perform a stutter effect at 342 | // different speeds. 343 | 344 | pad(noteIdAll(0x6E), amberLed).does(b.stutter(g, 1/8)) 345 | pad(noteIdAll(0x6F), amberLed).does(b.stutter(g, 1/4)) 346 | 347 | // ##### Auto loop 348 | // 349 | // * In *auto-loop* mode, the pads select *loops* of sizes 350 | // 0.5, 1, 2, 4, 8, 16, 32 or 64, beats (starting at the 351 | // top-left pad). On *shift*, it creates loops of sizes 352 | // 1/32, 1/16, 1/8, 1/4, 1/2, 1, 2, or 4 beats. 353 | 354 | loopSize = [ "0.03125", "0.0625", "0.125", "0.25", 355 | "0.5", "1", "2", "4", 356 | "8", "16", "32", "64" ] 357 | for (var j = 0; j < 8; ++j) 358 | pad(noteId(0x70+j), greenLed).does( 359 | g, "beatloop_" + loopSize[4+j] + "_toggle", 360 | g, "beatloop_" + loopSize[4+j] + "_enabled") 361 | for (var j = 0; j < 8; ++j) 362 | pad(noteIdShift(0x70+j), greenLed).does( 363 | g, "beatloop_" + loopSize[j] + "_toggle", 364 | g, "beatloop_" + loopSize[j] + "_enabled") 365 | 366 | // ##### Loop roll 367 | // 368 | // * In *loop-roll* mode, momentarily creates a loop and, on 369 | // release returns the playhead to where it would have been 370 | // without looping. Loop sizes are 1/32, 1/16, 1/8, 1/4, 371 | // 1/2, 1, 2, or 4 beats (starting at the top-left pad). On 372 | // *shift*, it is 0.5, 1, 2, 4, 8, 16, 32 or 64 beats. 373 | 374 | loopSize = [ "0.03125", "0.0625", "0.125", "0.25", 375 | "0.5", "1", "2", "4", 376 | "8", "16", "32", "64" ] 377 | for (var j = 0; j < 8; ++j) 378 | pad(noteId(0x78+j), greenLed).does( 379 | g, "beatlooproll_" + loopSize[j] + "_activate", 380 | g, "beatloop_" + loopSize[j] + "_enabled") 381 | for (var j = 0; j < 8; ++j) 382 | pad(noteIdShift(0x78+j), greenLed).does( 383 | g, "beatlooproll_" + loopSize[4+j] + "_activate", 384 | g, "beatloop_" + loopSize[4+j] + "_enabled") 385 | 386 | }, 387 | 388 | // ### Initialization 389 | // 390 | // The `preinit` function is called before the MIDI controls are 391 | // initialized. We are going to set the device in *basic mode*, 392 | // as mentioned in the manual. This means that mode management is 393 | // done by the device -- this will simplify the script and let 394 | // have direct lower latency mappings more often. 395 | 396 | preinit: function () { 397 | this.mixxx.midi.sendShortMsg(0xb7, 0x00, 0x6f) 398 | this.mixxx.midi.sendShortMsg(0xb7, 0x00, 0x00) 399 | }, 400 | 401 | init: function () { 402 | this.decks.activate(0) 403 | }, 404 | 405 | // ### Shutdown 406 | // 407 | // The documentation suggests to reset the device when the program 408 | // shuts down. This means that all the lights are turned off and 409 | // the device is in basic mode, ready to be used by some other 410 | // program. 411 | 412 | postshutdown: function () { 413 | this.mixxx.midi.sendShortMsg(0xb7, 0x00, 0x00) 414 | } 415 | 416 | }); 417 | 418 | // Utilities 419 | // --------- 420 | // 421 | // The *scaledDiff* function returns a behaviour option that is useful 422 | // to define encoders with a specific sensitivity, which is useful to 423 | // correct the issues of the stepped encoders. 424 | 425 | function scaledDiff (factor) { 426 | return function (v, v0) { 427 | return (v0 + factor * (v > 64 ? v - 128 : v)).clamp(0, 128) 428 | } 429 | } 430 | 431 | function scaledSelectKnob (factor) { 432 | return function (v) { 433 | return factor * (v > 64 ? v - 128 : v) 434 | } 435 | } 436 | 437 | // > Copyright (C) 2013 Juan Pedro Bolívar Puente 438 | // > 439 | // > This program is free software: you can redistribute it and/or 440 | // > modify it under the terms of the GNU General Public License as 441 | // > published by the Free Software Foundation, either version 3 of the 442 | // > License, or (at your option) any later version. 443 | // > 444 | // > This program is distributed in the hope that it will be useful, 445 | // > but WITHOUT ANY WARRANTY; without even the implied warranty of 446 | // > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 447 | // > GNU General Public License for more details. 448 | // > 449 | // > You should have received a copy of the GNU General Public License 450 | // > along with this program. If not, see . 451 | -------------------------------------------------------------------------------- /src/behaviour.litcoffee: -------------------------------------------------------------------------------- 1 | mixco.behaviour 2 | =============== 3 | 4 | > This file is part of the [Mixco framework](http://sinusoid.es/mixco). 5 | > - **View me [on a static web](http://sinusoid.es/mixco/src/behaviour.html)** 6 | > - **View me [on GitHub](https://github.com/arximboldi/mixco/blob/master/src/behaviour.litcoffee)** 7 | 8 | This module contains all the functionality that lets you add 9 | *behaviour* to the hardware *controls* -- i.e. determine what they do. 10 | 11 | events = require 'events' 12 | transform = require './transform' 13 | value = require './value' 14 | {indent, assert, factory, copy} = require './util' 15 | {multi, isinstance} = require 'heterarchy' 16 | _ = {extend, bind, map} = require 'underscore' 17 | 18 | Actor 19 | ----- 20 | 21 | An **Actor** is the basic object that we want to add behaviours to. 22 | In general, they are *controls*, as defined by the `mixco.control` 23 | module. They have an `event` event, however, it is not guaranteed to 24 | be emitted if the interface decides that direct mappings suffice. 25 | 26 | `send` should be defined with signature `(state) ->` when it is 27 | available -- i.e. the *Actor* has output and requires the associated 28 | behaviours to update it. 29 | 30 | class exports.Actor extends events.EventEmitter 31 | 32 | send: undefined 33 | 34 | Options 35 | ------- 36 | 37 | These modify a behaviour in the same way that the equivalent options 38 | for the `