464 lines
13 KiB
JavaScript
464 lines
13 KiB
JavaScript
/*jshint es5: true */
|
|
/*globals App, React */
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
/**
|
|
* Display main view
|
|
*
|
|
* @prop {url: string} URL to MIDI file
|
|
*/
|
|
App.components.create({
|
|
displayName: 'Main',
|
|
animation: false,
|
|
lastTime: null,
|
|
wheeling: false,
|
|
wasPlaying: false,
|
|
|
|
/**
|
|
* State and props config
|
|
*/
|
|
propTypes: {
|
|
url: React.PropTypes.string
|
|
},
|
|
|
|
getDefaultProps: function () {
|
|
return {
|
|
url: ''
|
|
};
|
|
},
|
|
|
|
getInitialState: function () {
|
|
return {
|
|
opened: false,
|
|
name: '',
|
|
|
|
meta: {},
|
|
timeline: [],
|
|
|
|
playing: false,
|
|
speed: 1,
|
|
time: 0
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Open a file
|
|
*
|
|
* @param {url: string} File path
|
|
* @param {showDialog: bool} Whether to show an open dialog (will ignore url param)
|
|
*/
|
|
open: function (url, showDialog) {
|
|
var remote, dialog, selection;
|
|
|
|
if (showDialog === true) {
|
|
remote = window.atom.require('remote');
|
|
dialog = remote.require('dialog');
|
|
|
|
selection = dialog.showOpenDialog(remote.getCurrentWindow(), {
|
|
title: 'Ouvrir',
|
|
filters: [
|
|
{
|
|
name: 'Fichiers MIDI',
|
|
extensions: ['mid', 'midi']
|
|
},
|
|
{
|
|
name: 'Tous les fichiers',
|
|
extensions: []
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!selection || !selection.length) {
|
|
return;
|
|
} else {
|
|
url = selection[0];
|
|
}
|
|
} else if (url === '') {
|
|
this.close();
|
|
return;
|
|
}
|
|
|
|
if (this.state.opened) {
|
|
this.close();
|
|
}
|
|
|
|
App.MIDI.file.load(url)
|
|
.then(App.MIDI.file.parse)
|
|
.then(function (data) {
|
|
var parts, name;
|
|
|
|
parts = url.split(/\\|\//g);
|
|
name = parts[parts.length - 1];
|
|
|
|
this.setState({
|
|
name: name,
|
|
opened: true,
|
|
|
|
meta: data.meta,
|
|
timeline: data.timeline
|
|
}, function () {
|
|
this.stop();
|
|
this.setupChannels();
|
|
});
|
|
}.bind(this))
|
|
.catch(function (err) {
|
|
var modal = App.components.Modal.Error({
|
|
title: "Erreur d'ouverture du fichier",
|
|
error: err
|
|
});
|
|
|
|
modal.open();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Create a new file
|
|
*/
|
|
create: function () {
|
|
this.setState({
|
|
name: 'Nouvelle composition',
|
|
opened: true,
|
|
|
|
meta: {},
|
|
timeline: []
|
|
}, function () {
|
|
this.stop();
|
|
this.setupChannels();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Close opened file
|
|
*/
|
|
close: function () {
|
|
this.setState({
|
|
name: '',
|
|
opened: false,
|
|
|
|
meta: {},
|
|
timeline: []
|
|
}, function () {
|
|
this.stop();
|
|
this.setupChannels();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Load file when mounted
|
|
* Set up keyboard listener
|
|
*/
|
|
componentDidMount: function () {
|
|
this.open(this.props.url);
|
|
window.addEventListener('keyup', this.keyUp);
|
|
},
|
|
|
|
/**
|
|
* Remove keyboard listener
|
|
*/
|
|
componentWillUnmount: function () {
|
|
window.removeEventListener('keyup', this.keyUp);
|
|
},
|
|
|
|
/**
|
|
* Load file when changed
|
|
*/
|
|
componentWillReceiveProps: function (props) {
|
|
if (props.url !== this.state.url) {
|
|
this.open(props.url);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update window title on name change
|
|
*/
|
|
componentWillUpdate: function (nextProps, nextState) {
|
|
var name = nextState.name;
|
|
|
|
if (nextState.name !== this.state.name) {
|
|
if (typeof name === 'string' && name !== '') {
|
|
document.title = name + ' - Piano';
|
|
} else {
|
|
document.title = 'Piano';
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Playback control
|
|
*/
|
|
|
|
/* start playback */
|
|
play: function () {
|
|
if (this.state.playing || this.state.url === '') {
|
|
return;
|
|
}
|
|
|
|
this.wasPlaying = false;
|
|
this.startedTime = window.performance.now();
|
|
this.animation = window.requestAnimationFrame(this.move);
|
|
|
|
this.setState({
|
|
playing: true
|
|
});
|
|
},
|
|
|
|
/* only plays if it was previously paused with halt() */
|
|
resume: function () {
|
|
if (this.wasPlaying) {
|
|
this.wasPlaying = false;
|
|
this.play();
|
|
}
|
|
},
|
|
|
|
/* pause playing */
|
|
pause: function () {
|
|
if (!this.state.playing) {
|
|
return;
|
|
}
|
|
|
|
window.cancelAnimationFrame(this.animation);
|
|
|
|
this.animation = false;
|
|
this.lastTime = null;
|
|
|
|
this.setState({
|
|
playing: false
|
|
});
|
|
},
|
|
|
|
/* halt play for future resuming */
|
|
halt: function () {
|
|
if (this.state.playing) {
|
|
this.wasPlaying = true;
|
|
this.pause();
|
|
}
|
|
},
|
|
|
|
/* pause and reset time */
|
|
stop: function () {
|
|
this.pause();
|
|
this.setTime(0);
|
|
},
|
|
|
|
/* update current time and play last notes */
|
|
move: function () {
|
|
var previous = this.state.time, next = previous,
|
|
time, speed = this.state.speed;
|
|
|
|
time = window.performance.now();
|
|
|
|
if (this.lastTime) {
|
|
next += (time - this.lastTime) / 1000 * speed;
|
|
}
|
|
|
|
if (previous > this.state.meta.length) {
|
|
this.stop();
|
|
return;
|
|
}
|
|
|
|
this.do(previous, next);
|
|
|
|
this.setState({
|
|
time: next
|
|
}, function () {
|
|
this.animation = window.requestAnimationFrame(this.move);
|
|
this.lastTime = time;
|
|
}.bind(this));
|
|
},
|
|
|
|
/* set up channel meta */
|
|
setupChannels: function () {
|
|
var channels = this.state.meta.channels || {}, channel,
|
|
length = 16, i;
|
|
|
|
for (i = 0; i < length; i += 1) {
|
|
channel = channels[i] || {};
|
|
|
|
App.MIDI.output.programChange(
|
|
channel.id || i,
|
|
channel.program || 0
|
|
);
|
|
}
|
|
},
|
|
|
|
/* do events in given interval */
|
|
do: function (start, end) {
|
|
var timeline = this.state.timeline, i,
|
|
length = timeline.length, event;
|
|
|
|
for (i = 0; i < length; i += 1) {
|
|
event = timeline[i];
|
|
|
|
if (event.time >= start && event.time < end) {
|
|
switch (event.type) {
|
|
case 'noteOn':
|
|
this.refs.keyboard.on(
|
|
event.note,
|
|
event.channel,
|
|
event.velocity
|
|
);
|
|
break;
|
|
case 'noteOff':
|
|
this.refs.keyboard.off(
|
|
event.note,
|
|
event.channel
|
|
);
|
|
break;
|
|
case 'noteAftertouch':
|
|
this.refs.keyboard.change(
|
|
event.note,
|
|
event.channel,
|
|
event.amount
|
|
);
|
|
break;
|
|
case 'controller':
|
|
App.MIDI.output.controller(
|
|
event.channel,
|
|
event.subtype,
|
|
event.value
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Allow file dropping
|
|
*/
|
|
dragOver: function (e) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
},
|
|
|
|
/**
|
|
* Handle drop
|
|
*/
|
|
drop: function (e) {
|
|
var files = e.dataTransfer.files, file,
|
|
length = files.length, reader;
|
|
|
|
if (length) {
|
|
this.open(files[0].path);
|
|
}
|
|
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
},
|
|
|
|
/**
|
|
* Delegates
|
|
*/
|
|
scrollNoteboard: function (delta) {
|
|
this.refs.noteboard.scroll(delta);
|
|
},
|
|
|
|
startWheel: function () {
|
|
if (this.wheeling !== false) {
|
|
clearTimeout(this.wheeling);
|
|
}
|
|
|
|
this.halt();
|
|
this.wheeling = setTimeout(this.resume, 250);
|
|
},
|
|
|
|
setPlaySpeed: function (speed) {
|
|
this.setState({
|
|
speed: speed
|
|
});
|
|
},
|
|
|
|
setTime: function (time) {
|
|
this.refs.keyboard.allOff();
|
|
this.setState({
|
|
time: time
|
|
});
|
|
},
|
|
|
|
showChannelsModal: function () {
|
|
if (this.state.url === '') {
|
|
return;
|
|
}
|
|
|
|
var modal = App.components.Channel.Modal({
|
|
channels: this.state.meta.channels
|
|
});
|
|
|
|
modal.open();
|
|
},
|
|
|
|
/**
|
|
* Render panel
|
|
*/
|
|
render: function () {
|
|
var control, noteboard, keyboard, scroll;
|
|
|
|
keyboard = App.components.Key.Board({
|
|
key: 'keyboard',
|
|
ref: 'keyboard'
|
|
});
|
|
|
|
noteboard = App.components.Note.Board({
|
|
key: 'noteboard',
|
|
ref: 'noteboard',
|
|
|
|
notes: this.state.meta.notes,
|
|
|
|
playing: this.state.playing,
|
|
speed: this.state.speed,
|
|
time: this.state.time,
|
|
length: this.state.meta.length,
|
|
opened: this.state.opened,
|
|
|
|
startWheel: this.startWheel,
|
|
setTime: this.setTime,
|
|
open: this.open,
|
|
create: this.create
|
|
});
|
|
|
|
control = App.components.Control({
|
|
key: 'control',
|
|
ref: 'control',
|
|
|
|
playing: this.state.playing,
|
|
opened: this.state.opened,
|
|
speed: this.state.speed,
|
|
|
|
play: this.play,
|
|
pause: this.pause,
|
|
showChannelsModal: this.showChannelsModal,
|
|
setPlaySpeed: this.setPlaySpeed,
|
|
close: this.close
|
|
});
|
|
|
|
scroll = App.components.UI.Scroll({
|
|
className: 'scroll',
|
|
key: 'scroll',
|
|
|
|
current: this.state.time,
|
|
max: this.state.meta.length,
|
|
onChange: this.scrollNoteboard,
|
|
onDragStart: this.halt,
|
|
onDragEnd: this.resume
|
|
});
|
|
|
|
return React.DOM.div({
|
|
className: 'main',
|
|
onDragOver: this.dragOver,
|
|
onDrop: this.drop
|
|
}, [
|
|
control,
|
|
scroll,
|
|
React.DOM.div({
|
|
key: 'roll',
|
|
className: 'roll'
|
|
}, [
|
|
noteboard,
|
|
keyboard
|
|
])
|
|
]);
|
|
}
|
|
});
|
|
}()); |