package jmslexamples.jsyn2; /** * Control the JMSL clock rate for a ParallelCollection of MusicShapes by moving * a scrollbar. * * Each of four musicshapes plays elements that are twice as fast and an octave * higher than the previous (gamelan style). This lets use clearly hear that the * elements stay in sync when the clock rate changes. This demonstrates a * significant improvement in JMSL scheduling. Timestretch works great to change * playback speed instantaneously at boundaries (like measures in a score), but * didn't work for continuously changing tempo like accel and decel. Instead, we * now change the JMSL.clock rate, and this demo shows that all children of the * collection will stay in sync as the user moves the scrollbar to change * playback rate. * * 2023-01-17 * * @author Nick Didkovsky, 2023-01-17, copyright (c) 2023, Nick Didkovsky */ import java.awt.BorderLayout; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import javax.swing.JFrame; import com.didkovsky.portview.PVButton; import com.didkovsky.portview.PVLabel; import com.didkovsky.portview.PVPanel; import com.didkovsky.portview.PVScrollbar; import com.didkovsky.portview.PVScrollbarListener; import com.didkovsky.portview.swing.ViewFactorySwing; import com.softsynth.jmsl.Composable; import com.softsynth.jmsl.JMSL; import com.softsynth.jmsl.JMSLMixerContainer; import com.softsynth.jmsl.JMSLRandom; import com.softsynth.jmsl.MusicShape; 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.TuningET; import com.softsynth.jmsl.view.PVLabelAdapter; import com.softsynth.jmsl.view.PVPanelAdapter; public class NicksDreamClock extends JFrame implements ActionListener, PVScrollbarListener { private JMSLMixerContainer mixer; private PVButton startButton; private PVButton stopButton; private PVScrollbar speedyScrollbar; private PVLabel speedyLabel; private ParallelCollection parCol; private double currentClockRate = 1.0; private int numRepeats = 1000; private void buildLayout() { setLayout(new BorderLayout(20, 20)); PVPanel p = new PVPanelAdapter(); p.setLayout(new GridLayout(0, 1)); speedyLabel = new PVLabelAdapter(durString()); speedyScrollbar = new PVScrollbar((int) currentClockRate * 1000, 10, 6000); speedyScrollbar.addPVScrollbarListener(this); // set the size of the scrollbar speedyScrollbar.setSize(320, 25); // set the increment that the value will jump when user clicks inside // scrollbar speedyScrollbar.setPageIncrement(100); p.add(speedyLabel.getComponent()); p.add(speedyScrollbar.getComponent()); add(BorderLayout.CENTER, p.getComponent()); p = new PVPanelAdapter(); p.add((startButton = JMSL.getViewFactory().createButton("Start")).getComponent()); p.add((stopButton = JMSL.getViewFactory().createButton("Stop")).getComponent()); add(BorderLayout.SOUTH, p.getComponent()); add(BorderLayout.NORTH, mixer.getPanAmpControlPanel()); startButton.addActionListener(this); stopButton.addActionListener(this); } int NUM_MUSICSHAPES = 4; void buildHierarchy() { parCol = new ParallelCollection(); parCol.addStartPlayable(new Playable() { @Override public double play(double time, Composable parent) throws InterruptedException { System.out.println("Parallel Collection starts at time=" + time); return time; } }); parCol.addRepeatPlayable(new Playable() { @Override public double play(double time, Composable parent) throws InterruptedException { System.out.println("Parallel Collection repeats at time=" + time); return time; } }); for (int i = 0; i < NUM_MUSICSHAPES; i++) { JSynUnitVoiceInstrument ins = new JSynUnitVoiceInstrument(1, jmslexamples.jsyn2.unitvoices.SpeedyUnitVoice.class.getName()); MusicShape s = new MusicShape(ins.getDimensionNameSpace()); s.setInstrument(ins); s.addStartPlayable(new Playable() { @Override public double play(double time, Composable parent) throws InterruptedException { System.out.println("MusicShape " + parent.getName() + " starts at time=" + time); return time; } }); s.addStopPlayable(new Playable() { @Override public double play(double time, Composable parent) throws InterruptedException { System.out.println( "MusicShape stops at time=" + time + ", at " + System.currentTimeMillis() + " msec"); return time; } }); double duration = Math.pow(2, i) * 0.25; double totalDur = 4; int numElements = (int) (totalDur / duration); System.out.println("duration = " + duration + ", num elements=" + numElements); double referenceFrequency = 55; int octaveScaler = 4 - i; double rootFrequency = referenceFrequency * Math.pow(2, octaveScaler); System.out.println(i + ") rootFrequency=" + rootFrequency); double frequency = rootFrequency; TuningET tuning = (TuningET) ins.getTuning(); for (int j = 0; j < numElements; j++) { double denom = JMSLRandom.choose(5) + 1.0; double numer = denom + JMSLRandom.choose(5) + 1.0; double ratio = numer / denom; frequency *= ratio; while (frequency < rootFrequency) frequency *= 2; while (frequency > rootFrequency * 2) frequency /= 2; System.out.println("frequency=" + frequency + ", rootFrequency=" + rootFrequency); double[] data = s.getDefaultArray(); data[0] = duration; data[1] = tuning.getPitch(frequency); data[2] = 0.5; data[3] = duration * 0.9; s.add(data); } // s.print(); parCol.add(s); } JMSL.clock.setRate(currentClockRate); parCol.setRepeats(numRepeats); } String durString() { return "Clock Rate: " + currentClockRate; } private void buildMixer() { mixer = new JMSLMixerContainer(); mixer.start(); double[] pans = { 0, 0.33, 0.66, 1.0 }; double[] amps = { 0.1, 0.4, 0.5, 0.6 }; for (int i = 0; i < parCol.size(); i++) { double pan = pans[i]; double amp = amps[i]; mixer.addInstrument(((MusicShape) parCol.get(i)).getInstrument(), pan, amp); } } public void build() { JMSL.setViewFactory(new ViewFactorySwing()); JMSL.clock.setAdvance(0.1); JMSLRandom.randomize(); JSynMusicDevice.instance().open(); buildHierarchy(); buildMixer(); buildLayout(); System.out.println("JMSL clock is a " + JMSL.clock.getClass().getName()); saveMusicShapesToFile(); } private void saveMusicShapesToFile() { for (int i = 0; i < parCol.size(); i++) { MusicShape s = (MusicShape) parCol.get(i); PrintWriter out; try { out = new PrintWriter(new FileWriter(new File("/Users/nick/Desktop/musicshape_" + i + ".java"))); s.dumpSource(out); out.close(); } catch (IOException e) { e.printStackTrace(); } } } public void cleanup() { removeAll(); parCol.finishAll(); try { parCol.waitForDone(); } catch (InterruptedException e) { e.printStackTrace(); } JMSL.closeMusicDevices(); } /** * the dream is to move this scrollbar and have the entire parallel collection * smoothly follow the timestretch change, staying in synch. */ void handleSpeedyChange() { currentClockRate = speedyScrollbar.getValue() / 1000.0; speedyLabel.setText(durString()); JMSL.clock.setRate(currentClockRate); } void handleStart() { parCol.finishAll(); try { parCol.waitForDone(); } catch (InterruptedException e) { e.printStackTrace(); } parCol.launch(JMSL.now()); } void handleStop() { parCol.finishAll(); } @Override public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == startButton) handleStart(); if (source == stopButton) handleStop(); } public static void main(String args[]) { NicksDreamClock nicksDream = new NicksDreamClock(); nicksDream.build(); nicksDream.pack(); nicksDream.setVisible(true); nicksDream.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { nicksDream.cleanup(); System.exit(0); } }); } @Override public void notifyScrollbarValueChanged(PVScrollbar sb) { if (sb == speedyScrollbar) { handleSpeedyChange(); } } }