/*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 ]) ]); } }); }());