this.container = el} />
58 | );
59 | }
60 | }
61 |
62 | const mapStateToProps = (state) => ({
63 | renderParams: state.videoRenderer.renderParams
64 | });
65 |
66 | const mapDispatchToProps = (dispatch) => ({
67 | onValueChange: (name, value) => dispatch(setRenderparam(name, value))
68 | });
69 |
70 | export default connect(mapStateToProps, mapDispatchToProps)(VideoControlsGui)
--------------------------------------------------------------------------------
/src/video-player.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import VideoRenderer from './lib/video/renderer';
4 |
5 | import './video-player.css';
6 |
7 | class VideoPlayer extends Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.videoRenderer = new VideoRenderer();
12 | this.simpleVideoContainer = document.createElement('div');
13 | this.simpleVideoContainer.className = 'simpleVideoContainer';
14 | }
15 |
16 | render() {
17 | return (
18 |
this.container = el } className="videoPlayer" />
19 | );
20 | }
21 |
22 | componentDidMount() {
23 | this.container.appendChild(this.simpleVideoContainer);
24 | this.container.appendChild(this.videoRenderer.getDomElement());
25 | }
26 |
27 | componentWillUpdate({ renderParams, videos, singleVideo }) {
28 | this.videoRenderer.setRenderParams(renderParams);
29 | this.videoRenderer.setVideos(videos);
30 |
31 | if (singleVideo && singleVideo !== this.props.singleVideo) {
32 | this.simpleVideoContainer.innerHTML = '';
33 | this.simpleVideoContainer.appendChild(singleVideo);
34 | }
35 |
36 | if (!singleVideo) {
37 | this.simpleVideoContainer.innerHTML = '';
38 | }
39 | }
40 |
41 | componentWillUnmount() {
42 | this.videoRenderer.stop();
43 | const domEl = this.videoRenderer.getDomElement();
44 | domEl.parentNode.removeChild(domEl);
45 | }
46 | }
47 |
48 | const mapStateToProps = (state, ownProps) => {
49 | const { scheduler, clips, videoRenderer } = state;
50 | const playingFileIds = Object.keys(scheduler.playing);
51 |
52 | const videos = playingFileIds
53 | .map((fileId) => {
54 | const clip = clips[scheduler.playing[fileId].clipId];
55 | if (clip && !clip.noFilter) {
56 | return scheduler.playing[fileId].payload.videoElement
57 | }
58 | return undefined;
59 | })
60 | .filter(Boolean);
61 |
62 | const singleVideo = playingFileIds
63 | .map((fileId) => {
64 | const clip = clips[scheduler.playing[fileId].clipId];
65 | if (clip && clip.noFilter) {
66 | return scheduler.playing[fileId].payload.videoElement
67 | }
68 | return undefined;
69 | })
70 | .filter(Boolean)
71 | .pop();
72 |
73 | const renderParams = videoRenderer.renderParams;
74 |
75 | return { videos, singleVideo, renderParams: { ...ownProps, ...renderParams } };
76 | };
77 |
78 | export default connect(mapStateToProps, null)(VideoPlayer);
--------------------------------------------------------------------------------
/src/data/scheduler.js:
--------------------------------------------------------------------------------
1 | /**
2 | * -------------------- ACTION TYPES ----------------------------
3 | */
4 | const ADD_SCHEDULED = 'jsconf2017/scheduler/ADD_SCHEDULED';
5 | const ADD_PLAYING = 'jsconf2017/scheduler/ADD_PLAYING';
6 | const ADD_TOSTOP = 'jsconf2017/scheduler/ADD_TOSTOP';
7 | const MEDIA_ENDED = 'jsconf2017/scheduler/MEDIA_ENDED';
8 | const FLUSH_SCHEDULED = 'jsconf2017/scheduler/FLUSH_SCHEDULED';
9 |
10 | /**
11 | * -------------------- REDUCER ----------------------------
12 | */
13 | export default function scheduler(state, action) {
14 | if (!state) {
15 | state = { scheduled: {}, toStop: {}, playing: {} };
16 | }
17 |
18 | const { id } = action;
19 |
20 | switch (action.type) {
21 | case ADD_SCHEDULED:
22 | return {
23 | ...state,
24 | scheduled: {
25 | ...state.scheduled,
26 | [id]: true
27 | }
28 | };
29 | case FLUSH_SCHEDULED:
30 | return {
31 | ...state,
32 | scheduled: {},
33 | toStop: {}
34 | };
35 | case ADD_PLAYING:
36 | const { payload, clipId } = action;
37 | return {
38 | ...state,
39 | playing: {
40 | ...state.playing,
41 | [id]: { payload, clipId }
42 | }
43 | };
44 | case ADD_TOSTOP:
45 | return {
46 | ...state,
47 | toStop: {
48 | ...state.toStop,
49 | [id]: true
50 | }
51 | };
52 | case MEDIA_ENDED:
53 | const playingCopy = { ...state.playing };
54 |
55 | delete playingCopy[id];
56 |
57 | return {
58 | ...state,
59 | playing: playingCopy
60 | };
61 | default:
62 | return state;
63 | }
64 | }
65 |
66 | /**
67 | * -------------------- HELPERS ----------------------------
68 | */
69 | export const playingId = (clipId, fileId) => `file:${fileId}-clip${clipId}`;
70 | export const isPlaying = (playingState, clip) => playingState[playingId(clip.id, clip.file)] || playingState[playingId(clip.id, clip.videoFile)]
71 |
72 | /**
73 | * -------------------- ACTION CREATORS ----------------------------
74 | */
75 | export const addPlaying = (fileId, payload, clipId) => ({ type: ADD_PLAYING, id: playingId(clipId, fileId), payload, clipId});
76 | export const addScheduled = (id) => ({ type: ADD_SCHEDULED, id });
77 | export const mediaEnded = (fileId, clipId) => ({ type: MEDIA_ENDED, id: playingId(clipId, fileId) });
78 | export const flushScheduled = () => ({ type: FLUSH_SCHEDULED });
79 | export const scheduleStop = (id) => ({ type: ADD_TOSTOP, id });
80 |
--------------------------------------------------------------------------------
/src/lib/junk.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copied over from https://github.com/sindresorhus/junk/blob/master/index.js
3 | * because the uglifyjs we're does not understand arrow functions.
4 | * Their licence applies:
5 | *
6 | The MIT License (MIT)
7 |
8 | Copyright (c) Sindre Sorhus
(sindresorhus.com)
9 |
10 | Permission is hereby granted, free of charge, to any person obtaining a copy
11 | of this software and associated documentation files (the "Software"), to deal
12 | in the Software without restriction, including without limitation the rights
13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | copies of the Software, and to permit persons to whom the Software is
15 | furnished to do so, subject to the following conditions:
16 |
17 | The above copyright notice and this permission notice shall be included in
18 | all copies or substantial portions of the Software.
19 |
20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 | THE SOFTWARE.
27 |
28 | */
29 |
30 | // # All
31 | // /^npm-debug\.log$/, // npm error log
32 | // /^\..*\.swp$/, // vim state
33 | // // macOS
34 | // /^\.DS_Store$/, // stores custom folder attributes
35 | // /^\.AppleDouble$/, // stores additional file resources
36 | // /^\.LSOverride$/, // contains the absolute path to the app to be used
37 | // /^Icon\r$/, // custom Finder icon: http://superuser.com/questions/298785/icon-file-on-os-x-desktop
38 | // /^\._.*/, // thumbnail
39 | // /^\.Spotlight-V100(?:$|\/)/, // directory that might appear on external disk
40 | // /\.Trashes/, // file that might appear on external disk
41 | // /^__MACOSX$/, // resource fork
42 | // # Linux
43 | // /~$/, // backup file
44 | // # Windows
45 | // /^Thumbs\.db$/, // image file cache
46 | // /^ehthumbs\.db$/, // folder config file
47 | // /^Desktop\.ini$/ // stores custom folder attributes
48 |
49 | exports.re = /^npm-debug\.log$|^\..*\.swp$|^\.DS_Store$|^\.AppleDouble$|^\.LSOverride$|^Icon\r$|^\._.*|^\.Spotlight-V100(?:$|\/)|\.Trashes|^__MACOSX$|~$|^Thumbs\.db$|^ehthumbs\.db$|^Desktop\.ini$/;
50 |
51 | exports.is = filename => exports.re.test(filename);
52 |
53 | exports.not = filename => !exports.is(filename);
54 |
--------------------------------------------------------------------------------
/src/editor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link, IndexLink } from 'react-router';
3 | import { connect } from 'react-redux'
4 | import LiveMode from './live-mode';
5 | import DragAndDropReveicer from './files/drag-and-drop-receiver';
6 | import Loader from './lib/loader';
7 | import uuid from 'uuid';
8 | import './editor.css';
9 |
10 | import { addFile } from './data/files';
11 | import { copyFileToProject } from './lib/files';
12 | import midi from './lib/midi';
13 | import scheduler from './lib/scheduler';
14 | import audioGraph from './lib/audio-graph';
15 | import store from './lib/store';
16 | import { readConfig, persistStorePeriodically } from './lib/files';
17 |
18 | class Editor extends Component {
19 |
20 | componentDidMount() {
21 | const { params: { project_id } } = this.props;
22 | const config = readConfig(project_id);
23 |
24 | // initialize the editor
25 | this.props.initEditor(config);
26 |
27 | // initialize everything under the hood
28 | audioGraph.init(store);
29 | scheduler.init(store);
30 | midi.init(store, scheduler.handleManualSchedule, scheduler.scheduleRow);
31 |
32 | // persist project config
33 | persistStorePeriodically(project_id, store);
34 | }
35 |
36 | render() {
37 | const { children, onDrop, params: { project_id } } = this.props;
38 | return (
39 |
40 |
41 |
42 |
43 |
44 | Pads
45 |
46 |
47 | Tracks
48 |
49 |
50 | Files
51 |
52 |
53 | Project
54 |
55 |
56 |
57 | { children }
58 |
59 |
60 |
61 |
62 |
63 | );
64 | }
65 | }
66 |
67 | const mapDispatchToProps = (dispatch, props) => ({
68 | onDrop: (filePath) => {
69 | const id = uuid.v4();
70 | const { params: { project_id } } = props;
71 | copyFileToProject(filePath, id, project_id)
72 | .then((file) => dispatch(addFile(id, file)));
73 | },
74 |
75 | initEditor: (config) => {
76 | dispatch({ type: 'init', state: config });
77 | }
78 | });
79 |
80 | export default connect(null, mapDispatchToProps)(Editor);
81 |
--------------------------------------------------------------------------------
/src/data/clips.js:
--------------------------------------------------------------------------------
1 | /**
2 | * -------------------- ACTION TYPES ----------------------------
3 | */
4 | const CHANGE_CLIP_FIELD = 'jsconf2017/clips/CHANGE_CLIP_FIELD';
5 | export const CREATE_CLIP = 'jsconf2017/clips/CREATE_CLIP';
6 | export const DELETE_CLIP = 'jsconf2017/clips/DELETE_CLIP';
7 |
8 | export const CLIP_TYPE_NONE = 'select a type';
9 | export const CLIP_TYPE_AUDIO_SAMPLE = 'audiosample';
10 | export const CLIP_TYPE_AUDIO_AND_VIDEO = 'audioandvideo';
11 | export const CLIP_TYPE_VIDEO = 'video';
12 | export const CLIP_TYPES = [CLIP_TYPE_NONE, CLIP_TYPE_AUDIO_SAMPLE, CLIP_TYPE_AUDIO_AND_VIDEO, CLIP_TYPE_VIDEO];
13 | export const AUDIO_BEHAVIOR_SCHEDULABLE = 'schedulable';
14 | export const AUDIO_BEHAVIOR_SINGLE = 'single';
15 | export const AUDIO_BEHAVIOR_TYPES = [AUDIO_BEHAVIOR_SCHEDULABLE, AUDIO_BEHAVIOR_SINGLE];
16 |
17 | const BASE_AUDIO_SAMPLE = {
18 | gain: 1,
19 | behavior: AUDIO_BEHAVIOR_SCHEDULABLE,
20 | file: '',
21 | loop: true,
22 | track: 'master'
23 | };
24 |
25 | const BASE_VIDEO_SAMPLE = {
26 | noFilter: false
27 | };
28 |
29 | const BASE_AUDIO_VIDEO_SAMPLE = {
30 | ...BASE_AUDIO_SAMPLE,
31 | videoFile: ''
32 | };
33 |
34 | /**
35 | * -------------------- REDUCER ----------------------------
36 | */
37 | export default function clips(state = {}, action) {
38 | const id = action.id;
39 |
40 | switch (action.type) {
41 | case CHANGE_CLIP_FIELD:
42 | const { field, value } = action;
43 | let clip = state[id];
44 |
45 | // make sure, the clip has the base values for its type
46 | if (clip && field === 'type') {
47 | switch (value) {
48 | case CLIP_TYPE_AUDIO_SAMPLE:
49 | clip = {
50 | ...BASE_AUDIO_SAMPLE,
51 | ...clip
52 | };
53 | break;
54 | case CLIP_TYPE_VIDEO:
55 | clip = {
56 | ...BASE_VIDEO_SAMPLE,
57 | ...clip
58 | };
59 | break;
60 | case CLIP_TYPE_AUDIO_AND_VIDEO:
61 | clip ={
62 | ...BASE_AUDIO_VIDEO_SAMPLE,
63 | ...clip
64 | };
65 | break;
66 | default:
67 | }
68 |
69 | }
70 |
71 | return {
72 | ...state,
73 | [id]: { ...clip, [field]: value }
74 | };
75 | case CREATE_CLIP:
76 | return {
77 | ...state,
78 | [id]: { id }
79 | }
80 | case DELETE_CLIP:
81 | const clipsCopy = { ...state };
82 | delete clipsCopy[id];
83 | return clipsCopy;
84 | default:
85 | return state;
86 | }
87 | }
88 |
89 | /**
90 | * -------------------- ACTION CREATORS ----------------------------
91 | */
92 | export const changeClipField = (id, field, value) => ({ type: CHANGE_CLIP_FIELD, id, field, value });
93 | export const createClip = (x, y, id, padId) => ({ type: CREATE_CLIP, x, y, id, padId });
94 | export const deleteClip = (id, padId) => ({ type: DELETE_CLIP, id, padId });
95 |
--------------------------------------------------------------------------------
/src/tracks/track-editor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux'
3 | import {
4 | changeTrackGain,
5 | changeTrackName,
6 | removeTrack
7 | } from '../data/tracks';
8 |
9 | import './track-editor.css';
10 |
11 | class TrackEditor extends Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | this.state = {
16 | editingName: false
17 | };
18 | }
19 |
20 | shouldComponentUpdate(nextProps, nextState) {
21 | return nextProps.track !== this.props.track
22 | || nextState.editingName !== this.state.editingName;
23 | }
24 |
25 | render() {
26 | const { editingName } = this.state;
27 | const { track: { gain, name } } = this.props;
28 |
29 | return (
30 |
31 | { editingName && (
32 | this.nameInput = input}
34 | type="text"
35 | onChange={this.onChangeText}
36 | onBlur={this.leaveEditMode}
37 | value={name}
38 | placeholder="Name the track..."
39 | className="trackEditor__nameInput"
40 | />
41 | )}
42 |
43 | { !editingName && (
44 |
{name}
48 | )}
49 |
50 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | enterEditMode = () => {
65 | this.setState({ editingName: true }, () => this.nameInput.focus());
66 | }
67 |
68 | leaveEditMode = () => {
69 | this.setState({ editingName: false });
70 | }
71 |
72 | onChangeText = (event) => {
73 | this.props.changeTrackName(event.target.value);
74 | }
75 |
76 | changeGain = (event) => {
77 | const gain = parseFloat(event.target.value, 10);
78 | this.props.changeTrackGain(gain);
79 | }
80 |
81 | removeTrack = () => {
82 | const { trackId, clips } = this.props;
83 |
84 | if (trackId === 'master') {
85 | return alert('You cannot remove the master track');
86 | }
87 |
88 | const associatedClip = Object
89 | .keys(clips)
90 | .find((clipId) => clips[clipId].track === trackId);
91 |
92 | if (associatedClip) {
93 | return alert('The track has still clips associated to it');
94 | }
95 |
96 | this.props.removeTrack();
97 | }
98 | }
99 |
100 | const mapStateToProps = (state, ownProps) => ({
101 | track: state.tracks[ownProps.trackId],
102 | clips: state.clips
103 | });
104 |
105 | const mapDispatchToProps = (dispatch, ownProps) => ({
106 | changeTrackName(name) { dispatch(changeTrackName(ownProps.trackId, name)) },
107 | changeTrackGain(gain) { dispatch(changeTrackGain(ownProps.trackId, gain)) },
108 | removeTrack() { dispatch(removeTrack(ownProps.trackId)) }
109 | });
110 |
111 | export default connect(mapStateToProps, mapDispatchToProps)(TrackEditor);
112 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jsconf-2017",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "autoprefixer": "6.5.1",
7 | "babel-core": "6.17.0",
8 | "babel-eslint": "7.0.0",
9 | "babel-jest": "16.0.0",
10 | "babel-loader": "6.2.5",
11 | "babel-preset-react-app": "^1.0.0",
12 | "case-sensitive-paths-webpack-plugin": "1.1.4",
13 | "chalk": "1.1.3",
14 | "connect-history-api-fallback": "1.3.0",
15 | "cross-spawn": "4.0.2",
16 | "css-loader": "0.25.0",
17 | "detect-port": "1.0.1",
18 | "dotenv": "2.0.0",
19 | "electron-packager": "8.3.0",
20 | "electron-prebuilt": "1.4.10",
21 | "eslint": "3.8.1",
22 | "eslint-config-react-app": "^0.3.0",
23 | "eslint-loader": "1.6.0",
24 | "eslint-plugin-flowtype": "2.21.0",
25 | "eslint-plugin-import": "2.0.1",
26 | "eslint-plugin-jsx-a11y": "2.2.3",
27 | "eslint-plugin-react": "6.4.1",
28 | "express": "4.14.0",
29 | "extract-text-webpack-plugin": "1.0.1",
30 | "file-loader": "0.9.0",
31 | "filesize": "3.3.0",
32 | "find-cache-dir": "0.1.1",
33 | "fs-extra": "0.30.0",
34 | "gzip-size": "3.0.0",
35 | "html-webpack-plugin": "2.24.0",
36 | "http-proxy-middleware": "0.17.2",
37 | "jest": "16.0.2",
38 | "json-loader": "0.5.4",
39 | "npm-run-all": "^4.0.2",
40 | "object-assign": "4.1.0",
41 | "path-exists": "2.1.0",
42 | "portfinder": "1.0.10",
43 | "postcss-cssnext": "2.9.0",
44 | "postcss-import": "9.0.0",
45 | "postcss-loader": "1.0.0",
46 | "promise": "7.1.1",
47 | "react-dev-utils": "^0.3.0",
48 | "recursive-readdir": "2.1.0",
49 | "rimraf": "2.5.4",
50 | "strip-ansi": "3.0.1",
51 | "style-loader": "0.13.1",
52 | "url-loader": "0.5.7",
53 | "webpack": "1.13.2",
54 | "webpack-dev-server": "1.16.2",
55 | "webpack-manifest-plugin": "1.1.0",
56 | "whatwg-fetch": "1.0.0"
57 | },
58 | "dependencies": {
59 | "dat.gui": "^0.6.1",
60 | "dilla": "1.8.3",
61 | "electron-log": "1.2.2",
62 | "event-emitter": "0.3.4",
63 | "file-saver": "1.3.3",
64 | "jszip": "3.1.3",
65 | "lodash": "4.17.2",
66 | "normalize.css": "5.0.0",
67 | "react": "15.4.0",
68 | "react-dom": "15.4.0",
69 | "react-redux": "4.4.6",
70 | "react-router": "^3.0.0",
71 | "recompose": "0.20.2",
72 | "redux": "3.6.0",
73 | "redux-logger": "2.7.4",
74 | "reselect": "2.5.4",
75 | "three": "^0.84.0",
76 | "uuid": "2.0.3",
77 | "web-midi": "2.0.1"
78 | },
79 | "scripts": {
80 | "start": "run-p start:*",
81 | "build": "node scripts/build.js",
82 | "test": "node scripts/test.js --env=jsdom",
83 | "start:server": "node scripts/start.js",
84 | "start:electron": "electron scripts/start-electron.js",
85 | "build-electron": "rm -rf ./build && yarn run build && node scripts/build-electron.js"
86 | },
87 | "jest": {
88 | "moduleFileExtensions": [
89 | "jsx",
90 | "js",
91 | "json"
92 | ],
93 | "moduleNameMapper": {
94 | "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/config/jest/FileStub.js",
95 | "^.+\\.css$": "/config/jest/CSSStub.js"
96 | },
97 | "setupFiles": [
98 | "/config/polyfills.js"
99 | ],
100 | "testPathIgnorePatterns": [
101 | "/(build|docs|node_modules)/"
102 | ],
103 | "testEnvironment": "node"
104 | },
105 | "babel": {
106 | "presets": [
107 | "react-app"
108 | ]
109 | },
110 | "eslintConfig": {
111 | "extends": "react-app"
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/lib/loader.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import path from 'path';
4 | import fs from 'fs';
5 | import log from 'electron-log';
6 | import audioContext from './audio/context';
7 | import { fileLoaded } from '../data/file-loader';
8 | import { getProjectPath } from './files';
9 | import { isAudio, isVideo } from './regular-expressions';
10 |
11 | const loaderContainerStyle = {
12 | position: 'fixed',
13 | left: 0,
14 | right: 0,
15 | bottom: 0
16 | };
17 |
18 | class Loader extends Component {
19 | constructor(props) {
20 | super(props);
21 |
22 | this.state = {};
23 | }
24 |
25 | shouldComponentUpdate(props) {
26 | return Object.keys(props.files).length !== Object.keys(this.props.files).length
27 | || Object.keys(props.fileLoader).length !== Object.keys(this.props.fileLoader).length
28 | }
29 |
30 | componentDidMount() {
31 | const { files } = this.props;
32 |
33 | Object.keys(files).forEach((fileId) => {
34 | this.loadFile(files[fileId], fileId);
35 | });
36 | }
37 |
38 | componentWillUpdate(nextProps) {
39 | const { files, fileLoader } = this.props;
40 | if (nextProps.files !== files) {
41 | Object
42 | .keys(nextProps.files)
43 | .filter((fileId) => {
44 | return !fileLoader[fileId];
45 | })
46 | .forEach((fileId) => {
47 | this.loadFile(nextProps.files[fileId], fileId);
48 | });
49 | }
50 | }
51 |
52 | componentWillReceiveProps(nextProps) {
53 | const { files, fileLoader } = nextProps;
54 | const totalFiles = Object.keys(files).length;
55 | const loadedFiles = Object.keys(fileLoader).length;
56 | const progress = loadedFiles / totalFiles;
57 | const done = progress === 1;
58 | this.setState({
59 | progress, done
60 | });
61 | }
62 |
63 | render() {
64 | const { progress } = this.state;
65 |
66 | const style = {
67 | backgroundColor: '#bada55',
68 | height: '5px',
69 | opacity: progress === 1 ? 0 : 1,
70 | transition: 'width 0.5s ease-in-out, opacity 1s ease-out',
71 | width: `${progress * 100}%`
72 | }
73 |
74 | return (
75 |
78 | );
79 | }
80 |
81 | loadFile(file, id) {
82 | if (isAudio.test(file.location)) {
83 | this.loadAudioFile(file, id);
84 | } else if (isVideo.test(file.location)) {
85 | this.loadVideoFile(file, id);
86 | }
87 | }
88 |
89 | loadAudioFile(file, id) {
90 | const filePath = path.join(getProjectPath(this.props.projectId), file.location);
91 | return new Promise((resolve, reject) => {
92 | resolve(fs.readFileSync(filePath));
93 | })
94 | .then((fileBuffer) => fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength))
95 | .then((arrayBuffer) => {
96 | return new Promise((resolve, reject) => {
97 | audioContext.decodeAudioData(arrayBuffer, resolve, reject);
98 | });
99 | })
100 | .then((audioBuffer) => {
101 | this.props.dispatch(fileLoaded(id, audioBuffer));
102 | }).catch((error) => {
103 | log.error('could not load audio file: ' + filePath);
104 | log.error(error.toString());
105 | });
106 | }
107 |
108 | loadVideoFile(file, id) {
109 | const filePath = path.join(getProjectPath(this.props.projectId), file.location);
110 |
111 | return new Promise((resolve, reject) => {
112 | const data = fs.readFileSync(filePath);
113 | const blob = new Blob([data], {type: 'video/mp4'});
114 | const videoElement = document.createElement('video');
115 | videoElement.preload = 'auto';
116 | videoElement.src = URL.createObjectURL(blob);
117 | videoElement.load();
118 |
119 | // FIXME: should wait for the video::canplaythrough-event (although
120 | // it probably won't matter using localhost-networking)
121 | this.props.dispatch(fileLoaded(id, videoElement));
122 | resolve();
123 | });
124 | }
125 |
126 | }
127 |
128 | const mapStateToProps = (state) => ({
129 | files: state.files,
130 | fileLoader: state.fileLoader
131 | });
132 |
133 | export default connect(mapStateToProps)(Loader);
134 |
--------------------------------------------------------------------------------
/src/lib/files.js:
--------------------------------------------------------------------------------
1 | import { remote } from 'electron';
2 | import log from 'electron-log';
3 | import _ from 'lodash';
4 | import fs from 'fs';
5 | import path from 'path';
6 | import JSZip from 'jszip';
7 | import { saveAs } from 'file-saver';
8 |
9 | let _userDataDirectory = remote.getGlobal('userDataDirectory');
10 | let _userProjectDirectory = path.join(_userDataDirectory, 'projects');
11 |
12 | try {
13 | fs.mkdirSync(_userDataDirectory);
14 | fs.mkdirSync(_userProjectDirectory);
15 | log.info('Created user data directory');
16 | } catch(e) {
17 | log.info('User directory already exists');
18 | }
19 |
20 | export const userDataDirectory = _userDataDirectory;
21 | export const userProjectDirectory = _userProjectDirectory;
22 |
23 | export function getProjectPath (projectId) {
24 | return path.join(userProjectDirectory, projectId);
25 | }
26 |
27 | export function getConfigPath (projectId) {
28 | return path.join(getProjectPath(projectId), 'project.json');
29 | }
30 |
31 | export function readConfig (projectId) {
32 | const configPath = getConfigPath(projectId);
33 | return JSON.parse(fs.readFileSync(configPath).toString());
34 | }
35 |
36 | export function persistStorePeriodically (projectId, store) {
37 | store.subscribe(_.throttle(() => {
38 | const filteredConfig = _.omit(store.getState(), ['scheduler', 'fileLoader', 'controllers']);
39 | saveAsProjectConfig(projectId, filteredConfig);
40 | }, 5000));
41 | }
42 |
43 | export function saveAsProjectConfig (projectId, projectConfig) {
44 | const configPath = getConfigPath(projectId);
45 | const configString = JSON.stringify(projectConfig, null, 2);
46 | fs.writeFileSync(configPath, configString);
47 | }
48 |
49 | export function saveProjectAsZip (projectId) {
50 | const projectPath = getProjectPath(projectId);
51 | const files = fs.readdirSync(projectPath);
52 | const zip = new JSZip();
53 |
54 | const readFileOperations = files.map((file) =>
55 | readFile(path.join(projectPath, file))
56 | .then((data) => zip.file(file, data))
57 | );
58 |
59 | Promise
60 | .all(readFileOperations)
61 | .then(() => zip.generateAsync({ type: 'blob'}))
62 | .then((blob) => saveAs(blob, `${projectId}.zip`));
63 | }
64 |
65 | function readFile (filePath) {
66 | return new Promise((resolve, reject) => {
67 | fs.readFile(filePath, function(err, data){
68 | if (err) {
69 | reject(err);
70 | } else {
71 | resolve(data);
72 | }
73 | });
74 | });
75 | }
76 |
77 | export function importProjectFromZip (zipPath) {
78 | const projectName = zipPath.split(path.sep).pop().replace('.zip', '');
79 | let projectPath = getProjectPath(projectName);
80 | let counter = 1;
81 |
82 | // make sure not to overwrite a previous project with the same name
83 | while (fs.existsSync(projectPath)) {
84 | projectPath = `${getProjectPath(projectName)}-${counter++}`;
85 | }
86 |
87 | fs.mkdirSync(projectPath);
88 |
89 | const zip = new JSZip();
90 | const fileContent = fs.readFileSync(zipPath);
91 | return zip.loadAsync(fileContent)
92 | .then(() => {
93 | const promises = [];
94 | zip.forEach((filePath, file) => {
95 | promises.push(
96 | new Promise((resolve) => {
97 | file
98 | .nodeStream()
99 | .pipe(fs.createWriteStream(path.join(projectPath, filePath)))
100 | .on('finish', resolve);
101 | })
102 | );
103 | });
104 | return Promise.all(promises);
105 | });
106 | }
107 |
108 | export function copyFileToProject (filePath, newFileId, projectId) {
109 | return readFile(filePath)
110 | .then((fileData) => new Promise((resolve, reject) => {
111 | const fileName = filePath.split(path.sep).pop();
112 | const newFileName = `${newFileId}-${fileName}`;
113 | const newFilePath = path.join(getProjectPath(projectId), newFileName);
114 | fs.writeFileSync(newFilePath, fileData);
115 | resolve({name: fileName, location: newFileName});
116 | }))
117 | }
118 |
119 | export function deleteFile (projectId, fileLocation) {
120 | const filePath = path.join(getProjectPath(projectId), fileLocation);
121 | fs.unlink(filePath);
122 | }
123 |
--------------------------------------------------------------------------------
/src/data/pads.js:
--------------------------------------------------------------------------------
1 | import uuid from 'uuid';
2 | import { CREATE_CLIP, DELETE_CLIP } from './clips';
3 |
4 | /**
5 | * -------------------- ACTION TYPES ----------------------------
6 | */
7 | const SELECT_CLIP = 'jsconf2017/pads/SELECT_CLIP';
8 | const CREATE_PAD = 'jsconf2017/pads/CREATE_PAD';
9 | const REMOVE_PAD = 'jsconf2017/pads/REMOVE_PAD';
10 |
11 | /**
12 | * -------------------- REDUCER ----------------------------
13 | */
14 | export default function pads(state = {}, action) {
15 | const { padId } = action;
16 | switch (action.type) {
17 | case CREATE_CLIP:
18 | case SELECT_CLIP:
19 | case DELETE_CLIP:
20 | const pad = state[padId];
21 | return {
22 | ...state,
23 | [padId]: padReducer(pad, action)
24 | };
25 | case CREATE_PAD:
26 | const id = uuid.v4();
27 | return {
28 | ...state,
29 | [id]: generateEmptyPad(id)
30 | };
31 | case REMOVE_PAD:
32 | const copiedPads = { ...state };
33 | delete copiedPads[padId];
34 | return copiedPads;
35 | default:
36 | return state;
37 | }
38 | }
39 |
40 | function padReducer(state, action) {
41 | switch (action.type) {
42 | case CREATE_CLIP: {
43 | const { clips } = state;
44 | const { x, y, id } = action;
45 |
46 | return {
47 | ...state,
48 | clips: changeValueAtPoint({
49 | array2d: clips,
50 | value: id,
51 | x, y
52 | })
53 | };
54 | }
55 |
56 | case SELECT_CLIP: {
57 | return {
58 | ...state,
59 | selectedClipId: action.selectedClipId
60 | };
61 | }
62 |
63 | case DELETE_CLIP: {
64 | const { clips } = state;
65 | const { id } = action;
66 | const coordinates = findIn2dArray(clips, id);
67 |
68 | if (!coordinates) { return state; }
69 |
70 | const { x, y } = coordinates;
71 |
72 | return {
73 | ...state,
74 | clips: changeValueAtPoint({
75 | array2d: clips,
76 | value: null,
77 | x, y
78 | })
79 | };
80 | }
81 | default:
82 | return state;
83 | }
84 | }
85 |
86 | /**
87 | * -------------------- ACTION CREATORS ----------------------------
88 | */
89 | export const selectClip = (selectedClipId, padId) => ({ type: SELECT_CLIP, selectedClipId, padId });
90 |
91 | export const createPad = () => ({ type: CREATE_PAD });
92 |
93 | export const removePad = (padId) => ({ type: REMOVE_PAD, padId });
94 |
95 | /**
96 | * -------------------- HELPERS ----------------------------
97 | */
98 | function findIn2dArray(array2d, value) {
99 | for (var y = 0; y < 8; y++) {
100 | for (var x = 0; x < array2d.length; x++) {
101 | if (array2d[y][x] === value) {
102 | return { x, y };
103 | }
104 | }
105 | }
106 | return null;
107 | }
108 |
109 | function changeValueAtPoint({x, y, value, array2d}){
110 | const newRow = [
111 | ...array2d[y].slice(0, x),
112 | value,
113 | ...array2d[y].slice(x + 1)
114 | ];
115 |
116 | return array2d.slice(0, y)
117 | .concat([newRow])
118 | .concat(array2d.slice(y + 1));
119 | }
120 |
121 | function generateEmptyPad(id) {
122 | return {
123 | id,
124 | clips: [
125 | [
126 | null,
127 | null,
128 | null,
129 | null,
130 | null,
131 | null,
132 | null,
133 | null
134 | ],
135 | [
136 | null,
137 | null,
138 | null,
139 | null,
140 | null,
141 | null,
142 | null,
143 | null
144 | ],
145 | [
146 | null,
147 | null,
148 | null,
149 | null,
150 | null,
151 | null,
152 | null,
153 | null
154 | ],
155 | [
156 | null,
157 | null,
158 | null,
159 | null,
160 | null,
161 | null,
162 | null,
163 | null
164 | ],
165 | [
166 | null,
167 | null,
168 | null,
169 | null,
170 | null,
171 | null,
172 | null,
173 | null
174 | ],
175 | [
176 | null,
177 | null,
178 | null,
179 | null,
180 | null,
181 | null,
182 | null,
183 | null
184 | ],
185 | [
186 | null,
187 | null,
188 | null,
189 | null,
190 | null,
191 | null,
192 | null,
193 | null
194 | ],
195 | [
196 | null,
197 | null,
198 | null,
199 | null,
200 | null,
201 | null,
202 | null,
203 | null
204 | ]
205 | ],
206 | selectedClipId: null
207 | };
208 | }
209 |
--------------------------------------------------------------------------------
/src/projects.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import fs from 'fs';
3 | import path from 'path';
4 | import { Link } from 'react-router';
5 | import log from 'electron-log';
6 | import { shell } from 'electron';
7 | import { not } from './lib/junk';
8 | import { userProjectDirectory, importProjectFromZip } from './lib/files';
9 | import blankProject from './data/blank_project.json';
10 | const { dialog } = require('electron').remote;
11 |
12 | import './projects.css'
13 |
14 | export default class Projects extends React.Component {
15 | constructor(props) {
16 | super(props);
17 |
18 | this.state = {
19 | projects: null,
20 | importing: false,
21 | showNewForm: false
22 | };
23 | }
24 |
25 | componentDidMount() {
26 | this.syncDirectories();
27 | }
28 |
29 | render() {
30 | const { projects, showNewForm, importing } = this.state;
31 | const notReady = !projects;
32 | const noProjects = !notReady && projects.length === 0;
33 | const hasProjects = !!projects && projects.length > 0;
34 | const importButtonLabel = importing ? 'Importing...' : 'Import project';
35 |
36 | return (
37 |
38 | { notReady && (
39 |
{'Scanning for projects...'}
40 | )}
41 |
42 | { noProjects && (
43 |
{'No projects yet'}
44 | )}
45 |
46 | { hasProjects && (
47 |
48 | { projects.map((projectId, index) =>
49 | -
50 | {projectId}
51 |
52 | )}
53 |
54 | )}
55 |
56 |
57 | { showNewForm && (
58 |
67 | )}
68 |
69 |
75 |
82 |
88 |
89 |
90 | );
91 | }
92 |
93 | showNewProjectForm = () => {
94 | this.setState({
95 | showNewForm: true
96 | });
97 | }
98 |
99 | createNewProject = (event) => {
100 | event.preventDefault();
101 | const projectName = this.input.value;
102 | const projectPath = path.join(userProjectDirectory, projectName);
103 | const projectExists = fs.existsSync(projectPath);
104 |
105 | if (projectName && !projectExists) {
106 | const projectFilePath = path.join(projectPath, 'project.json');
107 | fs.mkdirSync(projectPath);
108 | fs.writeFileSync(projectFilePath, JSON.stringify(blankProject, null, 2));
109 | this.syncDirectories();
110 | this.setState({
111 | showNewForm: false
112 | });
113 | }
114 | }
115 |
116 | importProject = (event) => {
117 | event.preventDefault();
118 | const files = dialog.showOpenDialog({
119 | properties: ['openFile'],
120 | filters: [
121 | { name: 'Zip files', extensions: ['zip'] }
122 | ]
123 | });
124 | if (files) {
125 | const file = files.pop();
126 | this.setState({ importing: true });
127 | importProjectFromZip(file)
128 | .then(() => this.setState({ importing: false }))
129 | .then(() => this.syncDirectories());
130 | }
131 | }
132 |
133 | syncDirectories() {
134 | fs.readdir(userProjectDirectory, (err, dirs) => {
135 | if (err) {
136 | this.createProjectsDirectory();
137 | this.setState({ projects: [] });
138 | } else {
139 | this.setState({ projects: dirs.filter(not) })
140 | }
141 | });
142 | }
143 |
144 | createProjectsDirectory() {
145 | fs.mkdir(userProjectDirectory, (err) => {
146 | if (err) {
147 | const message = 'Could not create projects directory: ' + err.toString();
148 | log.error(message);
149 | alert(message);
150 | }
151 | });
152 | }
153 |
154 | openProjectsFolder = () => {
155 | shell.openItem(userProjectDirectory);
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/scripts/start-electron.js:
--------------------------------------------------------------------------------
1 | // prevent any non-us locale: https://github.com/electron/electron/issues/8825
2 | process.env.LC_ALL = 'en_US';
3 |
4 | const { app, BrowserWindow } = require('electron');
5 | const portfinder = require('portfinder');
6 | const express = require('express');
7 | const log = require('electron-log');
8 | const path = require('path');
9 |
10 | let isProduction = false;
11 | try {
12 | require('./prod.json');
13 | isProduction = true;
14 | log.info('running in prod mode');
15 | } catch (e) {
16 | log.info('running in dev mode');
17 | }
18 |
19 | global.userDataDirectory = path.join(userData(), 'jsconf-2017');
20 | log.info('user data dir: ' + global.userDataDirectory);
21 |
22 | // Keep a global reference of the window object, if you don't, the window will
23 | // be closed automatically when the JavaScript object is garbage collected.
24 | let win;
25 |
26 | function createWindow () {
27 | // Create the browser window.
28 | win = new BrowserWindow({
29 | width: 960,
30 | height: 800,
31 | title: 'JSConf 2017'
32 | });
33 |
34 | if (isProduction) {
35 | log.info('trying to find a port');
36 | portfinder.getPort(function (err, port) {
37 | if (err) {
38 | log.error('Could not find a port');
39 | log.error(err.toString());
40 | }
41 | const expressApp = express();
42 | expressApp.use(express.static(__dirname));
43 | log.info('static path ' + __dirname);
44 | expressApp.use((_, res) => res.redirect('/'));
45 |
46 | expressApp.listen(port, function (a,b) {
47 |
48 | log.info('JSConf 2017 is running on port', port);
49 |
50 | // and load the index.html of the app.
51 | win.loadURL('http://localhost:' + port);
52 | });
53 | });
54 | } else {
55 | win.loadURL('http://localhost:3000');
56 | }
57 |
58 | // Emitted when the window is closed.
59 | win.on('closed', () => {
60 | // Dereference the window object, usually you would store windows
61 | // in an array if your app supports multi windows, this is the time
62 | // when you should delete the corresponding element.
63 | win = null
64 | })
65 | }
66 |
67 | // This method will be called when Electron has finished
68 | // initialization and is ready to create browser windows.
69 | // Some APIs can only be used after this event occurs.
70 | app.on('ready', createWindow);
71 |
72 | // Quit when all windows are closed.
73 | app.on('window-all-closed', () => {
74 | // On macOS it is common for applications and their menu bar
75 | // to stay active until the user quits explicitly with Cmd + Q
76 | if (process.platform !== 'darwin') {
77 | app.quit()
78 | }
79 | });
80 |
81 | app.on('activate', () => {
82 | // On macOS it's common to re-create a window in the app when the
83 | // dock icon is clicked and there are no other windows open.
84 | if (win === null) {
85 | createWindow()
86 | }
87 | });
88 |
89 | /**
90 | * Had to import this helper from https://github.com/MrJohz/appdirectory and change it because of some issues on windows
91 | * Thei licence applies
92 | Copyright (c) 2014 Johz jonathan.frere@gmail.com
93 |
94 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
95 |
96 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
97 |
98 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
99 | */
100 | function userData (roaming, platform) {
101 | var dataPath;
102 | platform = platform || process.platform
103 | if (platform === "darwin") {
104 | dataPath = path.join(process.env.HOME, 'Library', 'Application Support')
105 | } else if (platform === "win32") {
106 | var sysVariable
107 | if (roaming) {
108 | sysVariable = "APPDATA"
109 | } else {
110 | sysVariable = "LOCALAPPDATA" // Note, on WinXP, LOCALAPPDATA doesn't exist, catch this later
111 | }
112 | dataPath = path.join(process.env[sysVariable] || process.env.APPDATA /*catch for XP*/)
113 | } else {
114 | if (process.env.XDG_DATA_HOME) {
115 | dataPath = path.join(process.env.XDG_DATA_HOME)
116 | } else {
117 | dataPath = path.join(process.env.HOME, ".local", "share")
118 | }
119 | }
120 | return dataPath
121 | }
122 |
--------------------------------------------------------------------------------
/src/lib/video/renderer.js:
--------------------------------------------------------------------------------
1 | import {
2 | Color,
3 | PerspectiveCamera,
4 | PlaneBufferGeometry,
5 | Points,
6 | Scene,
7 | ShaderMaterial,
8 | Texture,
9 | Vector3,
10 | WebGLRenderer
11 | } from 'three';
12 |
13 | import dotMatrixShader from './dot-matrix-shader.js';
14 |
15 | export default class VideoRenderer {
16 | constructor() {
17 | this.animationLoopRunning = false;
18 | this.videos = [];
19 |
20 | this.resolution = '160x90';
21 | this.geometryCache = {};
22 |
23 | this.initTexture();
24 | this.initRenderer();
25 |
26 | this.animationLoopTick = this.animationLoopTick.bind(this);
27 | }
28 |
29 | setVideos(videos = []) {
30 | this.videos = videos;
31 |
32 | if (this.videos.length === 0) {
33 | this.stop();
34 | } else {
35 | this.start();
36 | }
37 | }
38 |
39 | setRenderParams(renderParams) {
40 | this.renderer.setClearColor(renderParams.backgroundColor, 1);
41 | this.setResolution(renderParams.resolution);
42 | this.updateUniforms({
43 | pointColor: renderParams.foregroundColor,
44 | size: renderParams.pointSize,
45 | lumMin: renderParams.luminanceMin,
46 | lumMax: renderParams.luminanceMax,
47 | r0: renderParams.r0
48 | });
49 | }
50 |
51 | getDomElement() {
52 | return this.renderer.domElement;
53 | }
54 |
55 | /**
56 | * @private
57 | */
58 | start() {
59 | if (this.animationLoopRunning) { return; }
60 |
61 | this.animationLoopRunning = true;
62 | this.nextFrameId = requestAnimationFrame(this.animationLoopTick)
63 | }
64 |
65 | /**
66 | * @private
67 | */
68 | stop() {
69 | cancelAnimationFrame(this.nextFrameId);
70 | this.animationLoopRunning = false;
71 | }
72 |
73 | /**
74 | * @private
75 | */
76 | animationLoopTick() {
77 | const activeVideos = this.videos;
78 | const textureCanvas = this.texture.image;
79 |
80 | const n = activeVideos.length;
81 | const ctx = textureCanvas.getContext('2d');
82 | const w = textureCanvas.width;
83 | const h = textureCanvas.height;
84 |
85 | if (n === 0) {
86 | ctx.clearRect(0, 0, w, h);
87 | } else {
88 | activeVideos.forEach((video, index) => {
89 | const dstSliceWidth = w / n;
90 |
91 | // FIXME: only use a portion of the source-video to prevent distortion
92 | ctx.drawImage(video, index * dstSliceWidth, 0, dstSliceWidth, h);
93 | });
94 | }
95 |
96 | this.render();
97 |
98 | if (this.animationLoopRunning) {
99 | this.nextFrameId = requestAnimationFrame(this.animationLoopTick);
100 | }
101 | }
102 |
103 | /**
104 | * @private
105 | */
106 | render() { /* stub: will be overwritten in initRenderer */ }
107 |
108 | /**
109 | * @private
110 | */
111 | updateUniforms(values) {
112 | const { uniforms } = this.points.material;
113 |
114 | Object.keys(values).forEach(name => {
115 | const uniform = uniforms[name];
116 | if (!uniform) { return; }
117 |
118 | if (uniform.value instanceof Color) {
119 | uniform.value.set(values[name]);
120 | } else {
121 | uniform.value = values[name];
122 | }
123 | });
124 | }
125 |
126 | /**
127 | * @private
128 | */
129 | initTexture() {
130 | const canvas = document.createElement('canvas');
131 |
132 | canvas.width = 512;
133 | canvas.height = 256;
134 |
135 | // FIXME: configure texture
136 | this.texture = new Texture(canvas);
137 | }
138 |
139 | /**
140 | * @private
141 | */
142 | setResolution(resolution) {
143 | if (resolution === this.resolution) { return; }
144 |
145 | this.points.geometry = this.getGeometry(resolution);
146 | this.resolution = resolution;
147 | }
148 |
149 | /**
150 | * @private
151 | */
152 | getGeometry(resolution) {
153 | if (this.geometryCache[resolution]) {
154 | return this.geometryCache[resolution];
155 | }
156 |
157 | const [xRes, yRes] = resolution.split('x');
158 | const geometry = new PlaneBufferGeometry(16, 9, xRes, yRes);
159 |
160 | this.geometryCache[resolution] = geometry;
161 |
162 | return geometry;
163 | }
164 |
165 | /**
166 | * @private
167 | */
168 | initRenderer() {
169 | const renderer = new WebGLRenderer({ alpha: true, antialias: true });
170 | renderer.setSize(window.innerWidth, window.innerHeight);
171 | renderer.setClearColor(0xe10079);
172 |
173 | const scene = new Scene();
174 | // FIXME: maybe we should just use an orthographic camera here?
175 | const camera = new PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000);
176 | camera.position.set(0, 0, 6.3);
177 | camera.lookAt(new Vector3(0, 0, 0));
178 |
179 | const material = new ShaderMaterial(dotMatrixShader);
180 | material.uniforms.texture.value = this.texture;
181 | material.uniforms.scale.value = window.innerHeight / 2;
182 |
183 | // FIXME: resolution and size might be configurable
184 | const points = new Points(
185 | this.getGeometry(this.resolution),
186 | material);
187 |
188 | this.points = points;
189 | scene.add(points);
190 |
191 | // bind events
192 | window.addEventListener('resize', () => {
193 | renderer.setSize(window.innerWidth, window.innerHeight);
194 | camera.aspect = window.innerWidth / window.innerHeight;
195 | camera.updateProjectionMatrix();
196 | material.uniforms.scale.value = window.innerHeight / 2;
197 | });
198 |
199 | // finalize
200 | this.renderer = renderer;
201 | this.render = () => {
202 | this.texture.needsUpdate = true;
203 |
204 | renderer.render(scene, camera);
205 | }
206 | }
207 | }
--------------------------------------------------------------------------------
/src/lib/midi.js:
--------------------------------------------------------------------------------
1 | import midi from '../vendor/web-midi';
2 |
3 | import {
4 | addController,
5 | mapControllerToPad,
6 | removeController
7 | } from '../data/controllers';
8 |
9 | import {
10 | isPlaying
11 | } from '../data/scheduler';
12 |
13 | import {
14 | setRenderparam
15 | } from '../data/video-renderer';
16 |
17 | const COLOR_CODES = {
18 | OFF: 12,
19 | RED: 15,
20 | AMBER: 63,
21 | YELLOW: 62,
22 | GREEN: 60
23 | };
24 |
25 | class Midi {
26 | init(storeObject, clipHandler = () => {}, scheduleRow = () => {}) {
27 | this.store = storeObject;
28 | this.clipHandler = clipHandler;
29 | this.scheduleRow = scheduleRow;
30 | this.previousClips = null;
31 | this.previouseScheduler = null;
32 | this.previousControllers = {};
33 |
34 | midi.watchPortNames(this.setControllers);
35 |
36 | storeObject.subscribe(() => {
37 | const { clips, scheduler, controllers } = storeObject.getState();
38 | if (clips !== this.previousClips
39 | || scheduler !== this.previouseScheduler
40 | || controllers !== this.previousControllers) {
41 | this.previousClips = clips;
42 | this.previouseScheduler = scheduler;
43 | this.previousControllers = controllers;
44 | this.updateControllers();
45 | }
46 | });
47 | }
48 |
49 | setControllers = (controllers) => {
50 | const { store } = this;
51 | // remove controllers that were there previously
52 | Object.keys(this.previousControllers).forEach((id) => {
53 | this.previousControllers[id].controller.close();
54 | store.dispatch(removeController(id));
55 | });
56 |
57 | const currentControllers = store.getState().controllers;
58 | controllers.forEach((id, index) => {
59 | if (!currentControllers[id]) {
60 | const controller = midi(id, {});
61 | controller.on('data', ([type, key, data]) => {
62 | if (id === 'Launch Control') {
63 | // knobs
64 | if (type === 184 && key > 20 && key < 49) {
65 | // const y = Math.floor(key / 20) - 1;
66 | const x = (key % 20) - 1;
67 | const value = data / 127;
68 | switch (x) {
69 | case 0:
70 | return store.dispatch(setRenderparam('pointSize', value * 0.5));
71 | case 1:
72 | return store.dispatch(setRenderparam('luminanceMin', value));
73 | case 2:
74 | return store.dispatch(setRenderparam('luminanceMax', value));
75 | case 3:
76 | return store.dispatch(setRenderparam('r0', value));
77 | default:
78 | return;
79 | }
80 | }
81 | } else {
82 | const state = this.store.getState();
83 | const down = data > 0;
84 | const y = Math.floor(key / 16);
85 | const x = key % 16;
86 | if (down) {
87 | const padId = state.controllers[id].pad;
88 | const pad = state.pads[padId];
89 |
90 | // this controller is not controlling a clip
91 | if (!padId || !pad) { return; }
92 |
93 | if (x < 8 && y < 8) {
94 | const clipId = pad.clips[y][x];
95 | this.clipHandler(clipId, pad, x, y);
96 | } else if (type === 176 && (key >= 104 || key <= 111)) {
97 | this.mapControllerToPadIndex(id, key - 104);
98 | } else if (type === 144 && (key < 105 || key > 111)) {
99 | this.scheduleRow(pad, y);
100 | }
101 | }
102 | }
103 | });
104 | // map controller to matching pad at index if possible
105 | const padKeys = Object.keys(store.getState().pads);
106 | const initialPad = index < padKeys.length ? padKeys[index] : padKeys[0];
107 | store.dispatch(addController(id, controller, initialPad));
108 | }
109 | });
110 | }
111 |
112 | mapControllerToPadIndex(controller, padIndex) {
113 | const { pads } = this.store.getState();
114 | const padId = Object.keys(pads)[padIndex];
115 | // `padIndex` could be any index even if no pad assigned to it
116 | if (padId) {
117 | this.store.dispatch(mapControllerToPad(controller, padId));
118 | }
119 | }
120 |
121 | updateControllers = () => {
122 | const { clips, pads, controllers, scheduler } = this.store.getState();
123 |
124 | Object.keys(controllers).forEach((controllerId, index) => {
125 | const controllerConfig = controllers[controllerId];
126 | const { controller } = controllerConfig;
127 | const pad = pads[controllerConfig.pad];
128 |
129 | // write the clip status
130 | for (var y = 0; y < 8; y++) {
131 | for (var x = 0; x < 8; x++) {
132 | const key = y * 16 + x;
133 | if (pad) {
134 | const padClip = pad.clips[y][x];
135 | const clip = clips[padClip];
136 |
137 | if (clip) {
138 | const file = isPlaying(scheduler.playing, clip);
139 |
140 | if (scheduler.scheduled[padClip]) {
141 | controller.write([144, key, COLOR_CODES.YELLOW]);
142 | } else if(scheduler.toStop[padClip]) {
143 | controller.write([144, key, COLOR_CODES.RED]);
144 | } else if (file && file.clipId === padClip) {
145 | controller.write([144, key, COLOR_CODES.AMBER]);
146 | } else {
147 | controller.write([144, key, COLOR_CODES.GREEN]);
148 | }
149 | } else {
150 | controller.write([144, key, COLOR_CODES.OFF]);
151 | }
152 | } else {
153 | controller.write([144, key, COLOR_CODES.OFF]);
154 | }
155 | }
156 | }
157 |
158 | // write the pad status
159 | const padKeys = Object.keys(pads);
160 | const activePadId = pad && padKeys.indexOf(controllerConfig.pad);
161 | for (var i = 0; i < 8; i++) {
162 | controller.write([
163 | 176,
164 | 104 + i,
165 | activePadId === i ? COLOR_CODES.GREEN : i < padKeys.length ? COLOR_CODES.YELLOW : COLOR_CODES.OFF
166 | ]);
167 | }
168 | });
169 | }
170 | }
171 |
172 | const instance = new Midi();
173 | export default instance;
174 |
--------------------------------------------------------------------------------
/src/clip/clip-editor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import {
4 | CLIP_TYPE_NONE,
5 | CLIP_TYPE_AUDIO_SAMPLE,
6 | CLIP_TYPE_AUDIO_AND_VIDEO,
7 | CLIP_TYPE_VIDEO,
8 | CLIP_TYPES,
9 | AUDIO_BEHAVIOR_TYPES,
10 | changeClipField,
11 | deleteClip
12 | } from '../data/clips';
13 | import { getAudioFiles, getVideoFiles } from '../data/files';
14 |
15 | import '../styles/forms.css';
16 | import './clip-editor.css';
17 |
18 | class ClipEditor extends Component {
19 | shouldComponentUpdate(newProps){
20 | return newProps.clip !== this.props.clip
21 | || newProps.tracks !== this.props.tracks;
22 | }
23 |
24 | render() {
25 | const currentType = this.props.clip.type || CLIP_TYPE_NONE;
26 | const { track, tracks } = this.props;
27 |
28 | return (
29 |
30 |
41 | { this.renderForm() }
42 |
54 |
55 |
56 |
57 | );
58 | }
59 |
60 | renderForm() {
61 | const { clip, audioFiles, videoFiles } = this.props;
62 | const { behavior, file, videoFile, gain, loop, noFilter, type } = clip;
63 | const hasAudio = type === CLIP_TYPE_AUDIO_AND_VIDEO || type === CLIP_TYPE_AUDIO_SAMPLE;
64 | const hasVideo = type === CLIP_TYPE_AUDIO_AND_VIDEO || type === CLIP_TYPE_VIDEO;
65 | const isVideo = type === CLIP_TYPE_VIDEO;
66 |
67 | return (
68 |
138 | );
139 | }
140 |
141 | changeBehavior = (event) => {
142 | this.props.changeClipField('behavior', event.target.value);
143 | }
144 |
145 | changeType = (event) => {
146 | this.props.changeClipField('type', event.target.value);
147 | }
148 |
149 | changeSchedulable = () => {
150 | this.props.changeClipField('schedulable', !this.props.clip.schedulable);
151 | }
152 |
153 | changeLoop = (event) => {
154 | this.props.changeClipField('loop', event.target.checked);
155 | }
156 |
157 | changeNoFilter = (event) => {
158 | this.props.changeClipField('noFilter', event.target.checked);
159 | }
160 |
161 | changeGain = (event) => {
162 | this.props.changeClipField('gain', parseFloat(event.target.value));
163 | }
164 |
165 | changeFile = (event) => {
166 | this.props.changeClipField('file', event.target.value);
167 | }
168 |
169 | changeVideoFile = (event) => {
170 | this.props.changeClipField('videoFile', event.target.value);
171 | }
172 |
173 | changeTrack = (event) => {
174 | this.props.changeClipField('track', event.target.value);
175 | }
176 |
177 | deleteClip = (event) => {
178 | event.preventDefault();
179 | this.props.deleteClip(this.props.clip.id);
180 | }
181 |
182 | }
183 |
184 | const mapStateToProps = (state) => ({
185 | tracks: state.tracks,
186 | audioFiles: getAudioFiles(state),
187 | videoFiles: getVideoFiles(state)
188 | });
189 |
190 | const mapDispatchToProps = (dispatch, { clip, padId }) => ({
191 | changeClipField(field, value) {
192 | return dispatch(changeClipField(clip.id, field, value));
193 | },
194 |
195 | deleteClip(id) {
196 | dispatch(deleteClip(id, padId));
197 | }
198 | })
199 |
200 | export default connect(mapStateToProps, mapDispatchToProps)(ClipEditor);
201 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | // Do this as the first thing so that any code reading it knows the right env.
2 | process.env.NODE_ENV = 'production';
3 |
4 | // Load environment variables from .env file. Suppress warnings using silent
5 | // if this file is missing. dotenv will never modify any environment variables
6 | // that have already been set.
7 | // https://github.com/motdotla/dotenv
8 | require('dotenv').config({silent: true});
9 |
10 | var chalk = require('chalk');
11 | var fs = require('fs-extra');
12 | var path = require('path');
13 | var filesize = require('filesize');
14 | var gzipSize = require('gzip-size').sync;
15 | var rimrafSync = require('rimraf').sync;
16 | var webpack = require('webpack');
17 | var config = require('../config/webpack.config.prod');
18 | var paths = require('../config/paths');
19 | var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
20 | var recursive = require('recursive-readdir');
21 | var stripAnsi = require('strip-ansi');
22 |
23 | // Warn and crash if required files are missing
24 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
25 | process.exit(1);
26 | }
27 |
28 | // Input: /User/dan/app/build/static/js/main.82be8.js
29 | // Output: /static/js/main.js
30 | function removeFileNameHash(fileName) {
31 | return fileName
32 | .replace(paths.appBuild, '')
33 | .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3);
34 | }
35 |
36 | // Input: 1024, 2048
37 | // Output: "(+1 KB)"
38 | function getDifferenceLabel(currentSize, previousSize) {
39 | var FIFTY_KILOBYTES = 1024 * 50;
40 | var difference = currentSize - previousSize;
41 | var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0;
42 | if (difference >= FIFTY_KILOBYTES) {
43 | return chalk.red('+' + fileSize);
44 | } else if (difference < FIFTY_KILOBYTES && difference > 0) {
45 | return chalk.yellow('+' + fileSize);
46 | } else if (difference < 0) {
47 | return chalk.green(fileSize);
48 | } else {
49 | return '';
50 | }
51 | }
52 |
53 | // First, read the current file sizes in build directory.
54 | // This lets us display how much they changed later.
55 | recursive(paths.appBuild, (err, fileNames) => {
56 | var previousSizeMap = (fileNames || [])
57 | .filter(fileName => /\.(js|css)$/.test(fileName))
58 | .reduce((memo, fileName) => {
59 | var contents = fs.readFileSync(fileName);
60 | var key = removeFileNameHash(fileName);
61 | memo[key] = gzipSize(contents);
62 | return memo;
63 | }, {});
64 |
65 | // Remove all content but keep the directory so that
66 | // if you're in it, you don't end up in Trash
67 | rimrafSync(paths.appBuild + '/*');
68 |
69 | // Start the webpack build
70 | build(previousSizeMap);
71 |
72 | // Merge with the public folder
73 | copyPublicFolder();
74 | });
75 |
76 | // Print a detailed summary of build files.
77 | function printFileSizes(stats, previousSizeMap) {
78 | var assets = stats.toJson().assets
79 | .filter(asset => /\.(js|css)$/.test(asset.name))
80 | .map(asset => {
81 | var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name);
82 | var size = gzipSize(fileContents);
83 | var previousSize = previousSizeMap[removeFileNameHash(asset.name)];
84 | var difference = getDifferenceLabel(size, previousSize);
85 | return {
86 | folder: path.join('build', path.dirname(asset.name)),
87 | name: path.basename(asset.name),
88 | size: size,
89 | sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '')
90 | };
91 | });
92 | assets.sort((a, b) => b.size - a.size);
93 | var longestSizeLabelLength = Math.max.apply(null,
94 | assets.map(a => stripAnsi(a.sizeLabel).length)
95 | );
96 | assets.forEach(asset => {
97 | var sizeLabel = asset.sizeLabel;
98 | var sizeLength = stripAnsi(sizeLabel).length;
99 | if (sizeLength < longestSizeLabelLength) {
100 | var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength);
101 | sizeLabel += rightPadding;
102 | }
103 | console.log(
104 | ' ' + sizeLabel +
105 | ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name)
106 | );
107 | });
108 | }
109 |
110 | // Print out errors
111 | function printErrors(summary, errors) {
112 | console.log(chalk.red(summary));
113 | console.log();
114 | errors.forEach(err => {
115 | console.log(err.message || err);
116 | console.log();
117 | });
118 | }
119 |
120 | // Create the production build and print the deployment instructions.
121 | function build(previousSizeMap) {
122 | console.log('Creating an optimized production build...');
123 | webpack(config).run((err, stats) => {
124 | if (err) {
125 | printErrors('Failed to compile.', [err]);
126 | process.exit(1);
127 | }
128 |
129 | if (stats.compilation.errors.length) {
130 | printErrors('Failed to compile.', stats.compilation.errors);
131 | process.exit(1);
132 | }
133 |
134 | console.log(chalk.green('Compiled successfully.'));
135 | console.log();
136 |
137 | console.log('File sizes after gzip:');
138 | console.log();
139 | printFileSizes(stats, previousSizeMap);
140 | console.log();
141 |
142 | var openCommand = process.platform === 'win32' ? 'start' : 'open';
143 | var homepagePath = require(paths.appPackageJson).homepage;
144 | var publicPath = config.output.publicPath;
145 | if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) {
146 | // "homepage": "http://user.github.io/project"
147 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.');
148 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.');
149 | console.log();
150 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
151 | console.log('To publish it at ' + chalk.green(homepagePath) + ', run:');
152 | console.log();
153 | console.log(' ' + chalk.cyan('npm') + ' install --save-dev gh-pages');
154 | console.log();
155 | console.log('Add the following script in your ' + chalk.cyan('package.json') + '.');
156 | console.log();
157 | console.log(' ' + chalk.dim('// ...'));
158 | console.log(' ' + chalk.yellow('"scripts"') + ': {');
159 | console.log(' ' + chalk.dim('// ...'));
160 | console.log(' ' + chalk.yellow('"deploy"') + ': ' + chalk.yellow('"gh-pages -d build"'));
161 | console.log(' }');
162 | console.log();
163 | console.log('Then run:');
164 | console.log();
165 | console.log(' ' + chalk.cyan('npm') + ' run deploy');
166 | console.log();
167 | } else if (publicPath !== '/') {
168 | // "homepage": "http://mywebsite.com/project"
169 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.');
170 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.');
171 | console.log();
172 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
173 | console.log();
174 | } else {
175 | // no homepage or "homepage": "http://mywebsite.com"
176 | console.log('The project was built assuming it is hosted at the server root.');
177 | if (homepagePath) {
178 | // "homepage": "http://mywebsite.com"
179 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.');
180 | console.log();
181 | } else {
182 | // no homepage
183 | console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.');
184 | console.log('For example, add this to build it for GitHub Pages:')
185 | console.log();
186 | console.log(' ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(','));
187 | console.log();
188 | }
189 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
190 | console.log('You may also serve it locally with a static server:')
191 | console.log();
192 | console.log(' ' + chalk.cyan('npm') + ' install -g pushstate-server');
193 | console.log(' ' + chalk.cyan('pushstate-server') + ' build');
194 | console.log(' ' + chalk.cyan(openCommand) + ' http://localhost:9000');
195 | console.log();
196 | }
197 | });
198 | }
199 |
200 | function copyPublicFolder() {
201 | fs.copySync(paths.appPublic, paths.appBuild, {
202 | dereference: true,
203 | filter: file => file !== paths.appHtml
204 | });
205 | }
206 |
--------------------------------------------------------------------------------
/public/initial/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "gain": 1,
4 | "bpm": 100
5 | },
6 | "tracks": {
7 | "master": {
8 | "name": "master",
9 | "id": "master",
10 | "gain": 1,
11 | "filters": [
12 | "239786428374t928374628934728347"
13 | ]
14 | },
15 | "secondChannel": {
16 | "name": "master2",
17 | "id": "secondChannel",
18 | "gain": 1,
19 | "filters": []
20 | }
21 | },
22 | "filters": {
23 | "239786428374t928374628934728347": {
24 | "type": "lowpass"
25 | }
26 | },
27 | "pads": {
28 | "pad1": {
29 | "id": "pad1",
30 | "clips": [
31 | [
32 | "63dd984a-c6b8-4d5c-bcd1-4cdab36055f3",
33 | "f954f9b6-4055-4533-a3d7-416ee1441bba",
34 | "473ef6f9-16e8-48e1-a809-54693a17a509",
35 | "a18d0496-dd87-4a7c-ac95-4d76b4728595",
36 | null,
37 | null,
38 | null,
39 | null
40 | ],
41 | [
42 | "2316526c-ec07-4153-a47a-d831b2aaf18e",
43 | "762e04a0-b651-4560-bad1-31011b7b27e5",
44 | "ccda5e09-62b6-40af-be94-e10077bf6e59",
45 | "e39f4804-4a40-42f7-bac5-eade1ba47b96",
46 | null,
47 | null,
48 | null,
49 | null
50 | ],
51 | [
52 | null,
53 | null,
54 | null,
55 | null,
56 | null,
57 | null,
58 | null,
59 | null
60 | ],
61 | [
62 | null,
63 | null,
64 | null,
65 | null,
66 | null,
67 | null,
68 | null,
69 | null
70 | ],
71 | [
72 | null,
73 | null,
74 | null,
75 | null,
76 | null,
77 | null,
78 | null,
79 | null
80 | ],
81 | [
82 | null,
83 | null,
84 | null,
85 | null,
86 | null,
87 | null,
88 | null,
89 | null
90 | ],
91 | [
92 | null,
93 | null,
94 | null,
95 | null,
96 | null,
97 | null,
98 | null,
99 | null
100 | ],
101 | [
102 | null,
103 | null,
104 | null,
105 | null,
106 | null,
107 | null,
108 | null,
109 | null
110 | ]
111 | ],
112 | "selectedClipId": "e39f4804-4a40-42f7-bac5-eade1ba47b96"
113 | },
114 | "pad2": {
115 | "id": "pad2",
116 | "clips": [
117 | [
118 | null,
119 | null,
120 | null,
121 | null,
122 | null,
123 | null,
124 | null,
125 | null
126 | ],
127 | [
128 | null,
129 | null,
130 | null,
131 | null,
132 | null,
133 | null,
134 | null,
135 | null
136 | ],
137 | [
138 | null,
139 | null,
140 | null,
141 | null,
142 | null,
143 | null,
144 | null,
145 | null
146 | ],
147 | [
148 | "e39f4804-4a40-42f7-bac5-eade1ba47b910",
149 | "e39f4804-4a40-42f7-bac5-eade1ba47b911",
150 | null,
151 | null,
152 | null,
153 | null,
154 | null,
155 | null
156 | ],
157 | [
158 | null,
159 | null,
160 | null,
161 | null,
162 | null,
163 | null,
164 | null,
165 | null
166 | ],
167 | [
168 | null,
169 | null,
170 | null,
171 | null,
172 | null,
173 | null,
174 | null,
175 | null
176 | ],
177 | [
178 | null,
179 | null,
180 | null,
181 | null,
182 | null,
183 | null,
184 | null,
185 | null
186 | ],
187 | [
188 | null,
189 | null,
190 | null,
191 | null,
192 | null,
193 | null,
194 | null,
195 | null
196 | ]
197 | ],
198 | "selectedClipId": "2316526c-ec07-4153-a47a-d831b2aaf18e"
199 | }
200 | },
201 | "clips": {
202 | "2316526c-ec07-4153-a47a-d831b2aaf18e": {
203 | "id": "2316526c-ec07-4153-a47a-d831b2aaf18e",
204 | "type": "audiosample",
205 | "behavior": "single",
206 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21685",
207 | "track": "master",
208 | "gain": 1,
209 | "loop": false
210 | },
211 | "762e04a0-b651-4560-bad1-31011b7b27e5": {
212 | "id": "762e04a0-b651-4560-bad1-31011b7b27e5",
213 | "type": "audiosample",
214 | "behavior": "single",
215 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21686",
216 | "track": "master",
217 | "gain": 1,
218 | "loop": false
219 | },
220 | "63dd984a-c6b8-4d5c-bcd1-4cdab36055f3": {
221 | "gain": 1,
222 | "behavior": "single",
223 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21681",
224 | "loop": false,
225 | "track": "master",
226 | "id": "63dd984a-c6b8-4d5c-bcd1-4cdab36055f3",
227 | "type": "audiosample"
228 | },
229 | "f954f9b6-4055-4533-a3d7-416ee1441bba": {
230 | "gain": 1,
231 | "behavior": "single",
232 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21682",
233 | "loop": false,
234 | "track": "master",
235 | "id": "f954f9b6-4055-4533-a3d7-416ee1441bba",
236 | "type": "audiosample"
237 | },
238 | "473ef6f9-16e8-48e1-a809-54693a17a509": {
239 | "gain": 1,
240 | "behavior": "single",
241 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21683",
242 | "loop": false,
243 | "track": "master",
244 | "id": "473ef6f9-16e8-48e1-a809-54693a17a509",
245 | "type": "audiosample"
246 | },
247 | "a18d0496-dd87-4a7c-ac95-4d76b4728595": {
248 | "gain": 1,
249 | "behavior": "single",
250 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21684",
251 | "loop": false,
252 | "track": "master",
253 | "id": "a18d0496-dd87-4a7c-ac95-4d76b4728595",
254 | "type": "audiosample"
255 | },
256 | "ccda5e09-62b6-40af-be94-e10077bf6e59": {
257 | "gain": 1,
258 | "behavior": "single",
259 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21687",
260 | "loop": false,
261 | "track": "master",
262 | "id": "ccda5e09-62b6-40af-be94-e10077bf6e59",
263 | "type": "audiosample"
264 | },
265 | "e39f4804-4a40-42f7-bac5-eade1ba47b96": {
266 | "gain": 1,
267 | "behavior": "single",
268 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21688",
269 | "loop": false,
270 | "track": "master",
271 | "id": "e39f4804-4a40-42f7-bac5-eade1ba47b96",
272 | "type": "audiosample"
273 | },
274 | "e39f4804-4a40-42f7-bac5-eade1ba47b910": {
275 | "gain": 1,
276 | "behavior": "single",
277 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec2168",
278 | "loop": false,
279 | "track": "master",
280 | "id": "e39f4804-4a40-42f7-bac5-eade1ba47b910",
281 | "type": "audiosample"
282 | },
283 | "e39f4804-4a40-42f7-bac5-eade1ba47b911": {
284 | "gain": 1,
285 | "behavior": "single",
286 | "file": "36c1262c-608c-41db-a12c-7a2fad9d4c14",
287 | "loop": false,
288 | "track": "master",
289 | "id": "e39f4804-4a40-42f7-bac5-eade1ba47b911",
290 | "type": "audiosample"
291 | }
292 | },
293 | "files": {
294 | "2a86e9ee-b409-4e2b-91e0-bcc051ec2168": {
295 | "location": "mysound.mp3",
296 | "name": "mysound"
297 | },
298 | "36c1262c-608c-41db-a12c-7a2fad9d4c14": {
299 | "location": "mysound2.mp3",
300 | "name": "mysound2"
301 | },
302 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21681": {
303 | "location": "1.mp3",
304 | "name": "git1"
305 | },
306 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21682": {
307 | "location": "2.mp3",
308 | "name": "git2"
309 | },
310 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21683": {
311 | "location": "3.mp3",
312 | "name": "kick"
313 | },
314 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21684": {
315 | "location": "4.mp3",
316 | "name": "clap"
317 | },
318 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21685": {
319 | "location": "5.mp3",
320 | "name": "bell1"
321 | },
322 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21686": {
323 | "location": "6.mp3",
324 | "name": "bell2"
325 | },
326 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21687": {
327 | "location": "7.mp3",
328 | "name": "bell3"
329 | },
330 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21688": {
331 | "location": "8.mp3",
332 | "name": "bell4"
333 | }
334 | },
335 | "scheduler": {
336 | "scheduled": {},
337 | "playing": {},
338 | "toStop": {}
339 | }
340 | }
--------------------------------------------------------------------------------
/config/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var autoprefixer = require('autoprefixer');
3 | var webpack = require('webpack');
4 | var findCacheDir = require('find-cache-dir');
5 | var HtmlWebpackPlugin = require('html-webpack-plugin');
6 | var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
7 | var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
8 | var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
9 | var getClientEnvironment = require('./env');
10 | var paths = require('./paths');
11 |
12 | // Webpack uses `publicPath` to determine where the app is being served from.
13 | // In development, we always serve from the root. This makes config easier.
14 | var publicPath = '/';
15 | // `publicUrl` is just like `publicPath`, but we will provide it to our app
16 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
17 | // Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
18 | var publicUrl = '';
19 | // Get environment variables to inject into our app.
20 | var env = getClientEnvironment(publicUrl);
21 |
22 | // This is the development configuration.
23 | // It is focused on developer experience and fast rebuilds.
24 | // The production configuration is different and lives in a separate file.
25 | module.exports = {
26 | target: 'electron-renderer',
27 | // This makes the bundle appear split into separate modules in the devtools.
28 | // We don't use source maps here because they can be confusing:
29 | // https://github.com/facebookincubator/create-react-app/issues/343#issuecomment-237241875
30 | // You may want 'cheap-module-source-map' instead if you prefer source maps.
31 | devtool: 'eval',
32 | // These are the "entry points" to our application.
33 | // This means they will be the "root" imports that are included in JS bundle.
34 | // The first two entry points enable "hot" CSS and auto-refreshes for JS.
35 | entry: [
36 | // Include an alternative client for WebpackDevServer. A client's job is to
37 | // connect to WebpackDevServer by a socket and get notified about changes.
38 | // When you save a file, the client will either apply hot updates (in case
39 | // of CSS changes), or refresh the page (in case of JS changes). When you
40 | // make a syntax error, this client will display a syntax error overlay.
41 | // Note: instead of the default WebpackDevServer client, we use a custom one
42 | // to bring better experience for Create React App users. You can replace
43 | // the line below with these two lines if you prefer the stock client:
44 | // require.resolve('webpack-dev-server/client') + '?/',
45 | // require.resolve('webpack/hot/dev-server'),
46 | require.resolve('react-dev-utils/webpackHotDevClient'),
47 | // We ship a few polyfills by default:
48 | require.resolve('./polyfills'),
49 | // Finally, this is your app's code:
50 | paths.appIndexJs
51 | // We include the app code last so that if there is a runtime error during
52 | // initialization, it doesn't blow up the WebpackDevServer client, and
53 | // changing JS code would still trigger a refresh.
54 | ],
55 | output: {
56 | // Next line is not used in dev but WebpackDevServer crashes without it:
57 | path: paths.appBuild,
58 | // Add /* filename */ comments to generated require()s in the output.
59 | pathinfo: true,
60 | // This does not produce a real file. It's just the virtual path that is
61 | // served by WebpackDevServer in development. This is the JS bundle
62 | // containing code from all our entry points, and the Webpack runtime.
63 | filename: 'static/js/bundle.js',
64 | // This is the URL that app is served from. We use "/" in development.
65 | publicPath: publicPath
66 | },
67 | resolve: {
68 | // This allows you to set a fallback for where Webpack should look for modules.
69 | // We read `NODE_PATH` environment variable in `paths.js` and pass paths here.
70 | // We use `fallback` instead of `root` because we want `node_modules` to "win"
71 | // if there any conflicts. This matches Node resolution mechanism.
72 | // https://github.com/facebookincubator/create-react-app/issues/253
73 | fallback: paths.nodePaths,
74 | // These are the reasonable defaults supported by the Node ecosystem.
75 | // We also include JSX as a common component filename extension to support
76 | // some tools, although we do not recommend using it, see:
77 | // https://github.com/facebookincubator/create-react-app/issues/290
78 | extensions: ['.js', '.json', '.jsx', ''],
79 | alias: {
80 | // Support React Native Web
81 | // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
82 | 'react-native': 'react-native-web'
83 | }
84 | },
85 |
86 | module: {
87 | // First, run the linter.
88 | // It's important to do this before Babel processes the JS.
89 | preLoaders: [
90 | {
91 | test: /\.(js|jsx)$/,
92 | loader: 'eslint',
93 | include: paths.appSrc,
94 | }
95 | ],
96 | loaders: [
97 | // Process JS with Babel.
98 | {
99 | test: /\.(js|jsx)$/,
100 | include: paths.appSrc,
101 | loader: 'babel',
102 | query: {
103 |
104 | // This is a feature of `babel-loader` for webpack (not Babel itself).
105 | // It enables caching results in ./node_modules/.cache/react-scripts/
106 | // directory for faster rebuilds. We use findCacheDir() because of:
107 | // https://github.com/facebookincubator/create-react-app/issues/483
108 | cacheDirectory: findCacheDir({
109 | name: 'react-scripts'
110 | })
111 | }
112 | },
113 | // "postcss" loader applies autoprefixer to our CSS.
114 | // "css" loader resolves paths in CSS and adds assets as dependencies.
115 | // "style" loader turns CSS into JS modules that inject