├── client ├── main.css ├── main.html └── main.jsx ├── .gitignore ├── .meteor ├── .gitignore ├── release ├── platforms ├── .id ├── .finished-upgraders ├── packages └── versions ├── _config.yml ├── public ├── clap1.mp3 ├── clap1.wav └── test.png ├── fonts ├── FontAwesome.otf ├── fontawesome-webfont.eot ├── fontawesome-webfont.ttf ├── fontawesome-webfont.woff └── fontawesome-webfont.woff2 ├── imports ├── client │ ├── components │ │ ├── editor │ │ │ ├── editor.sass │ │ │ └── Editor.jsx │ │ ├── mixer │ │ │ ├── knob │ │ │ │ ├── assets │ │ │ │ │ ├── top.png │ │ │ │ │ ├── back.png │ │ │ │ │ └── knob.png │ │ │ │ ├── knob.sass │ │ │ │ └── Knob.jsx │ │ │ └── MixTable.jsx │ │ ├── utils │ │ │ ├── Icon.jsx │ │ │ └── currys.js │ │ ├── AddSong.jsx │ │ ├── screens │ │ │ ├── Index.jsx │ │ │ ├── Mixer.jsx │ │ │ ├── Mixer2.jsx │ │ │ └── Main.jsx │ │ ├── Bpm.jsx │ │ ├── css │ │ │ ├── common.sass │ │ │ └── styles.js │ │ ├── code │ │ │ ├── prepareCode.js │ │ │ ├── evalCode.js │ │ │ └── updateParamsInCode.js │ │ ├── sound │ │ │ ├── AddSound.jsx │ │ │ ├── Sound.jsx │ │ │ └── SoundPush.jsx │ │ ├── Song.jsx │ │ ├── explorer │ │ │ ├── Explorer.jsx │ │ │ └── Explorer2.jsx │ │ └── player │ │ │ ├── soundsWatcher.js │ │ │ ├── sounds.js │ │ │ ├── Player.jsx │ │ │ └── tones.js │ └── router │ │ └── Router.jsx └── api │ ├── tones.js │ ├── songs.js │ └── sounds.js ├── static-assets.json ├── server └── main.js ├── README.md └── package.json /client/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.6 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /public/clap1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotgreg/XinDaw/HEAD/public/clap1.mp3 -------------------------------------------------------------------------------- /public/clap1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotgreg/XinDaw/HEAD/public/clap1.wav -------------------------------------------------------------------------------- /public/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotgreg/XinDaw/HEAD/public/test.png -------------------------------------------------------------------------------- /fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotgreg/XinDaw/HEAD/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotgreg/XinDaw/HEAD/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotgreg/XinDaw/HEAD/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /imports/client/components/editor/editor.sass: -------------------------------------------------------------------------------- 1 | .component-editor 2 | .CodeMirror 3 | height: 100% 4 | -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotgreg/XinDaw/HEAD/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotgreg/XinDaw/HEAD/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /imports/client/components/mixer/knob/assets/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotgreg/XinDaw/HEAD/imports/client/components/mixer/knob/assets/top.png -------------------------------------------------------------------------------- /static-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "map": { 3 | "imports/client/components/mixer/knob/assets/": "knob/" 4 | }, 5 | "verbose": true 6 | } 7 | -------------------------------------------------------------------------------- /imports/client/components/mixer/knob/assets/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotgreg/XinDaw/HEAD/imports/client/components/mixer/knob/assets/back.png -------------------------------------------------------------------------------- /imports/client/components/mixer/knob/assets/knob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotgreg/XinDaw/HEAD/imports/client/components/mixer/knob/assets/knob.png -------------------------------------------------------------------------------- /client/main.html: -------------------------------------------------------------------------------- 1 | 2 | XinDaw 3 | 4 | 5 | 6 |
7 | 8 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import '/imports/api/sounds.js'; 3 | import '/imports/api/songs.js'; 4 | import '/imports/api/tones.js'; 5 | 6 | Meteor.startup(() => { 7 | // code to run on server at startup 8 | }); 9 | -------------------------------------------------------------------------------- /imports/client/components/utils/Icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Icon extends React.Component { 4 | 5 | constructor(props){ 6 | super(props) 7 | } 8 | 9 | render() { 10 | return ( 11 | 12 | ) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 9aj2n61gb8uxx103be7u 8 | -------------------------------------------------------------------------------- /client/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meteor } from 'meteor/meteor'; 3 | import { render } from 'react-dom'; 4 | 5 | import { BrowserRouter } from 'react-router-dom' 6 | 7 | import RouterWrapper from '/imports/client/router/Router.jsx'; 8 | 9 | const AppWrapper = () => ( 10 | 11 | 12 | 13 | ) 14 | 15 | Meteor.startup(() => { 16 | render(, document.getElementById('render-target')); 17 | }); 18 | -------------------------------------------------------------------------------- /imports/client/components/mixer/knob/knob.sass: -------------------------------------------------------------------------------- 1 | @import "../../css/common.sass" 2 | 3 | .component-knob 4 | width: 50px 5 | float: left 6 | padding: 5px 7 | font-size: $font-size-s 8 | .name 9 | text-align: center 10 | margin-bottom: 3px 11 | .knob 12 | +icon('knob/knob.png', 30px) 13 | margin: 0 auto 3px auto 14 | input.number 15 | border: none 16 | background: none 17 | text-align: center 18 | width: 60px 19 | font-size: $font-size-s 20 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | -------------------------------------------------------------------------------- /imports/client/components/AddSong.jsx: -------------------------------------------------------------------------------- 1 | import {random} from 'lodash'; 2 | import React from 'react'; 3 | import dedent from 'dedent-js'; 4 | 5 | import { Meteor } from 'meteor/meteor' 6 | 7 | import * as css from '/imports/client/components/css/styles.js' 8 | 9 | export default class AddSong extends React.Component { 10 | 11 | constructor(props){ 12 | super(props) 13 | } 14 | 15 | createSong = () => { 16 | Meteor.call('songs.insert'); 17 | } 18 | 19 | render() { 20 | return ( 21 | 22 | addSong 23 | 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /imports/api/tones.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { Mongo } from 'meteor/mongo'; 3 | 4 | import {each} from 'lodash'; 5 | 6 | export const Tones = new Mongo.Collection('tones'); 7 | 8 | if (!Meteor.isServer) return false 9 | 10 | Meteor.publish('tones', () => Tones.find()) 11 | 12 | Meteor.methods({ 13 | 'tones.insert'(tone) { 14 | Tones.insert(tone); 15 | }, 16 | 17 | 'tones.removeFromSoundId'(soundId) { 18 | Tones.remove({soundId: soundId}); 19 | }, 20 | 21 | 'tones.removeAll'() { 22 | Tones.remove({}); 23 | }, 24 | 25 | 'tones.update'(tone) { 26 | Tones.update(tone._id, { $set: tone }) 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /imports/client/components/screens/Index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Router, Route, IndexRoute, Link, hashHistory } from 'react-router-dom' 4 | 5 | export default class IndexScreen extends React.Component { 6 | constructor(props){ 7 | super(props) 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 |

Xindaw

14 | 19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /imports/client/components/screens/Mixer.jsx: -------------------------------------------------------------------------------- 1 | import { withTracker } from 'meteor/react-meteor-data'; 2 | import { Tones } from '/imports/api/tones.js'; 3 | 4 | import Tone from 'tone'; 5 | import React from 'react'; 6 | 7 | import {each} from 'lodash'; 8 | import styled from 'styled-components'; 9 | 10 | import MixTable from '../mixer/MixTable'; 11 | 12 | 13 | class MixerScreen extends React.Component { 14 | constructor(props){ 15 | super(props) 16 | } 17 | 18 | render() { 19 | return ( 20 | 21 | ) 22 | } 23 | } 24 | 25 | 26 | // 27 | // DATA CONTAINER (METEOR) 28 | // 29 | 30 | export default withTracker(props => { 31 | Meteor.subscribe('tones'); 32 | return { 33 | tones: Tones.find({}, { sort: { createdAt: -1 } }).fetch(), 34 | }; 35 | })(MixerScreen); 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XinDaw 2 | 3 | ![Interface](https://user-images.githubusercontent.com/2981891/33467752-cfe98452-d691-11e7-8aac-9078a29bc421.jpg) 4 | 5 | 6 | ## Introduction 7 | * A Live Coding environment & Daw experimentation leveraging websockets (ToneJS/React/Meteor) 8 | * A surely far too ambitious project 9 | 10 | ## Why 11 | * Its like a Daw using live coding as its base 12 | * Multiscreens using websockets. For the initial design I plan to have 13 | * a main screen (live coding and sounds explorer, on desktop) 14 | * a mixer screen (all the knobs and other sounds modifiers, controlled on an ipad) 15 | * a video screen (a canvas which will evolve according to some musical parameter, projected or on TV screens) 16 | 17 | ## Current State 18 | Under intensive care right now, should reach a first usable state + some docs soonish 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /imports/client/router/Router.jsx: -------------------------------------------------------------------------------- 1 | import {Route, Switch, IndexRoute } from 'react-router-dom' 2 | import React from 'react'; 3 | 4 | import MainScreen from '/imports/client/components/screens/Main'; 5 | import MixerScreen from '/imports/client/components/screens/Mixer'; 6 | import MixerScreen2 from '/imports/client/components/screens/Mixer2'; 7 | import IndexScreen from '/imports/client/components/screens/Index'; 8 | 9 | export default class RouterWrapper extends React.Component { 10 | 11 | constructor(props){ 12 | super(props) 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /imports/client/components/Bpm.jsx: -------------------------------------------------------------------------------- 1 | import Tone from 'tone'; 2 | import React from 'react'; 3 | 4 | import * as css from '/imports/client/components/css/styles.js' 5 | 6 | export default class Bpm extends React.Component { 7 | 8 | constructor(props){ 9 | super(props) 10 | this.state = { 11 | bpm: '120' 12 | } 13 | } 14 | 15 | componentDidMount () { 16 | Tone.Transport.bpm.value = this.state.bpm 17 | } 18 | 19 | updateBpm = () => { 20 | setTimeout(() => Tone.Transport.bpm.value = this.state.bpm, 100) 21 | this.setState({bpm: this.refs.bpm.value}) 22 | } 23 | 24 | render() { 25 | return ( 26 | 27 | 32 | Change BPM 33 | 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /imports/client/components/css/common.sass: -------------------------------------------------------------------------------- 1 | // =icon($url, $size) 2 | // width: $size 3 | // height: $size 4 | // background-image: url($url) 5 | // background-size: contain 6 | // background-repeat: none 7 | 8 | // **************************** 9 | // VARIABLES 10 | // **************************** 11 | 12 | $font: monospace; 13 | $font-size-s: 9px; 14 | 15 | // **************************** 16 | // BASE 17 | // **************************** 18 | body { 19 | font-family: monospace; 20 | margin: 0; 21 | padding: 0; 22 | } 23 | 24 | // **************************** 25 | // MIXINS 26 | // **************************** 27 | 28 | @mixin icon($url, $size) { 29 | width: $size; 30 | height: $size; 31 | background-image: url($url); 32 | background-size: contain; 33 | background-repeat: none; 34 | } 35 | 36 | // **************************** 37 | // FONT 38 | // **************************** 39 | 40 | .input.text { 41 | border: none; 42 | padding: 10px; 43 | background: rgba(0, 0, 0, 0.05); 44 | } 45 | -------------------------------------------------------------------------------- /imports/client/components/code/prepareCode.js: -------------------------------------------------------------------------------- 1 | import esprima from 'esprima' 2 | import { intersection, filter } from 'lodash' 3 | 4 | export let prepareCode = (code) => { 5 | try { 6 | result = esprima.parseModule(code) 7 | 8 | // get all the vars declarations 9 | let variables = filter(result.body, e => e.type === "VariableDeclaration") 10 | 11 | // build the object returned according to what is given 12 | let toneVar = filter(variables, v => ['t', 'tone', 'c'].includes(v.declarations[0].id.name))[0] || false 13 | if (toneVar) toneVar = toneVar.declarations[0].id.name 14 | 15 | let optionsVar = filter(variables, v => ['o', 'options'].includes(v.declarations[0].id.name))[0] || false 16 | if (optionsVar) optionsVar = optionsVar.declarations[0].id.name 17 | 18 | let allElements = variables.map(v => v.declarations[0].id.name) 19 | 20 | let returnStatement = `return {c: ${toneVar}, o: ${optionsVar}, e: [${allElements}]}` 21 | 22 | return code + '\n\r' + returnStatement 23 | 24 | } catch (e) { 25 | console.warn('PrepareCode Error :', e) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /imports/client/components/code/evalCode.js: -------------------------------------------------------------------------------- 1 | import { each, filter } from 'lodash' 2 | import { prepareCode } from './prepareCode' 3 | 4 | import Tone from 'tone'; 5 | import teoria from 'teoria' 6 | 7 | export let evalCode = (code) => { 8 | 9 | code = prepareCode(code) 10 | 11 | try { 12 | let result = eval(`(function self(){${code}}())`); 13 | let error = `EDITOR RESULT ERROR => the code should contain one c(code) var` 14 | 15 | if(!result || typeof result !== 'object') return {status: 'err', body: error} 16 | if(!result.c) return {status: 'err', body: error} 17 | if(!result.e) return {status: 'err', body: error} 18 | if(result.o && result.o.vars) { 19 | let error = {status: 'err', body: 'EDITOR RESULT ERROR => each result.o.vars structure should be [NAMEVAR, VAR, MINVAL, MAXVAL]'} 20 | if (filter(result.o.vars, v => v instanceof Array).length !== result.o.vars.length) return error 21 | } 22 | 23 | return {status: 'ok', body: result} 24 | 25 | } catch (e) { 26 | let error = `EDITOR SYNTAX ERROR => ${e.message}` 27 | return {status: 'err', body: error} 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /imports/client/components/utils/currys.js: -------------------------------------------------------------------------------- 1 | import _ from 'ramda'; 2 | import R from 'ramda'; 3 | 4 | //// NUMBERS 5 | export const intervalWithIntVariation = (callback, min, max, step, intTime) => { 6 | step = step || 1 7 | intTime = intTime || 100 8 | let nb = min 9 | let up = true 10 | 11 | return setInterval(() => { 12 | nb = up ? nb + step : nb - step 13 | if (nb === max) up = false 14 | if (nb === min) up = true 15 | callback(nb) 16 | }, intTime) 17 | } 18 | 19 | 20 | 21 | //// ARRAY MANIP 22 | // + 23 | // export const arrayWithThisObj = (obj, array) => [...array, obj] 24 | 25 | // - 26 | export const arrayWithoutObjFrom = R.curry((prop, val, array) => R.filter(item => item[prop] !== val, array)) 27 | 28 | // get 29 | export const objInArrayFrom = R.curry((idProp, idVal, array) => R.find(R.propEq(idProp, idVal), array)) 30 | export const indexObjInArrayFrom = R.curry((idProp, idVal, array) => R.findIndex(R.propEq(idProp, idVal), array)) 31 | 32 | // update 33 | export const arrayWithUpdatedObjFrom = R.curry((idProp, idVal, update, array) => R.update(indexObjInArrayFrom(idProp, idVal, array), update, array)) 34 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base # Packages every Meteor app needs to have 8 | mobile-experience # Packages for a great mobile UX 9 | mongo # The database Meteor supports right now 10 | blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views 11 | reactive-var # Reactive variable for tracker 12 | tracker # Meteor's client-side reactive programming library 13 | 14 | standard-minifier-css # CSS minifier run for production mode 15 | standard-minifier-js # JS minifier run for production mode 16 | es5-shim # ECMAScript 5 compatibility for older browsers 17 | ecmascript # Enable ECMAScript2015+ syntax in app code 18 | shell-server # Server-side component of the `meteor shell` command 19 | 20 | react-meteor-data 21 | fortawesome:fontawesome 22 | fourseven:scss 23 | runisland:static-assets 24 | -------------------------------------------------------------------------------- /imports/client/components/sound/AddSound.jsx: -------------------------------------------------------------------------------- 1 | import {random} from 'lodash'; 2 | import React from 'react'; 3 | import dedent from 'dedent-js'; 4 | 5 | import { Meteor } from 'meteor/meteor' 6 | 7 | let notes = ['A2','B2','C2','D2','E2','F2','G2'] 8 | let timeNotes = ['2n','4n','8n','16n'] 9 | let times = ['0.5','1','1.5','2','2.5'] 10 | 11 | import * as css from '/imports/client/components/css/styles.js' 12 | 13 | export default class AddSound extends React.Component { 14 | 15 | constructor(props){ 16 | super(props) 17 | } 18 | 19 | createSound = () => { 20 | let codeDefault = dedent(` 21 | let freeverb = new Tone.Freeverb().toMaster(); 22 | freeverb.dampening.value = ${random(1000,5000)}; 23 | let synth = new Tone.AMSynth().connect(freeverb); 24 | Tone.Transport.schedule(time => { 25 | synth.triggerAttackRelease('${notes[random(notes.length - 1)]}', '${timeNotes[random(timeNotes.length - 1)]}', time) 26 | }, ${times[random(times.length - 1)]})`) 27 | 28 | Meteor.call('sounds.insert', codeDefault); 29 | } 30 | 31 | render() { 32 | return ( 33 | 34 | addSound 35 | 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-todos", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor run" 6 | }, 7 | "dependencies": { 8 | "babel-runtime": "^6.20.0", 9 | "dedent-js": "^1.0.1", 10 | "esprima": "^4.0.0", 11 | "font-awesome": "^4.7.0", 12 | "lodash": "^4.17.4", 13 | "meteor-node-stubs": "~0.2.4", 14 | "ramda": "^0.25.0", 15 | "react": "^16.0.0", 16 | "react-addons-pure-render-mixin": "^15.6.2", 17 | "react-codemirror": "^1.0.0", 18 | "react-dom": "^16.0.0", 19 | "react-hammerjs": "^1.0.1", 20 | "react-meteor-data": "^0.2.10", 21 | "react-router-dom": "^4.2.2", 22 | "react-sortable-tree": "^1.5.2", 23 | "react-sortable-tree-theme-file-explorer": "^1.1.2", 24 | "styled-components": "^2.2.3", 25 | "teoria": "^2.5.0", 26 | "tone": "^0.11.11" 27 | }, 28 | "devDependencies": { 29 | "@meteorjs/eslint-config-meteor": "^1.0.5", 30 | "babel-eslint": "^8.0.1", 31 | "eslint": "^4.10.0", 32 | "eslint-config-airbnb": "^16.1.0", 33 | "eslint-import-resolver-meteor": "^0.4.0", 34 | "eslint-plugin-import": "^2.8.0", 35 | "eslint-plugin-jsx-a11y": "^6.0.2", 36 | "eslint-plugin-meteor": "^4.1.4", 37 | "eslint-plugin-react": "^7.4.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /imports/client/components/css/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | // 4 | // VARS 5 | // 6 | export const vars = { 7 | colors: { 8 | orange: '#f8981d', 9 | grey3: '#ababab' 10 | } 11 | } 12 | 13 | // 14 | // LAYOUT 15 | // 16 | 17 | export const Panel = styled.div` 18 | width: ${props => props.w}%; 19 | height: 99vh; 20 | overflow-y: auto; 21 | float: left; 22 | padding: 1% 23 | `; 24 | 25 | export const Inline = styled.div` 26 | display: flex; 27 | flex-direction: row; 28 | `; 29 | 30 | 31 | // 32 | // INTERFACE 33 | // 34 | 35 | // FORMS 36 | export const FieldWrapper = styled.div` 37 | padding: 10px; 38 | `; 39 | 40 | export const Label = styled.div` 41 | display: inline-block; 42 | `; 43 | 44 | export const Input = styled.input` 45 | border: none; 46 | padding: 10px; 47 | background: rgba(0, 0, 0, 0.05); 48 | `; 49 | 50 | export const Number = Input.extend` 51 | width: ${props => props.width}; 52 | `; 53 | 54 | export const Button = styled.button` 55 | border: none; 56 | padding: 10px; 57 | margin: 5px; 58 | background: none; 59 | border: 2px inset black; 60 | color: black; 61 | pointer: cursor; 62 | transition: all .3s cubic-bezier(.55,0,.1,1); 63 | &:hover { 64 | background: black; 65 | color: white; 66 | } 67 | `; 68 | -------------------------------------------------------------------------------- /imports/client/components/Song.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meteor } from 'meteor/meteor' 3 | 4 | import Icon from './utils/Icon' 5 | 6 | import styled from 'styled-components'; 7 | 8 | export default class Song extends React.Component { 9 | 10 | constructor(props){ 11 | super(props) 12 | } 13 | 14 | removeThatSong = () => Meteor.call('songs.remove', this.props.song._id); 15 | selectThatSong = () => Meteor.call('songs.select', this.props.song._id); 16 | 17 | render() { 18 | return ( 19 | 21 | {this.props.song.name} ({this.props.song.sounds.length}) 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | } 30 | 31 | const OneSong = styled.li` 32 | position: relative; 33 | min-height: 25px; 34 | `; 35 | 36 | const Name = styled.p` 37 | display: inline-block; 38 | height: 0px; 39 | margin: 0px; 40 | color: ${props => props.selected && 'blue'}; 41 | `; 42 | 43 | const Buttons = styled.p` 44 | position: absolute; 45 | top: 0px; 46 | margin: 0px; 47 | right: 0px; 48 | `; 49 | -------------------------------------------------------------------------------- /imports/client/components/code/updateParamsInCode.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor' 2 | import { intersection, filter, each, isNumber } from 'lodash' 3 | 4 | import { Tones } from '/imports/api/tones'; 5 | Meteor.subscribe('tones'); 6 | 7 | import { Sounds } from '/imports/api/sounds'; 8 | Meteor.subscribe('sounds'); 9 | 10 | // Function that take the code that generate a tone and inject into the o/options array 11 | // the current tone value of the parameter 12 | export let updateParamsInCode = (soundId, toneId) => { 13 | console.log(soundId, toneId) 14 | 15 | let sound = Sounds.findOne(soundId) 16 | let tone = Tones.findOne(toneId) 17 | 18 | let values = tone.options.vars.map(v => `${v[0]} - ${v[1][v[0]]} - ${ v[1].persistedValue ? v[1].persistedValue : ''}`) 19 | console.log(values) 20 | 21 | let newCode = sound.code 22 | each(tone.options.vars, v => { 23 | let regString = `\\[(\\'|\\")${v[0]}(\\'|\\")\\,(.+)\\,(.+)\\,(.+)\\,(.+)(\\,(.+))*\\]` 24 | let regex = new RegExp(regString,"gi") 25 | 26 | let nameProp = v[0] 27 | let newValue = isNumber(v[1].persistedValue) ? v[1].persistedValue : v[1][nameProp] 28 | 29 | // newCode = newCode.replace(regex, `['${v[0]}'$3, ${newValue}]`) 30 | newCode = newCode.replace(regex, `['${v[0]}',$3,$4,$5,${newValue}]`) 31 | }) 32 | Meteor.call('sounds.updateCode', soundId, newCode) 33 | } 34 | -------------------------------------------------------------------------------- /imports/client/components/explorer/Explorer.jsx: -------------------------------------------------------------------------------- 1 | import { withTracker } from 'meteor/react-meteor-data'; 2 | import { Meteor } from 'meteor/meteor' 3 | import { Sounds } from '/imports/api/sounds.js'; 4 | import React from 'react'; 5 | import Sound from '../sound/Sound.jsx'; 6 | 7 | import * as css from '/imports/client/components/css/styles.js' 8 | 9 | class FileExplorer extends React.Component { 10 | 11 | constructor(props){ 12 | super(props) 13 | this.state = { 14 | searchedTerm: '' 15 | } 16 | } 17 | 18 | 19 | updateSearchedTerm = e => this.setState({searchedTerm: e.target.value}) 20 | 21 | render() { 22 | return ( 23 |
24 | 25 | search: 26 | 30 | 31 |
    32 | {this.props.soundsFiltered(new RegExp(this.state.searchedTerm)).map(sound => 33 | 34 | )} 35 |
36 |
37 | ) 38 | } 39 | } 40 | 41 | export default withTracker(props => { 42 | return { 43 | sounds: Sounds.find().fetch(), 44 | soundsFiltered: search => Sounds.find( { $or : [ { name : search }, { tags : search } ] }, { sort: { name: -1 } }).fetch() 45 | // soundsFiltered: search => Sounds.find({"name": search}, { sort: { name: -1 } }).fetch() 46 | }; 47 | })(FileExplorer); 48 | -------------------------------------------------------------------------------- /imports/client/components/player/soundsWatcher.js: -------------------------------------------------------------------------------- 1 | import {filter, reduce, intersection, indexOf, find, isEqual, each} from 'lodash'; 2 | 3 | export let soundsWatcher = (props) => { 4 | // 1 DETECT SOUNDS ADDED 5 | each(props.array1, sound => { 6 | if (!sound) return false 7 | 8 | let oldSound = find(props.array2, {'_id': sound._id}) 9 | if (!oldSound) { 10 | if (props.added) { 11 | // console.log(`${sound.name} added`) 12 | return props.added(sound) 13 | } 14 | } 15 | }) 16 | 17 | // 2 DETECT SOUNDS DELETED 18 | each(props.array2, sound => { 19 | if (!sound) return false 20 | 21 | let newSound = find(props.array1, {'_id': sound._id}) 22 | if (!newSound) { 23 | if (props.deleted) { 24 | // console.log(`${sound.name} deleted`) 25 | return props.deleted(sound) 26 | } 27 | } 28 | 29 | // 3 DETECT SOUNDS UPDATED 30 | let res = reduce(sound, (result, value, key) => isEqual(value, newSound[key]) ? result : result.concat(key), []) 31 | 32 | if (res.length === 0) return 33 | 34 | if (intersection(['code', 'muted'], res).length > 0) { 35 | if (props.updated) { 36 | console.log(`${newSound.name} updated, prop ${res[0]} changed`) 37 | return props.updated(newSound, res[0]) 38 | } 39 | } else { 40 | if (props.nothing) { 41 | // console.log(`${newSound.name} NOT updated, prop ${res[0]} changed`) 42 | return props.nothing(newSound, res[0]) 43 | } 44 | } 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.1.0 2 | autoupdate@1.3.12 3 | babel-compiler@6.24.7 4 | babel-runtime@1.1.1 5 | base64@1.0.10 6 | binary-heap@1.0.10 7 | blaze@2.3.2 8 | blaze-html-templates@1.1.2 9 | blaze-tools@1.0.10 10 | boilerplate-generator@1.3.0 11 | caching-compiler@1.1.9 12 | caching-html-compiler@1.1.2 13 | callback-hook@1.0.10 14 | check@1.2.5 15 | ddp@1.4.0 16 | ddp-client@2.2.0 17 | ddp-common@1.3.0 18 | ddp-server@2.1.0 19 | deps@1.0.12 20 | diff-sequence@1.0.7 21 | dynamic-import@0.2.0 22 | ecmascript@0.9.0 23 | ecmascript-runtime@0.5.0 24 | ecmascript-runtime-client@0.5.0 25 | ecmascript-runtime-server@0.5.0 26 | ejson@1.1.0 27 | es5-shim@4.6.15 28 | fortawesome:fontawesome@4.7.0 29 | fourseven:scss@4.5.4 30 | geojson-utils@1.0.10 31 | hot-code-push@1.0.4 32 | html-tools@1.0.11 33 | htmljs@1.0.11 34 | http@1.3.0 35 | id-map@1.0.9 36 | jquery@1.11.10 37 | launch-screen@1.1.1 38 | livedata@1.0.18 39 | logging@1.1.19 40 | meteor@1.8.0 41 | meteor-base@1.2.0 42 | minifier-css@1.2.16 43 | minifier-js@2.2.0 44 | minimongo@1.4.0 45 | mobile-experience@1.0.5 46 | mobile-status-bar@1.0.14 47 | modules@0.11.0 48 | modules-runtime@0.9.0 49 | mongo@1.3.0 50 | mongo-dev-server@1.1.0 51 | mongo-id@1.0.6 52 | npm-mongo@2.2.33 53 | observe-sequence@1.0.16 54 | ordered-dict@1.0.9 55 | promise@0.10.0 56 | random@1.0.10 57 | react-meteor-data@0.2.14 58 | reactive-var@1.0.11 59 | reload@1.1.11 60 | retry@1.0.9 61 | routepolicy@1.0.12 62 | runisland:static-assets@0.1.5 63 | shell-server@0.3.0 64 | spacebars@1.0.15 65 | spacebars-compiler@1.1.3 66 | standard-minifier-css@1.3.5 67 | standard-minifier-js@2.2.0 68 | templating@1.3.2 69 | templating-compiler@1.3.3 70 | templating-runtime@1.3.2 71 | templating-tools@1.1.2 72 | tmeasday:check-npm-versions@0.2.0 73 | tracker@1.1.3 74 | ui@1.0.13 75 | underscore@1.0.10 76 | url@1.1.0 77 | webapp@1.4.0 78 | webapp-hashing@1.0.9 79 | -------------------------------------------------------------------------------- /imports/client/components/player/sounds.js: -------------------------------------------------------------------------------- 1 | import { evalCode } from '../code/evalCode'; 2 | import { stopTone, startTone, persistTone, initTonesModifiers } from './tones'; 3 | 4 | import {filter, isFunction, reduce, intersection, indexOf, find, isEqual, each, get} from 'lodash'; 5 | 6 | tones = [] 7 | window.tones = tones 8 | 9 | // when sound is created or updated, call that function to generate its tome 10 | export let updateSound = (sound, props) => { 11 | 12 | console.log('updateSound') 13 | removeSound(sound, true) 14 | console.log('updateSound removeSound') 15 | 16 | if (sound.muted) return {status:'ok'} 17 | 18 | let result = evalCode(sound.code) 19 | 20 | if (result.status === 'err') return result 21 | 22 | result = result.body 23 | result = {soundId: sound._id, name: sound.name, tone: result.c, elementsToDispose: result.e, options: result.o} 24 | 25 | startTone(result.tone) 26 | console.log('updateSound starttone') 27 | 28 | window.tones.push(result) 29 | 30 | initTonesModifiers(result.options.vars) 31 | 32 | persistTone(result) 33 | 34 | return {status:'ok'} 35 | } 36 | 37 | // when tone get removed 38 | export let removeSound = (sound) => { 39 | let old = find(window.tones, {'soundId': sound._id}) 40 | 41 | old && stopTone(old.tone) 42 | 43 | if (get(old, 'elementsToDispose')) Tone.Transport.scheduleOnce(t => { 44 | each(old.elementsToDispose, e => { 45 | // if we have elementsToDispose, try to dispose them (tonejs) 46 | if (isFunction(e.dispose)) try { e.dispose() } catch (e) { console.log('error when dispose() :'+ e.message) } 47 | 48 | // then put them all equal to null to force the garbage collection process 49 | e = null 50 | }) 51 | }, 1) 52 | 53 | window.tones = filter(window.tones, t => t.soundId !== sound._id) 54 | Meteor.call('tones.removeFromSoundId', sound._id) 55 | } 56 | -------------------------------------------------------------------------------- /imports/api/songs.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { Mongo } from 'meteor/mongo'; 3 | import { check } from 'meteor/check'; 4 | import { random, filter, uniq, each } from 'lodash'; 5 | 6 | import {Sounds} from './sounds'; 7 | 8 | export const Songs = new Mongo.Collection('songs'); 9 | 10 | if (!Meteor.isServer) return false 11 | 12 | Meteor.publish('songs', () => Songs.find()) 13 | 14 | Meteor.methods({ 15 | 'helloworld' () { 16 | return `helloworld-${random(10000)}` 17 | }, 18 | 'songs.insert'() { 19 | Songs.insert({ 20 | 'name': `song-${random(10000)}`, 21 | 'selected': false, 22 | 'selectedSound': false, 23 | 'sounds': [], 24 | 'createdAt': new Date() 25 | }); 26 | }, 27 | 28 | 'songs.remove'(songId) { 29 | Songs.remove(songId); 30 | }, 31 | 32 | 'songs.select'(songId) { 33 | Songs.update({ selected: true }, { $set: { selected: false } }) 34 | Songs.update(songId, { $set: { selected: true } }) 35 | }, 36 | 37 | 'songs.addSound'(soundId) { 38 | let song = Songs.findOne({selected: true}) 39 | Songs.update(song._id, { $set: { sounds: uniq([...song.sounds, soundId]) } }) 40 | }, 41 | 42 | 'songs.removeSound'(soundId) { 43 | let song = Songs.findOne({selected: true}) 44 | Songs.update(song._id, { $set: { sounds: filter(song.sounds, s => s !== soundId) } }) 45 | }, 46 | 47 | 'songs.selectSound'(soundId) { 48 | let song = Songs.findOne({selected: true}) 49 | Songs.update(song._id, { $set: { selectedSound: soundId } }) 50 | }, 51 | 52 | // clean all songs from possible orphans id 53 | 'songs.cleanSoundsSongs' () { 54 | let songs = Songs.find().fetch() 55 | 56 | each(songs, song => { 57 | Songs.update(song._id, { $set: { sounds: filter(song.sounds, sid => { 58 | // if the sound still exist keep it, otherwise remove it 59 | return Sounds.find(sid).fetch().length > 0 60 | }) 61 | }}) 62 | }) 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /imports/client/components/player/Player.jsx: -------------------------------------------------------------------------------- 1 | import { withTracker } from 'meteor/react-meteor-data'; 2 | import { Meteor } from 'meteor/meteor' 3 | 4 | import React from 'react'; 5 | 6 | import Tone from 'tone'; 7 | import teoria from 'teoria' 8 | import _ from 'lodash' 9 | 10 | import styled from 'styled-components'; 11 | 12 | import { updateSound, removeSound } from './sounds'; 13 | import { soundsWatcher } from './soundsWatcher'; 14 | import { observeTones } from './tones'; 15 | 16 | export default class Player extends React.Component { 17 | 18 | constructor(props){ 19 | super(props) 20 | this.state = { 21 | codeErrors: false 22 | } 23 | } 24 | 25 | componentDidMount () { 26 | // clean all the meteor collection replication of the window.tones object 27 | Meteor.call('tones.removeAll') 28 | 29 | Tone.Transport.start('+0.1') 30 | Tone.Transport.loopEnd = '2m' 31 | Tone.Transport.loop = true 32 | window.Tone = Tone 33 | 34 | Tone.context.latencyHint = "interactive" 35 | 36 | observeTones() 37 | } 38 | 39 | // initializeSounds 40 | componentWillReceiveProps(nextProps) { 41 | let a =[this.props.sounds, nextProps.sounds] 42 | 43 | soundsWatcher({ 44 | array1: nextProps.sounds, 45 | array2: this.props.sounds, 46 | added: s => { 47 | updateSound(s) 48 | }, 49 | updated: (s, p) => { 50 | let result = updateSound(s) 51 | result.status === 'err' ? this.setState({codeErrors: result.body}) : this.setState({codeErrors: false}) 52 | }, 53 | deleted: s => { 54 | removeSound(s) 55 | }, 56 | nothing: (s, p) => console.log('nothing', p) 57 | }) 58 | } 59 | 60 | componentWillUnmount () { 61 | Tone.Transport.stop() 62 | } 63 | 64 | render() { 65 | let errors = this.state.codeErrors ? this.state.codeErrors : 'no synthax error' 66 | 67 | return ( 68 |
69 | {errors} 70 |
71 | 72 | ) 73 | } 74 | } 75 | 76 | const Result = styled.p` 77 | color: ${props => props.error ? 'red' : 'green'}; 78 | `; 79 | -------------------------------------------------------------------------------- /imports/api/sounds.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { Mongo } from 'meteor/mongo'; 3 | import { check } from 'meteor/check'; 4 | import {random, each, filter} from 'lodash'; 5 | 6 | import {Songs} from './songs'; 7 | 8 | export const Sounds = new Mongo.Collection('sounds'); 9 | 10 | if (!Meteor.isServer) return false 11 | 12 | Meteor.publish('sounds', () => Sounds.find()) 13 | 14 | Meteor.methods({ 15 | 'sounds.insert'(code) { 16 | 17 | check(code, String); 18 | 19 | Sounds.insert({ 20 | 'name': `sound-${random(10000)}`, 21 | 'tags': ``, 22 | 'selected': false, 23 | 'muted': false, 24 | 'code': code, 25 | 'createdAt': new Date() 26 | }); 27 | }, 28 | 'sounds.remove'(soundId) { 29 | check(soundId, String); 30 | 31 | Sounds.remove(soundId); 32 | 33 | // Clean orphan sounds id in songs 34 | Meteor.call('songs.cleanSoundsSongs') 35 | }, 36 | 'sounds.clone'(soundId) { 37 | check(soundId, String); 38 | let sound = Sounds.findOne(soundId) 39 | Sounds.insert({ 40 | 'name': `${sound.name}-clone`, 41 | 'tags': sound.tags, 42 | 'selected': sound.selected, 43 | 'muted': sound.muted, 44 | 'code': sound.code, 45 | 'createdAt': new Date() 46 | }); 47 | }, 48 | // 49 | // UPDATERS 50 | // 51 | 'sounds.select'(soundId) { 52 | check(soundId, String); 53 | 54 | Sounds.update({ selected: true }, { $set: { selected: false } }) 55 | Sounds.update(soundId, { $set: { selected: true } }) 56 | }, 57 | 'sounds.toggleMute'(soundId) { 58 | check(soundId, String); 59 | let sound = Sounds.findOne(soundId) 60 | Sounds.update(soundId, { $set: { muted: !sound.muted } }) 61 | }, 62 | 'sounds.updateCode'(soundId, code) { 63 | check(soundId, String); 64 | Sounds.update(soundId, { $set: { code: code } }) 65 | }, 66 | 'sounds.updateName'(soundId, name) { 67 | check(soundId, String); 68 | Sounds.update(soundId, { $set: { name: name } }) 69 | }, 70 | 'sounds.updateTags'(soundId, tags) { 71 | check(soundId, String); 72 | Sounds.update(soundId, { $set: { tags: tags } }) 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /imports/client/components/screens/Mixer2.jsx: -------------------------------------------------------------------------------- 1 | import { withTracker } from 'meteor/react-meteor-data'; 2 | 3 | import { Meteor } from 'meteor/meteor' 4 | 5 | import { Tones } from '/imports/api/tones.js'; 6 | import { Sounds } from '/imports/api/sounds.js'; 7 | import { Songs } from '/imports/api/songs.js'; 8 | 9 | import { find, times, each } from 'lodash'; 10 | 11 | import React from 'react'; 12 | import styled from 'styled-components'; 13 | 14 | import MixTable from '../mixer/MixTable'; 15 | import SoundPush from '../sound/SoundPush.jsx'; 16 | 17 | class MixerScreen2 extends React.Component { 18 | constructor(props){ 19 | super(props) 20 | this.state = { 21 | songSounds: [], 22 | selectedSong: false 23 | } 24 | } 25 | 26 | componentWillUpdate(nextProps, nextState) { 27 | // get the selected song and its related sounds 28 | let selectedSong = find(nextProps.songs, {selected: true}) 29 | 30 | if (!selectedSong) return false 31 | let songSounds = times(selectedSong.sounds.length, id => Sounds.findOne(selectedSong.sounds[id])) 32 | 33 | nextState.songSounds = songSounds 34 | nextState.selectedSong = selectedSong 35 | 36 | if (!nextProps.tones[0]) return false 37 | // let values = nextProps.tones[0]. options.vars.map(v => `${v[0]} - ${v[1][v[0]]}`) 38 | // console.log(values) 39 | } 40 | 41 | render() { 42 | return ( 43 |
44 | 45 | {this.state.songSounds.map(sound => 46 | 47 | )} 48 | 49 | 50 |
51 | ) 52 | } 53 | } 54 | 55 | // CSS 56 | 57 | const SoundsPushs = styled.div` 58 | display: flex; 59 | flex-wrap: wrap; 60 | // justify-content: space-between; 61 | flex-direction: row; 62 | `; 63 | 64 | 65 | // 66 | // DATA CONTAINER (METEOR) 67 | // 68 | 69 | export default withTracker(props => { 70 | Meteor.subscribe('sounds'); 71 | Meteor.subscribe('songs'); 72 | Meteor.subscribe('tones'); 73 | return { 74 | songs: Songs.find({}, { sort: { createdAt: -1 } }).fetch(), 75 | sounds: Sounds.find({}, { sort: { createdAt: -1 } }).fetch(), 76 | tones: Tones.find({}, { sort: { createdAt: -1 } }).fetch() 77 | }; 78 | })(MixerScreen2); 79 | -------------------------------------------------------------------------------- /imports/client/components/sound/Sound.jsx: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor' 2 | import Songs from '/imports/api/songs' 3 | 4 | import React from 'react'; 5 | import styled from 'styled-components'; 6 | 7 | import Icon from '../utils/Icon' 8 | 9 | import {filter} from 'lodash'; 10 | 11 | export default class Sound extends React.Component { 12 | 13 | constructor(props){ 14 | super(props) 15 | } 16 | 17 | removeThatSound = () => Meteor.call('sounds.remove', this.props.sound._id); 18 | selectThatSound = () => Meteor.call('sounds.select', this.props.sound._id); 19 | toggleThatSound = () => Meteor.call('sounds.toggleMute', this.props.sound._id); 20 | cloneThatSound = () => Meteor.call('sounds.clone', this.props.sound._id); 21 | 22 | addToSong = () => Meteor.call('songs.addSound', this.props.sound._id); 23 | removeToSong = () => Meteor.call('songs.removeSound', this.props.sound._id); 24 | 25 | render() { 26 | let buttons = null 27 | if (this.props.type === 'songSound') { 28 | buttons = ([ 29 | , 32 | 35 | ]) 36 | } else { 37 | buttons = ([ 38 | , 41 | 44 | ]) 45 | } 46 | 47 | return ( 48 | 49 | {this.props.sound.name} 50 | 51 | 54 | {buttons} 55 | 56 | 57 | ) 58 | } 59 | } 60 | 61 | const OneSound = styled.li` 62 | position: relative; 63 | min-height: 25px; 64 | `; 65 | 66 | const Name = styled.p` 67 | display: inline-block; 68 | height: 0px; 69 | margin: 0px; 70 | color: ${props => props.selected && 'blue'}; 71 | `; 72 | 73 | const Buttons = styled.p` 74 | position: absolute; 75 | top: 0px; 76 | margin: 0px; 77 | right: 0px; 78 | `; 79 | -------------------------------------------------------------------------------- /imports/client/components/sound/SoundPush.jsx: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor' 2 | import Songs from '/imports/api/songs' 3 | 4 | import React from 'react'; 5 | import styled from 'styled-components'; 6 | 7 | import Icon from '../utils/Icon' 8 | 9 | import {filter} from 'lodash'; 10 | import * as css from '/imports/client/components/css/styles.js' 11 | 12 | export default class SoundPush extends React.Component { 13 | 14 | constructor(props){ 15 | super(props) 16 | } 17 | 18 | selectThatSound = () => Meteor.call('sounds.select', this.props.sound._id); 19 | toggleThatSound = () => Meteor.call('sounds.toggleMute', this.props.sound._id); 20 | 21 | removeToSong = () => Meteor.call('songs.removeSound', this.props.sound._id); 22 | 23 | toggleAndSelectThatSound = () => { this.selectThatSound(); this.toggleThatSound() } 24 | 25 | render() { 26 | let buttons = null 27 | if (this.props.type === 'songSound') { 28 | buttons = ([ 29 | 30 | 31 | , 32 | 33 | 34 | 35 | ]) 36 | } 37 | 38 | return ( 39 | 40 | {this.props.sound.name} 41 | 42 | {buttons} 43 | 44 | 45 | ) 46 | } 47 | } 48 | 49 | const OneSound = styled.li` 50 | position: relative; 51 | width: 80px; 52 | height: 80px; 53 | flex-shrink: 0; 54 | margin: 10px; 55 | padding: 10px; 56 | border: 2px black solid; 57 | min-height: 25px; 58 | list-style: none; 59 | font-size: 10px; 60 | background: ${props => props.muted && 'rgba(255, 233, 77, 0.5)'}; 61 | `; 62 | 63 | const Name = styled.p` 64 | display: block; 65 | height: 0px; 66 | margin: 0px; 67 | color: ${props => props.selected && 'blue'}; 68 | `; 69 | 70 | const Buttons = styled.p` 71 | position: absolute; 72 | bottom: 0px; 73 | left: 0px; 74 | display: flex; 75 | width: 100%; 76 | margin: 0px 77 | `; 78 | 79 | const SoundPushButton = css.Button.extend` 80 | flex-grow: 1; 81 | margin: 0px; 82 | border-right: 0px solid black; 83 | border-left: 0px solid black; 84 | border-bottom: 0px solid black; 85 | `; 86 | -------------------------------------------------------------------------------- /imports/client/components/player/tones.js: -------------------------------------------------------------------------------- 1 | import Tone from 'tone'; 2 | import { Meteor } from 'meteor/meteor' 3 | 4 | import { Tones } from '/imports/api/tones'; 5 | import { each, find, get, set, isNumber, round } from 'lodash'; 6 | 7 | Meteor.subscribe('tones'); 8 | 9 | export let getToneType = tone => { 10 | if (typeof tone === 'number') return 'transport-event' 11 | else if (typeof tone === 'object') return 'loop' 12 | 13 | return false 14 | } 15 | 16 | export let stopTone = tone => { 17 | console.log('stopTone') 18 | let type = getToneType(tone) 19 | type === 'loop' && Tone.Transport.scheduleOnce(t => tone.stop().dispose(), 1) 20 | type === 'transport-event' && Tone.Transport.clear(tone) 21 | type === 'transport-event' && console.log('transport event' + tone) 22 | } 23 | 24 | export let startTone = tone => { 25 | let type = getToneType(tone) 26 | type === 'loop' && Tone.Transport.scheduleOnce(t => tone.start(0), 1) 27 | } 28 | 29 | // for each option var defined in the o array, apply the init value 30 | export let initTonesModifiers = (vars) => { 31 | each(vars, v => { 32 | // ['pitchDecay', synth, 0, 0.5, 0.001 ,->0] which is init value of param 33 | if (isNumber(v[5])) { 34 | isNumber(v[1][v[0]]) ? v[1][v[0]] = round(v[5],5) : v[1].value = v[5] 35 | } 36 | }) 37 | } 38 | 39 | 40 | // persist the window.tones object to a meteor collection 41 | export let persistTone = tone => { 42 | if (!get(tone, 'options.vars')) return false 43 | each(tone.options.vars, (v,i) => { 44 | if (!v[1]) { 45 | console.warn(`the options ${v[0]} is not interpreted as its value is null`) 46 | tone.options.vars.splice(i, 1) 47 | return false 48 | } 49 | if (v[1].value) v[1].persistedValue = v[1].value 50 | }) 51 | Meteor.call('tones.insert', tone) 52 | } 53 | 54 | // if we have some vars inside the options that changed 55 | // spread the change to value inside window.tones to modify sound 56 | export let observeTones = () => { 57 | var handle = Tones.find().observeChanges({ 58 | changed: function(id, field) { 59 | let soundId = Tones.findOne(id).soundId 60 | let tone = find(window.tones, {soundId: soundId}) 61 | let vars = field['options']['vars'] 62 | 63 | each(vars, (v,i) => { 64 | // if the options.vars.variable is a Tone.Param (obj), update its .value prop 65 | // else if a number, update the number directly 66 | let nameProp = tone['options']['vars'][i][0] 67 | isNumber(v[1].persistedValue) ? tone['options']['vars'][i][1].value = v[1].persistedValue : tone['options']['vars'][i][1][nameProp] = v[1][nameProp] 68 | }) 69 | } 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /imports/client/components/mixer/MixTable.jsx: -------------------------------------------------------------------------------- 1 | import Tone from 'tone'; 2 | import React from 'react'; 3 | 4 | import { each, isNumber, find} from 'lodash'; 5 | import styled from 'styled-components'; 6 | 7 | import { Meteor } from 'meteor/meteor' 8 | import esprima from 'esprima' 9 | 10 | import Knob from './knob/Knob'; 11 | import { updateParamsInCode } from '../code/updateParamsInCode'; 12 | 13 | export default class MixTable extends React.Component { 14 | constructor(props){ 15 | super(props) 16 | } 17 | 18 | changeValue = (tone, i, value) => { 19 | 20 | let nameProp = tone.options.vars[i][0] 21 | if (isNumber(tone.options.vars[i][1].persistedValue)) tone.options.vars[i][1].persistedValue = value 22 | else tone.options.vars[i][1][nameProp] = value 23 | 24 | if (!tone.options.vars[i][1].amithesame) tone.options.vars[i][1].amithesame = 0 25 | tone.options.vars[i][1].amithesame = tone.options.vars[i][1].amithesame + 1 26 | 27 | Meteor.call('tones.update', tone) 28 | } 29 | 30 | componentWillUpdate (nextProps, nextState) { 31 | // console.log('MIXTABLE componentWillUpdate', nextProps, nextState) 32 | // nextState.variable = nextProps.variable 33 | } 34 | 35 | saveParamsTone = tone => { 36 | updateParamsInCode(tone.soundId, tone._id) 37 | } 38 | 39 | render() { 40 | return ( 41 | 42 | {this.props.tones.map(tone => ( 43 | 44 | {tone.name} 45 |
46 | {(tone.options && tone.options.vars) && tone.options.vars.map((v,i) => 47 | 56 | )} 57 |
58 |
save
59 |
60 | ) 61 | )} 62 | 63 |
64 | ) 65 | } 66 | } 67 | 68 | // 69 | // CSS 70 | // 71 | 72 | const Table = styled.div` 73 | 74 | `; 75 | 76 | const Clear = styled.div` 77 | clear: both; 78 | `; 79 | 80 | const SoundName = styled.div` 81 | padding: 10px; 82 | background: #dfdfdf; 83 | text-align: center; 84 | `; 85 | 86 | const SoundMixer = styled.div` 87 | background: #f3f3f3; 88 | max-width: 50%; 89 | margin: 5px; 90 | float: left; 91 | `; 92 | -------------------------------------------------------------------------------- /imports/client/components/editor/Editor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CodeMirror from 'react-codemirror'; 3 | 4 | import { Meteor } from 'meteor/meteor' 5 | 6 | import * as css from '/imports/client/components/css/styles.js' 7 | 8 | // require('./editor.css'); 9 | require('./editor.sass'); 10 | 11 | require('codemirror/lib/codemirror.css'); 12 | require('codemirror/theme/monokai.css'); 13 | require('codemirror/keymap/sublime.js'); 14 | require('codemirror/mode/javascript/javascript.js'); 15 | 16 | export default class Editor extends React.Component { 17 | 18 | constructor(props){ 19 | super(props) 20 | } 21 | 22 | componentDidUpdate(prevProps, prevState) { 23 | if (!this.props.sound) return false 24 | 25 | const codeMirror = this.refs['editor'].getCodeMirrorInstance(); 26 | 27 | this.refs.editor.getCodeMirror().setValue(this.props.sound.code); 28 | } 29 | 30 | saveCode = () => Meteor.call('sounds.updateCode', this.props.sound._id, this.refs.editor.getCodeMirror().getValue()) 31 | saveName = () => Meteor.call('sounds.updateName', this.props.sound._id, this.refs.name.value) 32 | saveTags = () => Meteor.call('sounds.updateTags', this.props.sound._id, this.refs.tags.value) 33 | save = () => { 34 | this.saveCode() 35 | // this.saveTitle() 36 | } 37 | 38 | render() { 39 | if (!this.props.sound) return false 40 | 41 | let options = { 42 | keyMap: "sublime", 43 | mode: "javascript", 44 | theme: "monokai", 45 | disableKeywords: true, 46 | completeSingle: false, 47 | lineNumbers: true, 48 | extraKeys: { 49 | "Ctrl-S": () => { 50 | this.saveCode() 51 | } 52 | }, 53 | completeOnSingleClick: false 54 | } 55 | 56 | return ( 57 |
58 | 59 | 60 | title: 61 | 67 | 68 | 69 | tags: 70 | 76 | 77 | 78 | 82 | Save Code 83 |
84 | ) 85 | } 86 | } 87 | 88 | // React.render(, document.getElementById('app')); 89 | -------------------------------------------------------------------------------- /imports/client/components/screens/Main.jsx: -------------------------------------------------------------------------------- 1 | import { withTracker } from 'meteor/react-meteor-data'; 2 | import { Meteor } from 'meteor/meteor' 3 | import { Sounds } from '/imports/api/sounds.js'; 4 | import { Songs } from '/imports/api/songs.js'; 5 | 6 | import React from 'react'; 7 | import { find, times, random } from 'lodash'; 8 | 9 | import AddSound from '../sound/AddSound.jsx'; 10 | import Sound from '../sound/Sound.jsx'; 11 | 12 | import AddSong from '../AddSong.jsx'; 13 | import Song from '../Song.jsx'; 14 | 15 | import Bpm from '../Bpm.jsx'; 16 | import Editor from '../editor/Editor.jsx'; 17 | import Player from '../player/Player.jsx'; 18 | import Explorer from '../explorer/Explorer.jsx'; 19 | // import Explorer from '../explorer/Explorer2.jsx'; 20 | 21 | // import styled from 'styled-components'; 22 | import * as css from '/imports/client/components/css/styles.js' 23 | 24 | export class MainScreen extends React.Component { 25 | 26 | constructor(props){ 27 | super(props) 28 | this.state = { 29 | songSounds: [], 30 | selectedSong: false, 31 | tones: [] 32 | } 33 | } 34 | 35 | componentWillUpdate(nextProps, nextState) { 36 | // get the selected song and its related sounds 37 | let selectedSong = find(nextProps.songs, {selected: true}) 38 | let songSounds = times(selectedSong.sounds.length, id => Sounds.findOne(selectedSong.sounds[id])) 39 | 40 | nextState.songSounds = songSounds 41 | nextState.selectedSong = selectedSong 42 | } 43 | 44 | render() { 45 | return ( 46 |
47 | 48 | 49 |

