diff options
author | RubbaBoy <[email protected]> | 2020-07-06 02:33:28 -0400 |
---|---|---|
committer | Phil Burk <[email protected]> | 2020-10-30 11:19:34 -0700 |
commit | 46888fae6eb7b1dd386f7af7d101ead99ae61981 (patch) | |
tree | 8969bbfd68d2fb5c0d8b86da49ec2eca230a72ab /src/main/java/com/jsyn/swing | |
parent | c51e92e813dd481603de078f0778e1f75db2ab05 (diff) |
Restructured project, added gradle, JUnit, logger, and more
Added Gradle (and removed ant), modernized testing via the JUnit framework, moved standalone examples from the tests directory to a separate module, removed sparsely used Java logger and replaced it with SLF4J. More work could be done, however this is a great start to greatly improving the health of the codebase.
Diffstat (limited to 'src/main/java/com/jsyn/swing')
18 files changed, 2575 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java b/src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java new file mode 100644 index 0000000..dc02259 --- /dev/null +++ b/src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java @@ -0,0 +1,199 @@ +/* + * Copyright 2012 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.HashSet; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +/** + * Support for playing musical scales on the ASCII keyboard of a computer. Has a Sustain checkbox + * that simulates a sustain pedal. Auto-repeat keys are detected and suppressed. + * + * @author Phil Burk (C) 2012 Mobileer Inc + */ +@SuppressWarnings("serial") +public abstract class ASCIIMusicKeyboard extends JPanel { + + private static final Logger LOGGER = LoggerFactory.getLogger(ASCIIMusicKeyboard.class); + + private final JCheckBox sustainBox; + private final JButton focusButton; + public static final String PENTATONIC_KEYS = "zxcvbasdfgqwert12345"; + public static final String SEPTATONIC_KEYS = "zxcvbnmasdfghjqwertyu1234567890"; + private String keyboardLayout = SEPTATONIC_KEYS; /* default music keyboard layout */ + private int basePitch = 48; + private final KeyListener keyListener; + private final JLabel countLabel; + private int onCount; + private int offCount; + private int pressedCount; + private int releasedCount; + private final HashSet<Integer> pressedKeys = new HashSet<Integer>(); + private final HashSet<Integer> onKeys = new HashSet<Integer>(); + + public ASCIIMusicKeyboard() { + focusButton = new JButton("Click here to play ASCII keys."); + focusButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + } + }); + keyListener = new KeyListener() { + + @Override + public void keyPressed(KeyEvent e) { + int key = e.getKeyChar(); + int idx = keyboardLayout.indexOf(key); + LOGGER.debug("keyPressed " + idx); + if (idx >= 0) { + if (!pressedKeys.contains(idx)) { + keyOn(convertIndexToPitch(idx)); + onCount++; + pressedKeys.add(idx); + onKeys.add(idx); + } + } + pressedCount++; + updateCountLabel(); + } + + @Override + public void keyReleased(KeyEvent e) { + int key = e.getKeyChar(); + int idx = keyboardLayout.indexOf(key); + LOGGER.debug("keyReleased " + idx); + if (idx >= 0) { + if (!sustainBox.isSelected()) { + noteOffInternal(idx); + onKeys.remove(idx); + } + pressedKeys.remove(idx); + } + releasedCount++; + updateCountLabel(); + } + + @Override + public void keyTyped(KeyEvent arg0) { + } + }; + focusButton.addKeyListener(keyListener); + add(focusButton); + + sustainBox = new JCheckBox("sustain"); + sustainBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + if (!sustainBox.isSelected()) { + for (Integer noteIndex : onKeys) { + noteOffInternal(noteIndex); + } + onKeys.clear(); + } + } + }); + add(sustainBox); + sustainBox.addKeyListener(keyListener); + + countLabel = new JLabel("0"); + add(countLabel); + } + + private void noteOffInternal(int idx) { + keyOff(convertIndexToPitch(idx)); + offCount++; + } + + protected void updateCountLabel() { + countLabel.setText(onCount + "/" + offCount + ", " + pressedCount + "/" + releasedCount); + } + + /** + * Convert index to a MIDI noteNumber in a major scale. Result will be offset by the basePitch. + */ + public int convertIndexToPitch(int keyIndex) { + int scale[] = { + 0, 2, 4, 5, 7, 9, 11 + }; + int octave = keyIndex / scale.length; + int idx = keyIndex % scale.length; + int pitch = (octave * 12) + scale[idx]; + return pitch + basePitch; + } + + /** + * This will be called when a key is released. It may also be called for sustaining notes when + * the Sustain check box is turned off. + * + * @param keyIndex + */ + public abstract void keyOff(int keyIndex); + + /** + * This will be called when a key is pressed. + * + * @param keyIndex + */ + public abstract void keyOn(int keyIndex); + + public String getKeyboardLayout() { + return keyboardLayout; + } + + /** + * Specify the keys that will be active for music. + * For example "qwertyui". + * If the first character in the layout is + * pressed then keyOn() will be called with 0. Default is SEPTATONIC_KEYS. + * + * @param keyboardLayout defines order of playable keys + */ + public void setKeyboardLayout(String keyboardLayout) { + this.keyboardLayout = keyboardLayout; + } + + public int getBasePitch() { + return basePitch; + } + + /** + * Define offset used by convertIndexToPitch(). + * + * @param basePitch + */ + public void setBasePitch(int basePitch) { + this.basePitch = basePitch; + } + + /** + * @return + */ + public KeyListener getKeyListener() { + return keyListener; + } +} diff --git a/src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java b/src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java new file mode 100644 index 0000000..647e8da --- /dev/null +++ b/src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import javax.swing.DefaultBoundedRangeModel; + +/** + * Double precision data model for sliders and knobs. Maps integer range info to a double value. + * + * @author Phil Burk, (C) 2002 SoftSynth.com, PROPRIETARY and CONFIDENTIAL + */ +public class DoubleBoundedRangeModel extends DefaultBoundedRangeModel { + private static final long serialVersionUID = 284361767102120148L; + protected String name; + private double dmin; + private double dmax; + + public DoubleBoundedRangeModel(String name, int resolution, double dmin, double dmax, + double dval) { + this.name = name; + this.dmin = dmin; + this.dmax = dmax; + setMinimum(0); + setMaximum(resolution); + setDoubleValue(dval); + } + + public boolean equivalentTo(Object other) { + if (!(other instanceof DoubleBoundedRangeModel)) + return false; + DoubleBoundedRangeModel otherModel = (DoubleBoundedRangeModel) other; + return (getValue() == otherModel.getValue()); + } + + /** Set name of value. This may be used in labels or when saving the value. */ + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public double getDoubleMinimum() { + return dmin; + } + + public double getDoubleMaximum() { + return dmax; + } + + public double sliderToDouble(int sliderValue) { + double doubleMin = getDoubleMinimum(); + return doubleMin + ((getDoubleMaximum() - doubleMin) * sliderValue / getMaximum()); + } + + public int doubleToSlider(double dval) { + double doubleMin = getDoubleMinimum(); + // TODO consider using Math.floor() instead of (int) if not too slow. + return (int) Math.round(getMaximum() * (dval - doubleMin) + / (getDoubleMaximum() - doubleMin)); + } + + public double getDoubleValue() { + return sliderToDouble(getValue()); + } + + public void setDoubleValue(double dval) { + setValue(doubleToSlider(dval)); + } + +} diff --git a/src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java b/src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java new file mode 100644 index 0000000..81b67df --- /dev/null +++ b/src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import java.util.Hashtable; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JSlider; +import javax.swing.border.TitledBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jsyn.util.NumericOutput; + +/** + * Slider that takes a DoubleBoundedRangeModel. It displays the current value in a titled border. + * + * @author Phil Burk, (C) 2002 SoftSynth.com, PROPRIETARY and CONFIDENTIAL + */ + +public class DoubleBoundedRangeSlider extends JSlider { + /** + * + */ + private static final long serialVersionUID = -440390322602838998L; + /** Places after decimal point for display. */ + private int places; + + public DoubleBoundedRangeSlider(DoubleBoundedRangeModel model) { + this(model, 5); + } + + public DoubleBoundedRangeSlider(DoubleBoundedRangeModel model, int places) { + super(model); + this.places = places; + setBorder(BorderFactory.createTitledBorder(generateTitleText())); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + updateTitle(); + } + }); + } + + protected void updateTitle() { + TitledBorder border = (TitledBorder) getBorder(); + if (border != null) { + border.setTitle(generateTitleText()); + repaint(); + } + } + + String generateTitleText() { + DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) getModel(); + double val = model.getDoubleValue(); + String valText = NumericOutput.doubleToString(val, 0, places); + return model.getName() + " = " + valText; + } + + public void makeStandardLabels(int labelSpacing) { + setMajorTickSpacing(labelSpacing / 2); + setLabelTable(createStandardLabels(labelSpacing)); + setPaintTicks(true); + setPaintLabels(true); + } + + public double nextLabelValue(double current, double delta) { + return current + delta; + } + + public void makeLabels(double start, double delta, int places) { + DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) getModel(); + // Create the label table + Hashtable<Integer, JLabel> labelTable = new Hashtable<Integer, JLabel>(); + double dval = start; + while (dval <= model.getDoubleMaximum()) { + int sliderValue = model.doubleToSlider(dval); + String text = NumericOutput.doubleToString(dval, 0, places); + labelTable.put(sliderValue, new JLabel(text)); + dval = nextLabelValue(dval, delta); + } + setLabelTable(labelTable); + setPaintLabels(true); + } + +} diff --git a/src/main/java/com/jsyn/swing/DoubleBoundedTextField.java b/src/main/java/com/jsyn/swing/DoubleBoundedTextField.java new file mode 100644 index 0000000..3301bb1 --- /dev/null +++ b/src/main/java/com/jsyn/swing/DoubleBoundedTextField.java @@ -0,0 +1,94 @@ +/* + * Copyright 2000 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import java.awt.Color; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +import javax.swing.JTextField; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * TextField that turns pink when modified, and white when the value is entered. + * + * @author (C) 2000-2010 Phil Burk, Mobileer Inc + * @version 16 + */ + +public class DoubleBoundedTextField extends JTextField { + private static final long serialVersionUID = 6882779668177620812L; + boolean modified = false; + int numCharacters; + private DoubleBoundedRangeModel model; + + public DoubleBoundedTextField(DoubleBoundedRangeModel pModel, int numCharacters) { + super(numCharacters); + this.model = pModel; + this.numCharacters = numCharacters; + setHorizontalAlignment(SwingConstants.LEADING); + setValue(model.getDoubleValue()); + addKeyListener(new KeyAdapter() { + @Override + public void keyTyped(KeyEvent e) { + if (e.getKeyChar() == '\n') { + model.setDoubleValue(getValue()); + } else { + markDirty(); + } + } + }); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + setValue(model.getDoubleValue()); + } + }); + } + + private void markDirty() { + modified = true; + setBackground(Color.pink); + repaint(); + } + + private void markClean() { + modified = false; + setBackground(Color.white); + setCaretPosition(0); + repaint(); + } + + @Override + public void setText(String text) { + markDirty(); + super.setText(text); + } + + private double getValue() throws NumberFormatException { + double val = Double.valueOf(getText()).doubleValue(); + markClean(); + return val; + } + + private void setValue(double value) { + super.setText(String.format("%6.4f", value)); + markClean(); + } +} diff --git a/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java b/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java new file mode 100644 index 0000000..2db4c29 --- /dev/null +++ b/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java @@ -0,0 +1,573 @@ +/* + * Copyright 1997 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.util.ArrayList; + +import com.jsyn.data.SegmentedEnvelope; +import com.jsyn.unitgen.VariableRateDataReader; + +/** + * Edit a list of ordered duration,value pairs suitable for use with a SegmentedEnvelope. + * + * @author (C) 1997-2013 Phil Burk, SoftSynth.com + * @see EnvelopePoints + * @see SegmentedEnvelope + * @see VariableRateDataReader + */ + +/* ========================================================================== */ +public class EnvelopeEditorBox extends XYController implements MouseListener, MouseMotionListener { + EnvelopePoints points; + ArrayList<EditListener> listeners = new ArrayList<EditListener>(); + int dragIndex = -1; + double dragLowLimit; + double dragHighLimit; + double draggedPoint[]; + double xBefore; // WX value before point + double xPicked; // WX value of picked point + double dragWX; + double dragWY; + int maxPoints = Integer.MAX_VALUE; + int radius = 4; + double verticalBarSpacing = 1.0; + boolean verticalBarsEnabled = false; + double maximumXRange = Double.MAX_VALUE; + double minimumXRange = 0.1; + int rangeStart = -1; // gx coordinates + int rangeEnd = -1; + int mode = EDIT_POINTS; + public final static int EDIT_POINTS = 0; + public final static int SELECT_SUSTAIN = 1; + public final static int SELECT_RELEASE = 2; + + Color rangeColor = Color.RED; + Color sustainColor = Color.BLUE; + Color releaseColor = Color.YELLOW; + Color overlapColor = Color.GREEN; + Color firstLineColor = Color.GRAY; + + public interface EditListener { + public void objectEdited(Object editor, Object edited); + } + + public EnvelopeEditorBox() { + addMouseListener(this); + addMouseMotionListener(this); + } + + public void setMaximumXRange(double maxXRange) { + maximumXRange = maxXRange; + } + + public double getMaximumXRange() { + return maximumXRange; + } + + public void setMinimumXRange(double minXRange) { + minimumXRange = minXRange; + } + + public double getMinimumXRange() { + return minimumXRange; + } + + public void setSelection(int start, int end) { + switch (mode) { + case SELECT_SUSTAIN: + points.setSustainLoop(start, end); + break; + case SELECT_RELEASE: + points.setReleaseLoop(start, end); + break; + } + // LOGGER.debug("start = " + start + ", end = " + end ); + } + + /** Set mode to either EDIT_POINTS or SELECT_SUSTAIN, SELECT_RELEASE; */ + public void setMode(int mode) { + this.mode = mode; + } + + public int getMode() { + return mode; + } + + /** + * Add a listener to receive edit events. Listener will be passed the editor object and the + * edited object. + */ + public void addEditListener(EditListener listener) { + listeners.add(listener); + } + + public void removeEditListener(EditListener listener) { + listeners.remove(listener); + } + + /** Send event to every subscribed listener. */ + public void fireObjectEdited() { + for (EditListener listener : listeners) { + listener.objectEdited(this, points); + } + } + + public void setMaxPoints(int maxPoints) { + this.maxPoints = maxPoints; + } + + public int getMaxPoints() { + return maxPoints; + } + + public int getNumPoints() { + return points.size(); + } + + public void setPoints(EnvelopePoints points) { + this.points = points; + setMaxWorldY(points.getMaximumValue()); + } + + public EnvelopePoints getPoints() { + return points; + } + + /** + * Return index of point before this X position. + */ + private int findPointBefore(double wx) { + int pnt = -1; + double px = 0.0; + xBefore = 0.0; + for (int i = 0; i < points.size(); i++) { + px += points.getDuration(i); + if (px > wx) + break; + pnt = i; + xBefore = px; + } + return pnt; + } + + private int pickPoint(double wx, double wxAperture, double wy, double wyAperture) { + double px = 0.0; + double wxLow = wx - wxAperture; + double wxHigh = wx + wxAperture; + // LOGGER.debug("wxLow = " + wxLow + ", wxHigh = " + wxHigh ); + double wyLow = wy - wyAperture; + double wyHigh = wy + wyAperture; + // LOGGER.debug("wyLow = " + wyLow + ", wyHigh = " + wyHigh ); + double wxScale = 1.0 / wxAperture; // only divide once, then multiply + double wyScale = 1.0 / wyAperture; + int bestPoint = -1; + double bestDistance = Double.MAX_VALUE; + for (int i = 0; i < points.size(); i++) { + double dar[] = points.getPoint(i); + px += dar[0]; + double py = dar[1]; + // LOGGER.debug("px = " + px + ", py = " + py ); + if ((px > wxLow) && (px < wxHigh) && (py > wyLow) && (py < wyHigh)) { + /* Inside pick range. Calculate distance squared. */ + double ndx = (px - wx) * wxScale; + double ndy = (py - wy) * wyScale; + double dist = (ndx * ndx) + (ndy * ndy); + // LOGGER.debug("dist = " + dist ); + if (dist < bestDistance) { + bestPoint = i; + bestDistance = dist; + xPicked = px; + } + } + } + return bestPoint; + } + + private void clickDownRange(boolean shiftDown, int gx, int gy) { + setSelection(-1, -1); + rangeStart = rangeEnd = gx; + repaint(); + } + + private void dragRange(int gx, int gy) { + rangeEnd = gx; + repaint(); + } + + private void clickUpRange(int gx, int gy) { + dragRange(gx, gy); + if (rangeEnd < rangeStart) { + int temp = rangeEnd; + rangeEnd = rangeStart; + rangeStart = temp; + } + // LOGGER.debug("clickUpRange: gx = " + gx + ", rangeStart = " + + // rangeStart ); + double wx = convertGXtoWX(rangeStart); + int i0 = findPointBefore(wx); + wx = convertGXtoWX(rangeEnd); + int i1 = findPointBefore(wx); + + if (i1 == i0) { + // set single point at zero so there is nothing played for queueOn() + if (gx < 0) { + setSelection(0, 0); + } + // else clear any existing loop + } else if (i1 == (i0 + 1)) { + setSelection(i1 + 1, i1 + 1); // set to a single point + } else if (i1 > (i0 + 1)) { + setSelection(i0 + 1, i1 + 1); // set to a range of two or more + } + + rangeStart = -1; + rangeEnd = -1; + fireObjectEdited(); + } + + private void clickDownPoints(boolean shiftDown, int gx, int gy) { + dragIndex = -1; + double wx = convertGXtoWX(gx); + double wy = convertGYtoWY(gy); + // calculate world values for aperture + double wxAp = convertGXtoWX(radius + 2) - convertGXtoWX(0); + // LOGGER.debug("wxAp = " + wxAp ); + double wyAp = convertGYtoWY(0) - convertGYtoWY(radius + 2); + // LOGGER.debug("wyAp = " + wyAp ); + int pnt = pickPoint(wx, wxAp, wy, wyAp); + // LOGGER.debug("pickPoint = " + pnt); + if (shiftDown) { + if (pnt >= 0) { + points.removePoint(pnt); + repaint(); + } + } else { + if (pnt < 0) // didn't hit one so look for point to left of click + { + if (points.size() < maxPoints) // add if room + { + pnt = findPointBefore(wx); + // LOGGER.debug("pointBefore = " + pnt); + dragIndex = pnt + 1; + if (pnt == (points.size() - 1)) { + points.add(wx - xBefore, wy); + } else { + points.insert(dragIndex, wx - xBefore, wy); + } + dragLowLimit = xBefore; + dragHighLimit = wx + (maximumXRange - points.getTotalDuration()); + repaint(); + } + } else + // hit one so drag it + { + dragIndex = pnt; + if (dragIndex <= 0) + dragLowLimit = 0.0; // FIXME envelope drag limit + else + dragLowLimit = xPicked - points.getPoint(dragIndex)[0]; + dragHighLimit = xPicked + (maximumXRange - points.getTotalDuration()); + // LOGGER.debug("dragLowLimit = " + dragLowLimit ); + } + } + // Set up drag point if we are dragging. + if (dragIndex >= 0) { + draggedPoint = points.getPoint(dragIndex); + } + + } + + private void dragPoint(int gx, int gy) { + if (dragIndex < 0) + return; + + double wx = convertGXtoWX(gx); + if (wx < dragLowLimit) + wx = dragLowLimit; + else if (wx > dragHighLimit) + wx = dragHighLimit; + draggedPoint[0] = wx - dragLowLimit; // duration + + double wy = convertGYtoWY(gy); + wy = clipWorldY(wy); + draggedPoint[1] = wy; + dragWY = wy; + dragWX = wx; + points.setDirty(true); + repaint(); + } + + private void clickUpPoints(int gx, int gy) { + dragPoint(gx, gy); + fireObjectEdited(); + dragIndex = -1; + } + + // Implement the MouseMotionListener interface for AWT 1.1 + @Override + public void mouseDragged(MouseEvent e) { + int x = e.getX(); + int y = e.getY(); + if (points == null) + return; + if (mode == EDIT_POINTS) { + dragPoint(x, y); + } else { + dragRange(x, y); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + } + + // Implement the MouseListener interface for AWT 1.1 + @Override + public void mousePressed(MouseEvent e) { + int x = e.getX(); + int y = e.getY(); + if (points == null) + return; + if (mode == EDIT_POINTS) { + clickDownPoints(e.isShiftDown(), x, y); + } else { + clickDownRange(e.isShiftDown(), x, y); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + } + + @Override + public void mouseReleased(MouseEvent e) { + int x = e.getX(); + int y = e.getY(); + if (points == null) + return; + if (mode == EDIT_POINTS) { + clickUpPoints(x, y); + } else { + clickUpRange(x, y); + } + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + /** + * Draw selected range. + */ + private void drawRange(Graphics g) { + if (rangeStart >= 0) { + int height = getHeight(); + int gx0 = 0, gx1 = 0; + + if (rangeEnd < rangeStart) { + gx0 = rangeEnd; + gx1 = rangeStart; + } else { + gx0 = rangeStart; + gx1 = rangeEnd; + } + g.setColor(rangeColor); + g.fillRect(gx0, 0, gx1 - gx0, height); + } + } + + private void drawUnderSelection(Graphics g, int start, int end) { + if (start >= 0) { + int height = getHeight(); + int gx0 = 0, gx1 = radius; + double wx = 0.0; + for (int i = 0; i <= (end - 1); i++) { + double dar[] = (double[]) points.elementAt(i); + wx += dar[0]; + if (start == (i + 1)) { + gx0 = convertWXtoGX(wx) + radius; + } + if (end == (i + 1)) { + gx1 = convertWXtoGX(wx) + radius; + } + } + if (gx0 == gx1) + gx0 = gx0 - radius; + g.fillRect(gx0, 0, gx1 - gx0, height); + } + } + + private void drawSelections(Graphics g) { + int sus0 = points.getSustainBegin(); + int sus1 = points.getSustainEnd(); + int rel0 = points.getReleaseBegin(); + int rel1 = points.getReleaseEnd(); + + g.setColor(sustainColor); + drawUnderSelection(g, sus0, sus1); + g.setColor(releaseColor); + drawUnderSelection(g, rel0, rel1); + // draw overlapping sustain and release region + if (sus1 >= rel0) { + int sel1 = (rel1 < sus1) ? rel1 : sus1; + g.setColor(overlapColor); + drawUnderSelection(g, rel0, sel1); + } + } + + /** + * Override this to draw a grid or other stuff under the envelope. + */ + public void drawUnderlay(Graphics g) { + if (dragIndex < 0) { + drawSelections(g); + drawRange(g); + } + if (verticalBarsEnabled) + drawVerticalBars(g); + } + + public void setVerticalBarsEnabled(boolean flag) { + verticalBarsEnabled = flag; + } + + public boolean areVerticalBarsEnabled() { + return verticalBarsEnabled; + } + + /** + * Set spacing in world coordinates. + */ + public void setVerticalBarSpacing(double spacing) { + verticalBarSpacing = spacing; + } + + public double getVerticalBarSpacing() { + return verticalBarSpacing; + } + + /** + * Draw vertical lines. + */ + private void drawVerticalBars(Graphics g) { + int width = getWidth(); + int height = getHeight(); + double wx = verticalBarSpacing; + int gx; + + // g.setColor( getBackground().darker() ); + g.setColor(Color.lightGray); + while (true) { + gx = convertWXtoGX(wx); + if (gx > width) + break; + g.drawLine(gx, 0, gx, height); + wx += verticalBarSpacing; + } + } + + public void drawPoints(Graphics g, Color lineColor) { + double wx = 0.0; + int gx1 = 0; + int gy1 = getHeight(); + for (int i = 0; i < points.size(); i++) { + double dar[] = (double[]) points.elementAt(i); + wx += dar[0]; + double wy = dar[1]; + int gx2 = convertWXtoGX(wx); + int gy2 = convertWYtoGY(wy); + if (i == 0) { + g.setColor(isEnabled() ? firstLineColor : firstLineColor.darker()); + g.drawLine(gx1, gy1, gx2, gy2); + g.setColor(isEnabled() ? lineColor : lineColor.darker()); + } else if (i > 0) { + g.drawLine(gx1, gy1, gx2, gy2); + } + int diameter = (2 * radius) + 1; + g.fillOval(gx2 - radius, gy2 - radius, diameter, diameter); + gx1 = gx2; + gy1 = gy2; + } + } + + public void drawAllPoints(Graphics g) { + drawPoints(g, getForeground()); + } + + /* Override default paint action. */ + @Override + public void paint(Graphics g) { + double wx = 0.0; + int width = getWidth(); + int height = getHeight(); + + // draw background and erase all values + g.setColor(isEnabled() ? getBackground() : getBackground().darker()); + g.fillRect(0, 0, width, height); + + if (points == null) { + g.setColor(getForeground()); + g.drawString("No EnvelopePoints", 10, 30); + return; + } + + // Determine total duration. + if (points.size() > 0) { + wx = points.getTotalDuration(); + // Adjust max X so that we see entire circle of last point. + double radiusWX = this.convertGXtoWX(radius) - this.getMinWorldX(); + double wxFar = wx + radiusWX; + if (wxFar > getMaxWorldX()) { + if (wx > maximumXRange) + wxFar = maximumXRange; + setMaxWorldX(wxFar); + } else if (wx < (getMaxWorldX() * 0.7)) { + double newMax = wx / 0.7001; // make slightly larger to prevent + // endless jitter, FIXME - still + // needed after repaint() + // removed from setMaxWorldX? + // LOGGER.debug("newMax = " + newMax ); + if (newMax < minimumXRange) + newMax = minimumXRange; + setMaxWorldX(newMax); + } + } + // LOGGER.debug("total X = " + wx ); + + drawUnderlay(g); + + drawAllPoints(g); + + /* Show X,Y,TotalX as text. */ + g.drawString(points.getName() + ", len=" + String.format("%7.3f", wx), 5, 15); + if ((draggedPoint != null) && (dragIndex >= 0)) { + String s = "i=" + dragIndex + ", dur=" + + String.format("%7.3f", draggedPoint[0]) + ", y = " + + String.format("%8.4f", draggedPoint[1]); + g.drawString(s, 5, 30); + } + } +} diff --git a/src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java b/src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java new file mode 100644 index 0000000..dc9f2cd --- /dev/null +++ b/src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java @@ -0,0 +1,164 @@ +/* + * Copyright 2009 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import java.awt.BorderLayout; +import java.awt.Button; +import java.awt.Checkbox; +import java.awt.CheckboxGroup; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Label; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; + +import javax.swing.JPanel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +public class EnvelopeEditorPanel extends JPanel { + EnvelopeEditorBox editor; + Checkbox pointsBox; + Checkbox sustainBox; + Checkbox releaseBox; + Checkbox autoBox; + Button onButton; + Button offButton; + Button clearButton; + Button yUpButton; + Button yDownButton; + DoubleBoundedTextField zoomField; + + public EnvelopeEditorPanel(EnvelopePoints points, int maxFrames) { + setSize(600, 300); + + setLayout(new BorderLayout()); + editor = new EnvelopeEditorBox(); + editor.setMaxPoints(maxFrames); + editor.setBackground(Color.cyan); + editor.setPoints(points); + editor.setMinimumSize(new Dimension(500, 300)); + + add(editor, "Center"); + + JPanel buttonPanel = new JPanel(); + add(buttonPanel, "South"); + + CheckboxGroup cbg = new CheckboxGroup(); + pointsBox = new Checkbox("points", cbg, true); + pointsBox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + editor.setMode(EnvelopeEditorBox.EDIT_POINTS); + } + }); + buttonPanel.add(pointsBox); + + sustainBox = new Checkbox("onLoop", cbg, false); + sustainBox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + editor.setMode(EnvelopeEditorBox.SELECT_SUSTAIN); + } + }); + buttonPanel.add(sustainBox); + + releaseBox = new Checkbox("offLoop", cbg, false); + releaseBox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + editor.setMode(EnvelopeEditorBox.SELECT_RELEASE); + } + }); + buttonPanel.add(releaseBox); + + autoBox = new Checkbox("AutoStop", false); + /* + * buttonPanel.add( onButton = new Button( "On" ) ); onButton.addActionListener( module ); + * buttonPanel.add( offButton = new Button( "Off" ) ); offButton.addActionListener( module + * ); buttonPanel.add( clearButton = new Button( "Clear" ) ); clearButton.addActionListener( + * module ); + */ + buttonPanel.add(yUpButton = new Button("Y*2")); + yUpButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + scaleEnvelopeValues(2.0); + } + }); + + buttonPanel.add(yDownButton = new Button("Y/2")); + yDownButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + scaleEnvelopeValues(0.5); + } + }); + + /* Add a TextField for setting the Y scale. */ + double max = getMaxEnvelopeValue(editor.getPoints()); + editor.setMaxWorldY(max); + buttonPanel.add(new Label("YMax =")); + final DoubleBoundedRangeModel model = new DoubleBoundedRangeModel("YMax", 100000, 1.0, + 100001.0, 1.0); + buttonPanel.add(zoomField = new DoubleBoundedTextField(model, 8)); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + try { + double val = model.getDoubleValue(); + editor.setMaxWorldY(val); + editor.repaint(); + } catch (NumberFormatException exp) { + zoomField.setText("ERROR"); + zoomField.selectAll(); + } + } + }); + + validate(); + } + + /** + * Multiply all the values in the envelope by scalar. + */ + double getMaxEnvelopeValue(EnvelopePoints points) { + double max = 1.0; + for (int i = 0; i < points.size(); i++) { + double value = points.getValue(i); + if (value > max) { + max = value; + } + } + return max; + } + + /** + * Multiply all the values in the envelope by scalar. + */ + void scaleEnvelopeValues(double scalar) { + EnvelopePoints points = editor.getPoints(); + for (int i = 0; i < points.size(); i++) { + double[] dar = points.getPoint(i); + dar[1] = dar[1] * scalar; // scale value + } + points.setDirty(true); + editor.repaint(); + } +} diff --git a/src/main/java/com/jsyn/swing/EnvelopePoints.java b/src/main/java/com/jsyn/swing/EnvelopePoints.java new file mode 100644 index 0000000..ab4ed03 --- /dev/null +++ b/src/main/java/com/jsyn/swing/EnvelopePoints.java @@ -0,0 +1,234 @@ +/* + * Copyright 1997 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import java.util.Vector; + +import com.jsyn.data.SegmentedEnvelope; + +/** + * Vector that contains duration,value pairs. Used by EnvelopeEditor + * + * @author (C) 1997 Phil Burk, SoftSynth.com + */ + +/* ========================================================================== */ +public class EnvelopePoints extends Vector { + private String name = ""; + private double maximumValue = 1.0; + private int sustainBegin = -1; + private int sustainEnd = -1; + private int releaseBegin = -1; + private int releaseEnd = -1; + private boolean dirty = false; + + /** + * Update only if points or loops were modified. + */ + public void updateEnvelopeIfDirty(SegmentedEnvelope envelope) { + if (dirty) { + updateEnvelope(envelope); + } + } + + /** + * The editor works on a vector of points, not a real envelope. The data must be written to a + * real SynthEnvelope in order to use it. + */ + public void updateEnvelope(SegmentedEnvelope envelope) { + int numFrames = size(); + for (int i = 0; i < numFrames; i++) { + envelope.write(i, getPoint(i), 0, 1); + } + envelope.setSustainBegin(getSustainBegin()); + envelope.setSustainEnd(getSustainEnd()); + envelope.setReleaseBegin(getReleaseBegin()); + envelope.setReleaseEnd(getReleaseEnd()); + envelope.setNumFrames(numFrames); + dirty = false; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setMaximumValue(double maximumValue) { + this.maximumValue = maximumValue; + } + + public double getMaximumValue() { + return maximumValue; + } + + public void add(double dur, double value) { + double dar[] = { + dur, value + }; + addElement(dar); + dirty = true; + } + + /** + * Insert point without changing total duration by reducing next points duration. + */ + public void insert(int index, double dur, double y) { + double dar[] = { + dur, y + }; + if (index < size()) { + ((double[]) elementAt(index))[0] -= dur; + } + insertElementAt(dar, index); + + if (index <= sustainBegin) + sustainBegin += 1; + if (index <= sustainEnd) + sustainEnd += 1; + if (index <= releaseBegin) + releaseBegin += 1; + if (index <= releaseEnd) + releaseEnd += 1; + dirty = true; + } + + /** + * Remove indexed point and update sustain and release loops if necessary. Did not name this + * "remove()" because of conflicts with new JDK 1.3 method with the same name. + */ + public void removePoint(int index) { + super.removeElementAt(index); + // move down loop if points below or inside loop removed + if (index < sustainBegin) + sustainBegin -= 1; + if (index <= sustainEnd) + sustainEnd -= 1; + if (index < releaseBegin) + releaseBegin -= 1; + if (index <= releaseEnd) + releaseEnd -= 1; + + // was entire loop removed? + if (sustainBegin > sustainEnd) { + sustainBegin = -1; + sustainEnd = -1; + } + // was entire loop removed? + if (releaseBegin > releaseEnd) { + releaseBegin = -1; + releaseEnd = -1; + } + dirty = true; + } + + public double getDuration(int index) { + return ((double[]) elementAt(index))[0]; + } + + public double getValue(int index) { + return ((double[]) elementAt(index))[1]; + } + + public double[] getPoint(int index) { + return (double[]) elementAt(index); + } + + public double getTotalDuration() { + double sum = 0.0; + for (int i = 0; i < size(); i++) { + double dar[] = (double[]) elementAt(i); + sum += dar[0]; + } + return sum; + } + + /** + * Set location of Sustain Loop in units of Frames. Set SustainBegin to -1 if no Sustain Loop. + * SustainEnd value is the frame index of the frame just past the end of the loop. The number of + * frames included in the loop is (SustainEnd - SustainBegin). + */ + public void setSustainLoop(int startFrame, int endFrame) { + this.sustainBegin = startFrame; + this.sustainEnd = endFrame; + dirty = true; + } + + /*** + * @return Beginning of sustain loop or -1 if no loop. + */ + public int getSustainBegin() { + return this.sustainBegin; + } + + /*** + * @return End of sustain loop or -1 if no loop. + */ + public int getSustainEnd() { + return this.sustainEnd; + } + + /*** + * @return Size of sustain loop in frames, 0 if no loop. + */ + public int getSustainSize() { + return (this.sustainEnd - this.sustainBegin); + } + + /** + * Set location of Release Loop in units of Frames. Set ReleaseBegin to -1 if no ReleaseLoop. + * ReleaseEnd value is the frame index of the frame just past the end of the loop. The number of + * frames included in the loop is (ReleaseEnd - ReleaseBegin). + */ + public void setReleaseLoop(int startFrame, int endFrame) { + this.releaseBegin = startFrame; + this.releaseEnd = endFrame; + dirty = true; + } + + /*** + * @return Beginning of release loop or -1 if no loop. + */ + public int getReleaseBegin() { + return this.releaseBegin; + } + + /*** + * @return End of release loop or -1 if no loop. + */ + public int getReleaseEnd() { + return this.releaseEnd; + } + + /*** + * @return Size of release loop in frames, 0 if no loop. + */ + public int getReleaseSize() { + return (this.releaseEnd - this.releaseBegin); + } + + public boolean isDirty() { + return dirty; + } + + public void setDirty(boolean b) { + dirty = b; + } + +} diff --git a/src/main/java/com/jsyn/swing/ExponentialRangeModel.java b/src/main/java/com/jsyn/swing/ExponentialRangeModel.java new file mode 100644 index 0000000..c807000 --- /dev/null +++ b/src/main/java/com/jsyn/swing/ExponentialRangeModel.java @@ -0,0 +1,110 @@ +/* + * Copyright 2011 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maps integer range info to a double value along an exponential scale. + * + * <pre> + * + * x = ival / resolution + * f(x) = a*(rootˆcx) + b + * f(0.0) = dmin + * f(1.0) = dmax + * b = dmin - a + * a = (dmax - dmin) / (rootˆc - 1) + * + * Inverse function: + * x = log( (y-b)/a ) / log(root) + * + * </pre> + * + * @author Phil Burk, (C) 2011 Mobileer Inc + */ +public class ExponentialRangeModel extends DoubleBoundedRangeModel { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExponentialRangeModel.class); + private static final long serialVersionUID = -142785624892302160L; + + double a = 1.0; + double b = -1.0; + double span = 1.0; + double root = 10.0; + + /** Use default root of 10.0 and span of 1.0. */ + public ExponentialRangeModel(String name, int resolution, double dmin, double dmax, double dval) { + this(name, resolution, dmin, dmax, dval, 1.0); + } + + /** Set span before setting double value so it is translated correctly. */ + ExponentialRangeModel(String name, int resolution, double dmin, double dmax, double dval, + double span) { + super(name, resolution, dmin, dmax, dval); + setRoot(10.0); + setSpan(span); + /* Set again after coefficients setup. */ + setDoubleValue(dval); + } + + private void updateCoefficients() { + a = (getDoubleMaximum() - getDoubleMinimum()) / (Math.pow(root, span) - 1.0); + b = getDoubleMinimum() - a; + } + + private void setRoot(double w) { + root = w; + updateCoefficients(); + } + + public double getRoot() { + return root; + } + + public void setSpan(double c) { + this.span = c; + updateCoefficients(); + } + + public double getSpan() { + return span; + } + + @Override + public double sliderToDouble(int sliderValue) { + updateCoefficients(); // TODO optimize when we call this + double x = (double) sliderValue / getMaximum(); + return (a * Math.pow(root, span * x)) + b; + } + + @Override + public int doubleToSlider(double dval) { + updateCoefficients(); // TODO optimize when we call this + double z = (dval - b) / a; + double x = Math.log(z) / (span * Math.log(root)); + return (int) Math.round(x * getMaximum()); + } + + public void test(int sliderValue) { + double dval = sliderToDouble(sliderValue); + int ival = doubleToSlider(dval); + LOGGER.debug(sliderValue + " => " + dval + " => " + ival); + } + +} diff --git a/src/main/java/com/jsyn/swing/InstrumentBrowser.java b/src/main/java/com/jsyn/swing/InstrumentBrowser.java new file mode 100644 index 0000000..8e74660 --- /dev/null +++ b/src/main/java/com/jsyn/swing/InstrumentBrowser.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import java.awt.Dimension; +import java.awt.GridLayout; +import java.util.ArrayList; + +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import com.jsyn.util.InstrumentLibrary; +import com.jsyn.util.VoiceDescription; + +/** + * Display a list of VoiceDescriptions and their associated presets. Notify PresetSelectionListeners + * when a preset is selected. + * + * @author Phil Burk (C) 2012 Mobileer Inc + */ +@SuppressWarnings("serial") +public class InstrumentBrowser extends JPanel { + private InstrumentLibrary library; + private JScrollPane listScroller2; + private VoiceDescription voiceDescription; + private ArrayList<PresetSelectionListener> listeners = new ArrayList<>(); + + public InstrumentBrowser(InstrumentLibrary library) { + this.library = library; + JPanel horizontalPanel = new JPanel(); + horizontalPanel.setLayout(new GridLayout(1, 2)); + + final JList<VoiceDescription> instrumentList = new JList<VoiceDescription>(library.getVoiceDescriptions()); + setupList(instrumentList); + instrumentList.addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + if (!e.getValueIsAdjusting()) { + int n = instrumentList.getSelectedIndex(); + if (n >= 0) { + showPresetList(n); + } + } + } + }); + + JScrollPane listScroller1 = new JScrollPane(instrumentList); + listScroller1.setPreferredSize(new Dimension(250, 120)); + add(listScroller1); + + instrumentList.setSelectedIndex(0); + } + + public void addPresetSelectionListener(PresetSelectionListener listener) { + listeners.add(listener); + } + + public void removePresetSelectionListener(PresetSelectionListener listener) { + listeners.remove(listener); + } + + private void firePresetSelectionListeners(VoiceDescription voiceDescription, int presetIndex) { + for (PresetSelectionListener listener : listeners) { + listener.presetSelected(voiceDescription, presetIndex); + } + } + + private void showPresetList(int n) { + if (listScroller2 != null) { + remove(listScroller2); + } + voiceDescription = library.getVoiceDescriptions()[n]; + final JList<String> presetList = new JList<String>(voiceDescription.getPresetNames()); + setupList(presetList); + presetList.addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getValueIsAdjusting() == false) { + int n = presetList.getSelectedIndex(); + if (n >= 0) { + firePresetSelectionListeners(voiceDescription, n); + } + } + } + }); + + listScroller2 = new JScrollPane(presetList); + listScroller2.setPreferredSize(new Dimension(250, 120)); + add(listScroller2); + presetList.setSelectedIndex(0); + validate(); + } + + private void setupList(@SuppressWarnings("rawtypes") JList list) { + list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); + list.setLayoutOrientation(JList.VERTICAL); + list.setVisibleRowCount(-1); + } +} diff --git a/src/main/java/com/jsyn/swing/JAppletFrame.java b/src/main/java/com/jsyn/swing/JAppletFrame.java new file mode 100644 index 0000000..53bd65b --- /dev/null +++ b/src/main/java/com/jsyn/swing/JAppletFrame.java @@ -0,0 +1,65 @@ +/* + * Copyright 1997 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.JApplet; +import javax.swing.JFrame; + +/** + * Frame that allows a program to be run as either an Application or an Applet. Used by JSyn example + * programs. + * + * @author (C) 1997 Phil Burk, SoftSynth.com + */ + +public class JAppletFrame extends JFrame { + private static final long serialVersionUID = -6047247494856379114L; + JApplet applet; + + public JAppletFrame(String frameTitle, final JApplet pApplet) { + super(frameTitle); + this.applet = pApplet; + getContentPane().add(applet); + repaint(); + + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + applet.stop(); + applet.destroy(); + try { + System.exit(0); + } catch (SecurityException exc) { + System.err.println("System.exit(0) not allowed by Java VM."); + } + } + + @Override + public void windowClosed(WindowEvent e) { + } + }); + } + + public void test() { + applet.init(); + applet.start(); + } + +} diff --git a/src/main/java/com/jsyn/swing/PortBoundedRangeModel.java b/src/main/java/com/jsyn/swing/PortBoundedRangeModel.java new file mode 100644 index 0000000..a5cf841 --- /dev/null +++ b/src/main/java/com/jsyn/swing/PortBoundedRangeModel.java @@ -0,0 +1,45 @@ +/* + * Copyright 2011 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jsyn.ports.UnitInputPort; + +/** + * A bounded range model that drives a UnitInputPort. The range of the model is set based on the min + * and max of the port. + * + * @author Phil Burk (C) 2011 Mobileer Inc + */ +public class PortBoundedRangeModel extends DoubleBoundedRangeModel { + private static final long serialVersionUID = -8011867146560305808L; + private UnitInputPort port; + + public PortBoundedRangeModel(UnitInputPort pPort) { + super(pPort.getName(), 10000, pPort.getMinimum(), pPort.getMaximum(), pPort.getValue()); + this.port = pPort; + addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + port.set(getDoubleValue()); + } + }); + } + +} diff --git a/src/main/java/com/jsyn/swing/PortControllerFactory.java b/src/main/java/com/jsyn/swing/PortControllerFactory.java new file mode 100644 index 0000000..a73d047 --- /dev/null +++ b/src/main/java/com/jsyn/swing/PortControllerFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2010 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jsyn.ports.UnitInputPort; + +/** + * Factory class for making various controllers for JSyn ports. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class PortControllerFactory { + private static final int RESOLUTION = 100000; + + public static DoubleBoundedRangeSlider createPortSlider(final UnitInputPort port) { + DoubleBoundedRangeModel rangeModel = new DoubleBoundedRangeModel(port.getName(), + RESOLUTION, port.getMinimum(), port.getMaximum(), port.get()); + rangeModel.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) e.getSource(); + double value = model.getDoubleValue(); + port.set(value); + } + }); + return new DoubleBoundedRangeSlider(rangeModel, 4); + } + + public static DoubleBoundedRangeSlider createExponentialPortSlider(final UnitInputPort port) { + ExponentialRangeModel rangeModel = new ExponentialRangeModel(port.getName(), RESOLUTION, + port.getMinimum(), port.getMaximum(), port.get()); + rangeModel.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + ExponentialRangeModel model = (ExponentialRangeModel) e.getSource(); + double value = model.getDoubleValue(); + port.set(value); + } + }); + return new DoubleBoundedRangeSlider(rangeModel, 4); + } + +} diff --git a/src/main/java/com/jsyn/swing/PortModelFactory.java b/src/main/java/com/jsyn/swing/PortModelFactory.java new file mode 100644 index 0000000..8bec76a --- /dev/null +++ b/src/main/java/com/jsyn/swing/PortModelFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright 2009 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.jsyn.ports.UnitInputPort; + +public class PortModelFactory { + private static final int RESOLUTION = 1000000; + + public static DoubleBoundedRangeModel createLinearModel(final UnitInputPort pPort) { + final DoubleBoundedRangeModel model = new DoubleBoundedRangeModel(pPort.getName(), + RESOLUTION, pPort.getMinimum(), pPort.getMaximum(), pPort.get()); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + pPort.set(model.getDoubleValue()); + } + }); + return model; + } + + public static ExponentialRangeModel createExponentialModel(final UnitInputPort pPort) { + final ExponentialRangeModel model = new ExponentialRangeModel(pPort.getName(), RESOLUTION, + pPort.getMinimum(), pPort.getMaximum(), pPort.get()); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + pPort.set(model.getDoubleValue()); + } + }); + return model; + } + + public static ExponentialRangeModel createExponentialModel(final int partNum, + final UnitInputPort pPort) { + final ExponentialRangeModel model = new ExponentialRangeModel(pPort.getName(), RESOLUTION, + pPort.getMinimum(), pPort.getMaximum(), pPort.get()); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + pPort.set(partNum, model.getDoubleValue()); + } + }); + return model; + } + +} diff --git a/src/main/java/com/jsyn/swing/PresetSelectionListener.java b/src/main/java/com/jsyn/swing/PresetSelectionListener.java new file mode 100644 index 0000000..daf0310 --- /dev/null +++ b/src/main/java/com/jsyn/swing/PresetSelectionListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2009 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import com.jsyn.util.VoiceDescription; + +public interface PresetSelectionListener { + public void presetSelected(VoiceDescription voiceDescription, int presetIndex); +} diff --git a/src/main/java/com/jsyn/swing/RotaryController.java b/src/main/java/com/jsyn/swing/RotaryController.java new file mode 100644 index 0000000..c26c37f --- /dev/null +++ b/src/main/java/com/jsyn/swing/RotaryController.java @@ -0,0 +1,335 @@ +/* + * Copyright 2010 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; + +import javax.swing.BoundedRangeModel; +import javax.swing.JPanel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * Rotary controller looks like a knob on a synthesizer. You control this knob by clicking on it and + * dragging <b>up</b> or <b>down</b>. If you move the mouse to the <b>left</b> of the knob then you + * will have <b>coarse</b> control. If you move the mouse to the <b>right</b> of the knob then you + * will have <b>fine</b> control. + * <P> + * + * @author (C) 2010 Phil Burk, Mobileer Inc + * @version 16.1 + */ +public class RotaryController extends JPanel { + private static final long serialVersionUID = 6681532871556659546L; + private static final double SENSITIVITY = 0.01; + private final BoundedRangeModel model; + + private final double minAngle = 1.4 * Math.PI; + private final double maxAngle = -0.4 * Math.PI; + private final double unitIncrement = 0.01; + private int lastY; + private int startX; + private Color knobColor = Color.LIGHT_GRAY; + private Color lineColor = Color.RED; + private double baseValue; + + public enum Style { + LINE, LINEDOT, ARROW, ARC + }; + + private Style style = Style.ARC; + + public RotaryController(BoundedRangeModel model) { + this.model = model; + setMinimumSize(new Dimension(50, 50)); + setPreferredSize(new Dimension(50, 50)); + addMouseListener(new MouseHandler()); + addMouseMotionListener(new MouseMotionHandler()); + model.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + safeRepaint(); + } + + }); + } + + // This can be overridden in subclasses to workaround OpenJDK bugs. + public void safeRepaint() { + repaint(); + } + + public BoundedRangeModel getModel() { + return model; + } + + private class MouseHandler extends MouseAdapter { + + @Override + public void mousePressed(MouseEvent e) { + lastY = e.getY(); + startX = e.getX(); + } + + @Override + public void mouseReleased(MouseEvent e) { + if (isEnabled()) { + setKnobByXY(e.getX(), e.getY()); + } + } + } + + private class MouseMotionHandler extends MouseMotionAdapter { + @Override + public void mouseDragged(MouseEvent e) { + if (isEnabled()) { + setKnobByXY(e.getX(), e.getY()); + } + } + } + + private int getModelRange() { + return (((model.getMaximum() - model.getExtent()) - model.getMinimum())); + } + + /** + * A fractional value is useful for drawing. + * + * @return model value as a normalized fraction between 0.0 and 1.0 + */ + public double getFractionFromModel() { + double value = model.getValue(); + return convertValueToFraction(value); + } + + private double convertValueToFraction(double value) { + return (value - model.getMinimum()) / getModelRange(); + } + + private void setKnobByXY(int x, int y) { + // Scale increment by X position. + int xdiff = startX - x; // More to left causes bigger increments. + double power = xdiff * SENSITIVITY; + double perPixel = unitIncrement * Math.pow(2.0, power); + + int ydiff = lastY - y; + double fractionalDelta = ydiff * perPixel; + // Only update the model if we actually change values. + // This is needed in case the range is small. + int valueDelta = (int) Math.round(fractionalDelta * getModelRange()); + if (valueDelta != 0) { + model.setValue(model.getValue() + valueDelta); + lastY = y; + } + } + + private double fractionToAngle(double fraction) { + return (fraction * (maxAngle - minAngle)) + minAngle; + } + + private void drawLineIndicator(Graphics g, int x, int y, int radius, double angle, + boolean drawDot) { + double arrowSize = radius * 0.95; + int arrowX = (int) (arrowSize * Math.sin(angle)); + int arrowY = (int) (arrowSize * Math.cos(angle)); + g.setColor(lineColor); + g.drawLine(x, y, x + arrowX, y - arrowY); + if (drawDot) { + // draw little dot at end + double dotScale = 0.1; + int dotRadius = (int) (dotScale * arrowSize); + if (dotRadius > 1) { + int dotX = x + (int) ((0.99 - dotScale) * arrowX) - dotRadius; + int dotY = y - (int) ((0.99 - dotScale) * arrowY) - dotRadius; + g.fillOval(dotX, dotY, dotRadius * 2, dotRadius * 2); + } + } + } + + private void drawArrowIndicator(Graphics g, int x0, int y0, int radius, double angle) { + int arrowSize = (int) (radius * 0.95); + int arrowWidth = (int) (radius * 0.2); + int xp[] = { + 0, arrowWidth, 0, -arrowWidth + }; + int yp[] = { + arrowSize, -arrowSize / 2, 0, -arrowSize / 2 + }; + double sa = Math.sin(angle); + double ca = Math.cos(angle); + for (int i = 0; i < xp.length; i++) { + int x = xp[i]; + int y = yp[i]; + xp[i] = x0 - (int) ((x * ca) - (y * sa)); + yp[i] = y0 - (int) ((x * sa) + (y * ca)); + } + g.fillPolygon(xp, yp, xp.length); + } + + private void drawArcIndicator(Graphics g, int x, int y, int radius, double angle) { + final double DEGREES_PER_RADIAN = 180.0 / Math.PI; + final int minAngleDegrees = (int) (minAngle * DEGREES_PER_RADIAN); + final int maxAngleDegrees = (int) (maxAngle * DEGREES_PER_RADIAN); + + int zeroAngleDegrees = (int) (fractionToAngle(baseValue) * DEGREES_PER_RADIAN); + + double arrowSize = radius * 0.95; + int arcX = x - radius; + int arcY = y - radius; + int arcAngle = (int) (angle * DEGREES_PER_RADIAN); + int arrowX = (int) (arrowSize * Math.cos(angle)); + int arrowY = (int) (arrowSize * Math.sin(angle)); + + g.setColor(knobColor.darker().darker()); + g.fillArc(arcX, arcY, 2 * radius, 2 * radius, minAngleDegrees, maxAngleDegrees + - minAngleDegrees); + g.setColor(Color.ORANGE); + g.fillArc(arcX, arcY, 2 * radius, 2 * radius, zeroAngleDegrees, arcAngle - zeroAngleDegrees); + + // fill in middle + int arcWidth = radius / 4; + int diameter = ((radius - arcWidth) * 2); + g.setColor(knobColor); + g.fillOval(arcWidth + x - radius, arcWidth + y - radius, diameter, diameter); + + g.setColor(lineColor); + g.drawLine(x, y, x + arrowX, y - arrowY); + + } + + /** + * Override this method if you want to draw your own line or dot on the knob. + */ + public void drawIndicator(Graphics g, int x, int y, int radius, double angle) { + g.setColor(isEnabled() ? lineColor : lineColor.darker()); + switch (style) { + case LINE: + drawLineIndicator(g, x, y, radius, angle, false); + break; + case LINEDOT: + drawLineIndicator(g, x, y, radius, angle, true); + break; + case ARROW: + drawArrowIndicator(g, x, y, radius, angle); + break; + case ARC: + drawArcIndicator(g, x, y, radius, angle); + break; + } + } + + /** + * Override this method if you want to draw your own knob. + * + * @param g graphics context + * @param x position of center of knob + * @param y position of center of knob + * @param radius of knob in pixels + * @param angle in radians. Zero is straight up. + */ + public void drawKnob(Graphics g, int x, int y, int radius, double angle) { + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + int diameter = radius * 2; + // Draw shaded side. + g.setColor(knobColor.darker()); + g.fillOval(x - radius + 2, y - radius + 2, diameter, diameter); + g.setColor(knobColor); + g.fillOval(x - radius, y - radius, diameter, diameter); + + // Draw line or other indicator of knob position. + drawIndicator(g, x, y, radius, angle); + } + + // Draw the round knob based on the current size and model value. + // This used to have a bug where the scope would draw in this components background. + // Then I changed it from overriding paint() to overriding paintComponent() and it worked. + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + + int width = getWidth(); + int height = getHeight(); + int x = width / 2; + int y = height / 2; + + // Calculate radius from size of component. + int diameter = (width < height) ? width : height; + diameter -= 4; + int radius = diameter / 2; + + double angle = fractionToAngle(getFractionFromModel()); + drawKnob(g, x, y, radius, angle); + } + + public Color getKnobColor() { + return knobColor; + } + + /** + * @param knobColor color of body of knob + */ + public void setKnobColor(Color knobColor) { + this.knobColor = knobColor; + } + + public Color getLineColor() { + return lineColor; + } + + /** + * @param lineColor color of indicator on knob like a line or arrow + */ + public void setLineColor(Color lineColor) { + this.lineColor = lineColor; + } + + public void setStyle(Style style) { + this.style = style; + } + + public Style getStyle() { + return style; + } + + public double getBaseValue() { + return baseValue; + } + + /* + * Specify where the orange arc originates. For example a pan knob with a centered arc would + * have a baseValue of 0.5. + * @param baseValue a fraction between 0.0 and 1.0. + */ + public void setBaseValue(double baseValue) { + if (baseValue < 0.0) { + baseValue = 0.0; + } else if (baseValue > 1.0) { + baseValue = 1.0; + } + this.baseValue = baseValue; + } + +} diff --git a/src/main/java/com/jsyn/swing/RotaryTextController.java b/src/main/java/com/jsyn/swing/RotaryTextController.java new file mode 100644 index 0000000..81d6614 --- /dev/null +++ b/src/main/java/com/jsyn/swing/RotaryTextController.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import java.awt.BorderLayout; + +import javax.swing.BorderFactory; +import javax.swing.JPanel; + +/** + * Combine a RotaryController and a DoubleBoundedTextField into a convenient package. + * + * @author Phil Burk (C) 2010 Mobileer Inc + */ +public class RotaryTextController extends JPanel { + private static final long serialVersionUID = -2931828326251895375L; + private RotaryController rotary; + private DoubleBoundedTextField textField; + + public RotaryTextController(DoubleBoundedRangeModel pModel, int numDigits) { + rotary = new RotaryController(pModel); + textField = new DoubleBoundedTextField(pModel, numDigits); + setLayout(new BorderLayout()); + add(rotary, BorderLayout.CENTER); + add(textField, BorderLayout.SOUTH); + } + + /** Display the title in a border. */ + public void setTitle(String label) { + setBorder(BorderFactory.createTitledBorder(label)); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + rotary.setEnabled(enabled); + textField.setEnabled(enabled); + } +} diff --git a/src/main/java/com/jsyn/swing/SoundTweaker.java b/src/main/java/com/jsyn/swing/SoundTweaker.java new file mode 100644 index 0000000..dc48c8f --- /dev/null +++ b/src/main/java/com/jsyn/swing/SoundTweaker.java @@ -0,0 +1,120 @@ +/* + * Copyright 2009 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import java.awt.Component; +import java.awt.GridLayout; +import java.util.ArrayList; +import java.util.logging.Logger; + +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.jsyn.Synthesizer; +import com.jsyn.ports.UnitInputPort; +import com.jsyn.ports.UnitPort; +import com.jsyn.unitgen.UnitGenerator; +import com.jsyn.unitgen.UnitSource; +import com.jsyn.unitgen.UnitVoice; +import com.jsyn.util.Instrument; +import com.softsynth.math.AudioMath; + +@SuppressWarnings("serial") +public class SoundTweaker extends JPanel { + private UnitSource source; + private ASCIIMusicKeyboard keyboard; + private Synthesizer synth; + + static Logger logger = Logger.getLogger(SoundTweaker.class.getName()); + + public SoundTweaker(Synthesizer synth, String title, UnitSource source) { + this.synth = synth; + this.source = source; + + setLayout(new GridLayout(0, 2)); + + UnitGenerator ugen = source.getUnitGenerator(); + ArrayList<Component> sliders = new ArrayList<Component>(); + + add(new JLabel(title)); + + if (source instanceof Instrument) { + add(keyboard = createPolyphonicKeyboard()); + } else if (source instanceof UnitVoice) { + add(keyboard = createMonophonicKeyboard()); + } + + // Arrange the faders in a stack. + // Iterate through the ports. + for (UnitPort port : ugen.getPorts()) { + if (port instanceof UnitInputPort) { + UnitInputPort inputPort = (UnitInputPort) port; + Component slider; + // Use an exponential slider if it seems appropriate. + if ((inputPort.getMinimum() > 0.0) + && ((inputPort.getMaximum() / inputPort.getMinimum()) > 4.0)) { + slider = PortControllerFactory.createExponentialPortSlider(inputPort); + } else { + slider = PortControllerFactory.createPortSlider(inputPort); + + } + add(slider); + sliders.add(slider); + } + } + + if (keyboard != null) { + for (Component slider : sliders) { + slider.addKeyListener(keyboard.getKeyListener()); + } + } + validate(); + } + + @SuppressWarnings("serial") + private ASCIIMusicKeyboard createPolyphonicKeyboard() { + return new ASCIIMusicKeyboard() { + @Override + public void keyOff(int pitch) { + ((Instrument) source).noteOff(pitch, synth.createTimeStamp()); + } + + @Override + public void keyOn(int pitch) { + double freq = AudioMath.pitchToFrequency(pitch); + ((Instrument) source).noteOn(pitch, freq, 0.5, synth.createTimeStamp()); + } + }; + } + + @SuppressWarnings("serial") + private ASCIIMusicKeyboard createMonophonicKeyboard() { + return new ASCIIMusicKeyboard() { + @Override + public void keyOff(int pitch) { + ((UnitVoice) source).noteOff(synth.createTimeStamp()); + } + + @Override + public void keyOn(int pitch) { + double freq = AudioMath.pitchToFrequency(pitch); + ((UnitVoice) source).noteOn(freq, 0.5, synth.createTimeStamp()); + } + }; + } + +} diff --git a/src/main/java/com/jsyn/swing/XYController.java b/src/main/java/com/jsyn/swing/XYController.java new file mode 100644 index 0000000..0d97c62 --- /dev/null +++ b/src/main/java/com/jsyn/swing/XYController.java @@ -0,0 +1,132 @@ +/* + * Copyright 1997 Phil Burk, Mobileer Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jsyn.swing; + +import javax.swing.JPanel; + +/** + * Root class for 2 dimensional X,Y controller for wave editors, Theremins, etc. Maps pixel + * coordinates into "world" coordinates. + * + * @author (C) 1997 Phil Burk, SoftSynth.com + */ + +public class XYController extends JPanel { + double minWorldX = 0.0; + double maxWorldX = 1.0; + double minWorldY = 0.0; + double maxWorldY = 1.0; + + public XYController() { + } + + public XYController(double minWX, double minWY, double maxWX, double maxWY) { + setMinWorldX(minWX); + setMaxWorldX(maxWX); + setMinWorldY(minWY); + setMaxWorldY(maxWY); + } + + /** + * Set minimum World coordinate value for the horizontal X dimension. The minimum value + * corresponds to the left of the component. + */ + public void setMinWorldX(double minWX) { + minWorldX = minWX; + } + + public double getMinWorldX() { + return minWorldX; + } + + /** + * Set maximum World coordinate value for the horizontal X dimension. The minimum value + * corresponds to the right of the component. + */ + public void setMaxWorldX(double maxWX) { + maxWorldX = maxWX; + } + + public double getMaxWorldX() { + return maxWorldX; + } + + /** + * Set minimum World coordinate value for the vertical Y dimension. The minimum value + * corresponds to the bottom of the component. + */ + public void setMinWorldY(double minWY) { + minWorldY = minWY; + } + + public double getMinWorldY() { + return minWorldY; + } + + /** + * Set maximum World coordinate value for the vertical Y dimension. The maximum value + * corresponds to the top of the component. + */ + public void setMaxWorldY(double maxWY) { + maxWorldY = maxWY; + } + + public double getMaxWorldY() { + return maxWorldY; + } + + /** Convert from graphics coordinates (pixels) to world coordinates. */ + public double convertGXtoWX(int gx) { + int width = getWidth(); + return minWorldX + ((maxWorldX - minWorldX) * gx) / width; + } + + public double convertGYtoWY(int gy) { + int height = getHeight(); + return minWorldY + ((maxWorldY - minWorldY) * (height - gy)) / height; + } + + /** Convert from world coordinates to graphics coordinates (pixels). */ + public int convertWXtoGX(double wx) { + int width = getWidth(); + return (int) (((wx - minWorldX) * width) / (maxWorldX - minWorldX)); + } + + public int convertWYtoGY(double wy) { + int height = getHeight(); + return height - (int) (((wy - minWorldY) * height) / (maxWorldY - minWorldY)); + } + + /** Clip wx to the min and max World X values. */ + public double clipWorldX(double wx) { + if (wx < minWorldX) + wx = minWorldX; + else if (wx > maxWorldX) + wx = maxWorldX; + return wx; + } + + /** Clip wy to the min and max World Y values. */ + public double clipWorldY(double wy) { + if (wy < minWorldY) + wy = minWorldY; + else if (wy > maxWorldY) + wy = maxWorldY; + return wy; + } + +} |