piano/js/components/Main.js

493 lines
14 KiB
JavaScript

/*jshint browser: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':
if (this.state.meta.channels[event.channel].sound) {
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();
},
/**
* Display a modal for managing channels
*/
showChannelsModal: function () {
if (this.state.url === '') {
return;
}
this.channelsModal.open();
},
/**
* Switch given parameter on channel
*
* @param {param: string} Parameter to switch
* @param {channel: number} Channel ID
*/
switchChannelParam: function (param, channel) {
var channels = this.state.meta.channels.slice(0),
length = channels.length, i;
for (i = 0; i < length; i += 1) {
if (channels[i].id === channel) {
channels[i][param] = !channels[i][param];
}
}
this.state.meta.channels = channels;
this.setState({
meta: this.state.meta
});
},
/**
* 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
});
},
/**
* 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,
channels: this.state.meta.channels,
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
});
this.channelsModal = App.components.Channel.Board({
channels: this.state.meta.channels,
switch: this.switchChannelParam
});
return React.DOM.div({
className: 'main',
onDragOver: this.dragOver,
onDrop: this.drop
}, [
control,
scroll,
React.DOM.div({
key: 'roll',
className: 'roll'
}, [
noteboard,
keyboard
])
]);
}
});
}());