/*
* Created on Feb 10, 2007
*
* Copyright (c) 2007 Jens Gulden
*
* http://www.frinika.com
*
* This file is part of Frinika.
*
* Frinika is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* Frinika is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Frinika; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.frinika.sequencer.gui.menu.midi;
import static com.frinika.localization.CurrentLocale.getMessage;
import com.frinika.gui.OptionsDialog;
import com.frinika.project.gui.ProjectFrame;
import com.frinika.sequencer.model.ControllerEvent;
import com.frinika.sequencer.model.PitchBendEvent;
import com.frinika.sequencer.model.NoteEvent;
import com.frinika.sequencer.model.MultiEvent;
import com.frinika.sequencer.model.MidiPart;
import com.frinika.sequencer.gui.TimeFormat;
import com.frinika.sequencer.gui.TimeSelector;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.JPanel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.util.*;
/**
* Menu-action for inserting controller-events accordig to different,
* selectable kinds of functions. In a special mode, also note-events
* can be inserted, or velocity-values of existing notes can be changed.
*
* @author Jens Gulden
*/
public class MidiInsertControllersAction extends AbstractMidiAction {
protected Collection<ControllerFunction> availableFunctions;
int controller = 7;
long start;
long length = 128 * 4 * 1;
long resolution = 16;
ControllerFunction function;
public MidiInsertControllersAction(ProjectFrame frame) {
super(frame, "sequencer.midi.insert_controllers");
initControllerFunctions();
function = availableFunctions.iterator().next(); // first one by default
}
private void initControllerFunctions() {
availableFunctions = new ArrayList<ControllerFunction>();
availableFunctions.add(new Linear());
availableFunctions.add(new Triangle());
availableFunctions.add(new Saw());
availableFunctions.add(new Sine());
availableFunctions.add(new Square());
}
public Collection<ControllerFunction> getAvailableControllerFunctions() {
// (modeled as instance-method (non-static) for possibly later enhancements)
return availableFunctions;
}
@Override
public void performAction() {
if (controller > 0) { // normal controller
// clear all controllers of this kind in the given range
removeAllControllers(getMidiPart(), controller, start, length);
// insert new ones
int lastVal = -1;
for (long x = 0; x < length; x += resolution) {
int val = function.value(x);
if (controller != 64) {
if (val < 0) val = 0; else if (val > 127) val = 127;
}
if (val != lastVal) {
insertController(getMidiPart(), start + x, controller, val);
}
lastVal = val;
}
} else if (controller == 0) { // velocities (of existing notes)
super.performAction(); // use default mechanism provided by superclass, will call modifyNoteEvent
} else if (controller == -1) { // new notes
for (long x = 0; x < length; x += resolution) {
int val = function.value(x);
if (val < 0) val = 0; else if (val > 127) val = 127;
insertNote(getMidiPart(), start + x, val, (int)resolution - 1, 100);
}
}
}
@Override
public void modifyNoteEvent(NoteEvent note) {
// called via super.performAction() iff controller==0.
long st = note.getStartTick();
if ((st >= start) && (st < start + length)){
long x = st - start;
int val = function.value(x);
if (val < 0) val = 0; else if (val > 127) val = 127;
((NoteEvent)note).setVelocity(val);
}
}
private void insertController(MidiPart part, long tick, int controller, int value) {
if (controller == 64) { // pitch bend
PitchBendEvent c = new PitchBendEvent(part, tick, value);
part.add(c);
} else {
ControllerEvent c = new ControllerEvent(part, tick, controller, value);
part.add(c);
}
}
private void insertNote(MidiPart part, long tick, int note, int duration, int velocity) {
NoteEvent n = new NoteEvent(part, tick, note, 100, part.getMidiChannel(), duration);
part.add(n);
}
private void removeAllControllers(MidiPart part, int ctrl, long start, long length) {
for (MultiEvent evt : new ArrayList<MultiEvent>(part.getMultiEvents()) ) {
if (evt instanceof ControllerEvent) {
long st = evt.getStartTick();
if ((st >= start) && (st <= start + length)) {
if (((ControllerEvent)evt).getControlNumber() == ctrl) {
part.remove(evt);
}
}
}
}
}
@Override
protected JComponent createGUI() {
return new MidiInsertControllersActionEditor(frame, this);
}
@Override
protected OptionsDialog createDialog() {
OptionsDialog d = new OptionsDialog(frame, createGUI(), getMessage(actionId)) {
@Override
public void repack() {
// nop
}
};
d.pack();
d.setSize( d.getWidth() + 250 , d.getHeight() + 50 );
return d;
}
// --- inner classes -----------------------------------------------------
public interface ControllerFunction {
public String getName();
public Icon getIcon(int width, int height);
public JComponent createGUI();
public int value(long tick);
public int iconValue(int x, int width, int height);
}
public class ControllerFunctionIcon implements Icon {
private ControllerFunction function;
private int width;
private int height;
public ControllerFunctionIcon(ControllerFunction function, int width, int height) {
this.function = function;
this.width = width;
this.height = height;
}
public int getIconHeight() {
return width;
}
public int getIconWidth() {
return height;
}
public void paintIcon(Component c, Graphics g, int x, int y) {
int v = f(0);
g.setColor(java.awt.Color.black);
for (int i = 1; i < width; i++) {
int newV = f(i);
g.drawLine(x+i-1, y+height-v, x+i, y+height-newV);
v = newV;
}
}
private int f(int x) {
return function.iconValue(x+height/8, width, height-height/4);
}
}
public abstract class AbstractControllerFunction implements ControllerFunction {
protected String name;
protected AbstractControllerFunction() {
// nop
}
protected AbstractControllerFunction(String name) {
this();
setName(name);
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public Icon getIcon(int width, int height) {
return new ControllerFunctionIcon(this, width, height);
}
}
abstract class AbstractCyclicControllerFunction extends AbstractControllerFunction {
int min = 0;
int max = 127;
long phase = 128 * 4 * 2;
long shift = 0;
protected AbstractCyclicControllerFunction(String name) {
super(name);
}
public JComponent createGUI() {
//JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JPanel panel = new JPanel(new FlowLayout());
panel.add(new JLabel("Between"));
panel.add(spinner(min, 0, 127, new ChangeListener() {
public void stateChanged(ChangeEvent e) {
((SpinnerNumberModel)((JSpinner)e.getSource()).getModel()).setMaximum( (controller == 64) ? 16383 : 127); // hack to allow use of pitch bend, too
min = (Integer)((JSpinner)e.getSource()).getValue();
}
}));
panel.add(new JLabel("and"));
panel.add(spinner(max, 0, 127, new ChangeListener() {
public void stateChanged(ChangeEvent e) {
((SpinnerNumberModel)((JSpinner)e.getSource()).getModel()).setMaximum( (controller == 64) ? 16383 : 127); // hack to allow use of pitch bend, too
max = (Integer)((JSpinner)e.getSource()).getValue();
}
}));
createGUIExtra(panel);
return panel;
}
protected void createGUIExtra(JPanel panel) {
panel.add(new JPanel()); // spacer
panel.add(new JLabel("Interval"));
final TimeSelector phaseTimeSelector = new TimeSelector(phase, frame.getProjectContainer(), TimeFormat.BAR_BEAT_TICK);
phaseTimeSelector.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
phase = phaseTimeSelector.getTicks();
}
});
panel.add(phaseTimeSelector);
panel.add(new JLabel("Shift"));
final TimeSelector shiftTimeSelector = new TimeSelector(shift, frame.getProjectContainer(), TimeFormat.BAR_BEAT_TICK);
phaseTimeSelector.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
shift = shiftTimeSelector.getTicks();
}
});
panel.add(shiftTimeSelector);
}
}
class Linear extends AbstractCyclicControllerFunction {
Linear() {
super(getMessage("sequencer.midi.controllerfunction.linear"));
}
public int value(long x) {
return min + Math.round(((max - min) * x) / (float)length);
}
public int iconValue(int x, int width, int height) {
return (x * height) / width;
}
@Override
public void createGUIExtra(JPanel panel) {
// nop
}
}
class Triangle extends AbstractCyclicControllerFunction {
Triangle() {
super(getMessage("sequencer.midi.controllerfunction.triangle"));
}
public int value(long x) {
int ph = (int)((x + shift) % phase);
int diff = max - min;
int val = (int)(Math.round((max + diff - min) * ph / (float)phase)) + min;
if (val > max) {
int d = val - max;
val = max - d;
}
return val;
}
public int iconValue(int x, int width, int height) {
int y = (x * 4) % (height * 2);
int d = y - height;
if (d > 0) y = height - d;
return y;
}
}
class Saw extends AbstractCyclicControllerFunction {
Saw() {
super(getMessage("sequencer.midi.controllerfunction.saw"));
}
public int value(long x) {
int ph = (int)((x + shift) % phase);
int diff = max - min;
int val = (int)Math.round(min + (diff * ph / (float)phase));
return val;
}
public int iconValue(int x, int width, int height) {
return (x*2) % height;
}
}
class Sine extends AbstractCyclicControllerFunction {
Sine() {
super(getMessage("sequencer.midi.controllerfunction.sine"));
}
public int value(long x) {
double amplitude = (max - min)/2.0;
double center = min + amplitude;
double sin = Math.sin( ( ((x + shift ) * 2.0 * Math.PI) / (double)phase ) + (1.5 * Math.PI) );
int val = (int) Math.round( ( sin * amplitude ) + center );
return val;
}
public int iconValue(int x, int width, int height) {
int h = height / 2;
int y = (int) Math.round( Math.sin( ( x / 3f ) + (1.75 * Math.PI) ) * h * 0.75 ) + h;
return y;
}
}
class Square extends AbstractCyclicControllerFunction {
Square() {
super(getMessage("sequencer.midi.controllerfunction.square"));
}
public int value(long x) {
return min + (int)(((x + shift ) / phase) % 2) * (max - min);
}
public int iconValue(int x, int width, int height) {
return ( ((x / 8) % 2) != 0 ? height-height/4 : height/4 );
}
}
private static JSpinner spinner(int dflt, int min, int max, ChangeListener l) {
JSpinner s = new JSpinner(new SpinnerNumberModel(dflt, min, max, 1));
s.addChangeListener(l);
return s;
}
}