/* * Created on Jun 28, 2004 * */ package jmslexamples.jsyn2; import java.awt.BorderLayout; import java.awt.Color; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.Hashtable; import javax.swing.JFrame; import com.didkovsky.portview.PVButton; import com.didkovsky.portview.PVPanel; import com.didkovsky.portview.swing.ViewFactorySwing; import com.softsynth.jmsl.Composable; import com.softsynth.jmsl.DimensionNameSpace; import com.softsynth.jmsl.Instrument; import com.softsynth.jmsl.InstrumentPlayable; import com.softsynth.jmsl.JMSL; import com.softsynth.jmsl.JMSLMixerContainer; import com.softsynth.jmsl.JMSLRandom; import com.softsynth.jmsl.Limits; import com.softsynth.jmsl.MusicList; import com.softsynth.jmsl.ParallelCollection; import com.softsynth.jmsl.Playable; import com.softsynth.jmsl.jsyn2.JSynMusicDevice; import com.softsynth.jmsl.jsyn2.JSynUnitVoiceInstrument; import com.softsynth.jmsl.util.HalfCosineInterpolator; import com.softsynth.jmsl.util.Interpolator; import com.softsynth.jmsl.util.LinearInterpolator; import com.softsynth.jmsl.view.PVPanelAdapter; /** * Demonstrates continuous control by JMSL over JSyn synth parameters. Key idea: * for each pitch, instrument.on() starts a sound, instrument.update() changes * its timbre and can be called frequently at control rates. instrument.off() * terminates sound * * Use MusicList stuffed with ControlElement's whose durations are very short. * They turn an instrument on, update its timbre, turn it off * * Uses Interpolators to generate smoothly changing data * * JSyn2 version Dec 2016 ND * * Not an Applet any more, now a JFrame. Browsers do not support Applets any more. * * @author Nick Didkovsky, (c) 2004 Nick Didkovsky, All rights reserved, * nick@didkovsky.com * */ public class TimbralInterpolation extends JFrame { String unitVoiceClassName = com.softsynth.jmsl.jsyn2.unitvoices.FilteredSawtoothBL.class.getName(); JMSLMixerContainer mixer; PVButton playButton; PVButton finishButton; ParallelCollection parallelCollection; JSynUnitVoiceInstrument instrument; public void init() { JMSLRandom.randomize(); } public void build() { init(); setLayout(new BorderLayout()); initMusicDevice(); instrument = new JSynUnitVoiceInstrument(8, unitVoiceClassName); buildMixer(); buildButtons(); } public void stop() { if (parallelCollection != null) parallelCollection.finishAll(); JMSL.closeMusicDevices(); removeAll(); } private void initMusicDevice() { JSynMusicDevice.instance().open(); JMSL.clock.setAdvance(0.5); } private void buildMixer() { mixer = new JMSLMixerContainer(); mixer.start(); mixer.addInstrument(instrument, 0.5, 0.60); add(BorderLayout.NORTH, mixer.getPanAmpControlPanel()); } private void buildContinuousTimbralControl() { TimbralGestureMaker gestureMaker = new TimbralGestureMaker(); gestureMaker.setInstrument(instrument); parallelCollection = new ParallelCollection(); int numGestures = JMSLRandom.choose(3, 10); System.out.println("\nBuilding " + numGestures + " gestures"); for (int i = 0; i < numGestures; i++) { double pitch = JMSLRandom.choose(30.0, 60.0); double duration = JMSLRandom.choose(6.0, 20.0); int controlStreamIdentifier = i; gestureMaker.generateGesture(pitch, duration, controlStreamIdentifier); MusicList ml = gestureMaker.getControllerList(); parallelCollection.add(ml); if (i != 0) ml.setStartDelay(JMSLRandom.choose(20.0)); } parallelCollection.setRepeats(Integer.MAX_VALUE); parallelCollection.addRepeatPlayable(new Playable() { public double play(double time, Composable parent) throws InterruptedException { System.out.println("ParallelCollection " + parent.getName() + " is repeating"); return time; } }); } private void buildButtons() { playButton = JMSL.getViewFactory().createButton("PLAY"); playButton.setBackground(Color.GREEN); playButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ev) { buildContinuousTimbralControl(); parallelCollection.launch(JMSL.now()); playButton.setEnabled(false); finishButton.setEnabled(true); } }); finishButton = JMSL.getViewFactory().createButton("FINISH"); finishButton.setBackground(Color.RED); finishButton.setEnabled(false); finishButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ev) { parallelCollection.finishAll(); playButton.setEnabled(true); finishButton.setEnabled(false); // shut off each instrument by playing last element in MusicList for (int i = 0; i < parallelCollection.size(); i++) { MusicList m = (MusicList) parallelCollection.get(i); ControlElement lastControlElement = (ControlElement) m.get(m.size() - 1); lastControlElement.play(JMSL.now(), null, m.getInstrument()); } } }); PVPanel p = new PVPanelAdapter(); p.setLayout(new FlowLayout()); p.add(playButton.getComponent()); p.add(finishButton.getComponent()); add(BorderLayout.SOUTH, p.getComponent()); } /* Can be run as either an application or as an applet. */ public static void main(String args[]) { JMSL.setViewFactory(new ViewFactorySwing()); TimbralInterpolation jf = new TimbralInterpolation(); jf.build(); jf.setSize(250, 250); jf.setVisible(true); jf.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { jf.stop(); System.exit(0); } }); } } class TimbralGestureMaker { public static final double CONTROL_RATE = 0.01; private Interpolator[] interpolators; private Instrument instrument; private MusicList controllerList; /** * Each on(), update(), off() sequence shall be identified by a unique index. We * use that index as a key to look up the voice token returned by on(). That way * update() can look up the voice token and control the correct voice, and so * can off() which also uses a voice token. * */ private Hashtable controlStreamIdentifierVoiceTokenLookup = new Hashtable(); public void setInstrument(Instrument instrument) { this.instrument = instrument; } private void initControllerList() { controllerList = new MusicList(); controllerList.setInstrument(instrument); } public MusicList getControllerList() { return controllerList; } public void generateGesture(double pitch, double seconds, int controlStreamIdentifier) { initControllerList(); DimensionNameSpace dns = instrument.getDimensionNameSpace(); double[] dar = generateFirstElement(pitch, seconds, dns, controlStreamIdentifier); generateUpdates(pitch, seconds, dns, dar, controlStreamIdentifier); ControlElement offControlElement = new ControlElement(ControlElement.OFF, null, controlStreamIdentifier, controlStreamIdentifierVoiceTokenLookup); controllerList.add(offControlElement); } private double[] generateFirstElement(double pitch, double seconds, DimensionNameSpace dns, int controlStreamIdentifier) { double amp = JMSLRandom.choose(); double[] dar = makeElement(pitch, seconds, dns); controllerList.add(new ControlElement(ControlElement.ON, dar, controlStreamIdentifier, controlStreamIdentifierVoiceTokenLookup)); return dar; } private double[] generateUpdates(double pitch, double seconds, DimensionNameSpace dns, double[] dar, int controlStreamIdentifier) { int steps = (int) (seconds / CONTROL_RATE); int stepsThisStage = JMSLRandom.choose(1, 100); buildInterpolators(0, dar, stepsThisStage); for (int i = 0; i < steps; i++) { dar = new double[dns.dimension()]; dar[0] = CONTROL_RATE; dar[1] = pitch; dar[3] = CONTROL_RATE; // not used for (int dimension = 0; dimension < dns.dimension(); dimension++) { if (notDurPitchHold(dimension)) { Interpolator interpolator = interpolators[dimension]; double lowLimit = dns.getLowLimit(dimension); double highLimit = dns.getHighLimit(dimension); if (!Limits.within(interpolator.interp(i), lowLimit, highLimit)) { System.err.println( interpolator.interp(i) + " out of range of [" + lowLimit + " , " + highLimit + "]"); } dar[dimension] = Limits.clipTo(interpolator.interp(i), lowLimit, highLimit); } } controllerList.add(new ControlElement(ControlElement.UPDATE, dar, controlStreamIdentifier, controlStreamIdentifierVoiceTokenLookup)); if (0 == --stepsThisStage) { stepsThisStage = JMSLRandom.choose(1, 100); buildInterpolators(i, dar, stepsThisStage); } } return dar; } private double[] makeElement(double pitch, double seconds, DimensionNameSpace dns) { double dar[] = new double[dns.dimension()]; dar[0] = seconds; dar[1] = pitch; dar[3] = seconds; for (int dimension = 0; dimension < dns.dimension(); dimension++) { if (notDurPitchHold(dimension)) { double minValue = instrument.getDimensionNameSpace().getLowLimit(dimension); double maxValue = instrument.getDimensionNameSpace().getHighLimit(dimension); double value = JMSLRandom.choose(minValue, maxValue); dar[dimension] = value; } } return dar; } /** * exclude dimensions for duration, pitch, and hold time which are not subject * to continuous control */ private boolean notDurPitchHold(int dimension) { return dimension != 0 && dimension != 1 && dimension != 3; } private void buildInterpolators(int x1, double[] dar, int stepsThisStage) { DimensionNameSpace dns = instrument.getDimensionNameSpace(); interpolators = new Interpolator[dns.dimension()]; for (int dimension = 0; dimension < dns.dimension(); dimension++) { if (notDurPitchHold(dimension)) { double minValue = instrument.getDimensionNameSpace().getLowLimit(dimension); double maxValue = instrument.getDimensionNameSpace().getHighLimit(dimension); double endingValue = JMSLRandom.choose(minValue, maxValue); double startingValue = dar[dimension]; Interpolator interpolator; if (JMSLRandom.choose() < 0.50) { interpolator = new HalfCosineInterpolator(x1, startingValue, stepsThisStage + x1, endingValue); } else { interpolator = new LinearInterpolator(x1, startingValue, stepsThisStage + x1, endingValue); } interpolators[dimension] = interpolator; } } } } /** * This element can be added to a MusicList to turn its instrument on, off, or * update it. Generally useful */ class ControlElement implements InstrumentPlayable { private double[] dar; private int action; public static final int ON = 0; public static final int UPDATE = 1; public static final int OFF = 2; private Integer controlStreamIndex; private Hashtable controlStreamIdentifierVoiceTokenLookup; public ControlElement(int action, double[] dar, int controlStreamIndex, Hashtable controlStreamIdentifierVoiceTokenLookup) { this.dar = dar; this.action = action; this.controlStreamIdentifierVoiceTokenLookup = controlStreamIdentifierVoiceTokenLookup; this.controlStreamIndex = Integer.valueOf(controlStreamIndex); } public double play(double playTime, Composable parent, Instrument ins) { double timeStretch = parent == null ? 1 : parent.getTimeStretch(); switch (action) { case ON: Object tokenObj = ins.on(playTime, timeStretch, dar); controlStreamIdentifierVoiceTokenLookup.put(controlStreamIndex, (Integer) tokenObj); System.out.println("Control stream index " + controlStreamIndex + " is mapped to voice token " + tokenObj); break; case OFF: if (controlStreamIdentifierVoiceTokenLookup.containsKey(controlStreamIndex)) { int voiceToken = controlStreamIdentifierVoiceTokenLookup.get(controlStreamIndex).intValue(); ((JSynUnitVoiceInstrument) ins).off(playTime, voiceToken); } else { System.err.println("Cannot locate voice token for off(). Control stream " + controlStreamIndex + " probably did not start yet."); } break; case UPDATE: int voiceTokenForUpdate = controlStreamIdentifierVoiceTokenLookup.get(controlStreamIndex).intValue(); ((JSynUnitVoiceInstrument) ins).update(playTime, timeStretch, dar, voiceTokenForUpdate); break; } return playTime + TimbralGestureMaker.CONTROL_RATE; } public double[] getData() { return dar; } }