SOUNDS SONG

50 |
    51 | {this.state.songSounds.map(sound => { 52 | return 53 | } 54 | )} 55 |
56 | 57 |

SONGS

58 |
    59 | {this.props.songs.map(song => 60 | 61 | )} 62 |
63 | 64 |
65 | 66 | 67 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | ) 79 | } 80 | } 81 | 82 | // 83 | // DATA CONTAINER (METEOR) 84 | // 85 | 86 | export default withTracker(props => { 87 | Meteor.subscribe('sounds'); 88 | Meteor.subscribe('songs'); 89 | return { 90 | sounds: Sounds.find({}, { sort: { createdAt: -1 } }).fetch(), 91 | songs: Songs.find({}, { sort: { createdAt: -1 } }).fetch(), 92 | selectedSound: Sounds.findOne({selected: true}) 93 | }; 94 | })(MainScreen); 95 | -------------------------------------------------------------------------------- /imports/client/components/mixer/knob/Knob.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Hammer from 'react-hammerjs'; 4 | import { clamp, round, throttle, debounce } from 'lodash'; 5 | 6 | require('./knob.sass') 7 | 8 | export default class Knob extends React.Component { 9 | 10 | constructor(props){ 11 | super(props) 12 | 13 | let decimals = (this.props.step + "").split(".")[1] 14 | let precision = decimals ? decimals.length : 0 15 | 16 | // console.log(props.initVal, ) 17 | let val = props.initVal ? props.initVal : props.val 18 | 19 | this.state = { 20 | startingVal: 0, 21 | val: val, 22 | currentPercentage: 0, 23 | precision: precision 24 | } 25 | 26 | // console.log(this.props.name, this.state.val, props.initVal, props.val) 27 | } 28 | 29 | componentDidMount () { 30 | this.updateCurrentPercentage() 31 | } 32 | 33 | 34 | componentWillUpdate (nextProps, nextState) { 35 | // console.log('componentWillUpdate', nextProps, nextState) 36 | nextState.variable = nextProps.variable 37 | } 38 | 39 | // shouldComponentUpdate(nextProps, nextState){ 40 | // // return false; 41 | // } 42 | 43 | // 44 | // UX : touch management 45 | // 46 | 47 | throttleHandlePan = throttle(e => { this.handlePan(e) }, 200) 48 | 49 | handlePan = e => { 50 | e.preventDefault() 51 | let variator = (this.props.max / 200) * e.deltaY 52 | 53 | this.state.val = clamp(parseFloat(round(this.state.startingVal + variator, this.state.precision)), this.props.min, this.props.max) 54 | 55 | this.refs.input.value = round(this.state.val, this.state.precision) 56 | 57 | this.changeValue() 58 | } 59 | 60 | handlePanStart = e => { 61 | console.log('handlePanStart') 62 | e.preventDefault() 63 | this.setState({startingVal: this.state.val}) 64 | } 65 | 66 | modifyValueByInput = e => { 67 | this.state.val = parseFloat(e.target.value) 68 | 69 | this.changeValue() 70 | } 71 | 72 | // UI 73 | 74 | updateCurrentPercentage = () => { 75 | let aPercent = ( this.props.max - this.props.min ) / 100 76 | let currentRelativePosition = this.state.val - this.props.min 77 | let currentPercentage = currentRelativePosition / aPercent 78 | 79 | this.setState({currentPercentage: currentPercentage}) 80 | } 81 | 82 | // 83 | // DATA FLOW 84 | // 85 | 86 | changeValue = () => { 87 | this.updateCurrentPercentage() 88 | this.props.onValueChange(this.state.val) 89 | } 90 | 91 | // throttleChangeValue = throttle(() => { 92 | // this.updateCurrentPercentage() 93 | // this.props.onValueChange(this.state.val) 94 | // }, 50) 95 | 96 | render() { 97 | return ( 98 |
99 | 104 |
105 |
{this.props.name}
106 |
107 |
108 | 117 |
118 |
119 |
120 | ) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /imports/client/components/explorer/Explorer2.jsx: -------------------------------------------------------------------------------- 1 | import { withTracker } from 'meteor/react-meteor-data'; 2 | import { Meteor } from 'meteor/meteor' 3 | import { Sounds } from '/imports/api/sounds.js'; 4 | import React from 'react'; 5 | import Sound from '../sound/Sound.jsx'; 6 | 7 | import SortableTree, { addNodeUnderParent, removeNodeAtPath, getFlatDataFromTree, getTreeFromFlatData } from 'react-sortable-tree'; 8 | import FileExplorerTheme from 'react-sortable-tree-theme-file-explorer'; 9 | 10 | import styled from 'styled-components'; 11 | 12 | class FileExplorer2 extends React.Component { 13 | 14 | constructor(props){ 15 | super(props) 16 | this.state = { 17 | searchedTerm: '', 18 | treeData: [{ title: 'Chicken', children: [ { title: 'Egg', otherprop: 'lolcats2' } ], otherprop: 'lolcats' }] 19 | } 20 | 21 | setTimeout(() => { 22 | let newTreeData = [ ...this.state.treeData, { title: 'woooopreactivity', children: [ { title: 'Egg', otherprop: 'lolcats2' } ], otherprop: 'lolcats' }] 23 | this.setState({treeData: newTreeData}) 24 | }, 2000) 25 | } 26 | 27 | 28 | updateSearchedTerm = e => this.setState({searchedTerm: e.target.value}) 29 | 30 | updateTreeData = treeData => { 31 | 32 | let flatmap = getFlatDataFromTree({treeData: treeData, getNodeKey: this.getNodeKey, ignoreCollapsed: false}) 33 | flatmap = flatmap.map(({ node, path }) => { 34 | return { 35 | id: node.id, 36 | name: node.name, 37 | // The last entry in the path is this node's key 38 | // The second to last entry (accessed here) is the parent node's key 39 | parent: path.length > 1 ? path[path.length - 2] : null, 40 | } 41 | }) 42 | 43 | this.setState({ treeData }) 44 | } 45 | 46 | createNode = (node) => { 47 | return { 48 | title: `helloworld ${ 49 | node.title.split(' ')[0] 50 | }sson`, 51 | } 52 | } 53 | 54 | generateNodePropsFunc = ({ node, path }) => { 55 | let buttons = [ 56 | 57 | ] 58 | // if (!node.children) buttons.push() 59 | return {buttons: buttons} 60 | } 61 | 62 | getNodeKey = ({ treeIndex }) => { 63 | return treeIndex 64 | }; 65 | 66 | // addNode = ({ node, path }) => { 67 | addNode = (node, path) => { 68 | // return 69 | return this.setState(state => ({ 70 | treeData: addNodeUnderParent({ 71 | treeData: state.treeData, 72 | parentKey: path[path.length - 1], 73 | expandParent: true, 74 | getNodeKey: this.getNodeKey, 75 | newNode: this.createNode(node), 76 | }).treeData, 77 | })) 78 | } 79 | 80 | removeNode = (node, path) => { 81 | return this.setState(state => ({ 82 | treeData: removeNodeAtPath({ 83 | treeData: state.treeData, 84 | path: path, 85 | getNodeKey: this.getNodeKey, 86 | }), 87 | })) 88 | } 89 | 90 | render() { 91 | return ( 92 |
93 |
94 | 100 |
101 | Explorer 102 | 106 |
    107 | {this.props.soundsFiltered(new RegExp(this.state.searchedTerm)).map(sound => 108 | 109 | )} 110 |
111 |
112 | ) 113 | } 114 | } 115 | 116 | export default withTracker(props => { 117 | return { 118 | sounds: Sounds.find().fetch(), 119 | soundsFiltered: search => Sounds.find({"name": search}, { sort: { name: -1 } }).fetch() 120 | }; 121 | })(FileExplorer2); 122 | --------------------------------------------------------------------------------