aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/com/jsyn/scope
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/jsyn/scope')
-rw-r--r--src/main/java/com/jsyn/scope/AudioScope.java101
-rw-r--r--src/main/java/com/jsyn/scope/AudioScopeModel.java157
-rw-r--r--src/main/java/com/jsyn/scope/AudioScopeProbe.java94
-rw-r--r--src/main/java/com/jsyn/scope/DefaultWaveTraceModel.java48
-rw-r--r--src/main/java/com/jsyn/scope/MultiChannelScopeProbeUnit.java246
-rw-r--r--src/main/java/com/jsyn/scope/TriggerModel.java67
-rw-r--r--src/main/java/com/jsyn/scope/WaveTraceModel.java27
-rw-r--r--src/main/java/com/jsyn/scope/swing/AudioScopeProbeView.java45
-rw-r--r--src/main/java/com/jsyn/scope/swing/AudioScopeView.java112
-rw-r--r--src/main/java/com/jsyn/scope/swing/MultipleWaveDisplay.java58
-rw-r--r--src/main/java/com/jsyn/scope/swing/ScopeControlPanel.java46
-rw-r--r--src/main/java/com/jsyn/scope/swing/ScopeProbePanel.java87
-rw-r--r--src/main/java/com/jsyn/scope/swing/ScopeTriggerPanel.java47
-rw-r--r--src/main/java/com/jsyn/scope/swing/WaveTraceView.java122
14 files changed, 1257 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/scope/AudioScope.java b/src/main/java/com/jsyn/scope/AudioScope.java
new file mode 100644
index 0000000..7b2a98c
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/AudioScope.java
@@ -0,0 +1,101 @@
+/*
+ * 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.scope;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.scope.swing.AudioScopeView;
+
+// TODO Auto and Manual triggers.
+// TODO Auto scaling of vertical.
+// TODO Fixed size Y scale knobs.
+// TODO Pan back and forth around trigger.
+// TODO Continuous capture
+/**
+ * Digital oscilloscope for JSyn.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class AudioScope {
+ public enum TriggerMode {
+ AUTO, NORMAL // , MANUAL
+ }
+
+ public enum ViewMode {
+ WAVEFORM, SPECTRUM
+ }
+
+ private AudioScopeView audioScopeView = null;
+ private AudioScopeModel audioScopeModel;
+
+ public AudioScope(Synthesizer synth) {
+ audioScopeModel = new AudioScopeModel(synth);
+ }
+
+ public AudioScopeProbe addProbe(UnitOutputPort output) {
+ return addProbe(output, 0);
+ }
+
+ public AudioScopeProbe addProbe(UnitOutputPort output, int partIndex) {
+ return audioScopeModel.addProbe(output, partIndex);
+ }
+
+ public void start() {
+ audioScopeModel.start();
+ }
+
+ public void stop() {
+ audioScopeModel.stop();
+ }
+
+ public AudioScopeModel getModel() {
+ return audioScopeModel;
+ }
+
+ public AudioScopeView getView() {
+ if (audioScopeView == null) {
+ audioScopeView = new AudioScopeView();
+ audioScopeView.setModel(audioScopeModel);
+ }
+ return audioScopeView;
+ }
+
+ public void setTriggerMode(TriggerMode triggerMode) {
+ audioScopeModel.setTriggerMode(triggerMode);
+ }
+
+ public void setTriggerSource(AudioScopeProbe probe) {
+ audioScopeModel.setTriggerSource(probe);
+ }
+
+ public void setTriggerLevel(double level) {
+ getModel().getTriggerModel().getLevelModel().setDoubleValue(level);
+ }
+
+ public double getTriggerLevel() {
+ return getModel().getTriggerModel().getLevelModel().getDoubleValue();
+ }
+
+ /**
+ * Not yet implemented.
+ * @param viewMode
+ */
+ public void setViewMode(ViewMode viewMode) {
+ // TODO Auto-generated method stub
+ }
+
+}
diff --git a/src/main/java/com/jsyn/scope/AudioScopeModel.java b/src/main/java/com/jsyn/scope/AudioScopeModel.java
new file mode 100644
index 0000000..85c4413
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/AudioScopeModel.java
@@ -0,0 +1,157 @@
+/*
+ * 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.scope;
+
+import java.util.ArrayList;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.scope.AudioScope.TriggerMode;
+
+public class AudioScopeModel implements Runnable {
+ private static final int PRE_TRIGGER_SIZE = 32;
+ private Synthesizer synthesisEngine;
+ private ArrayList<AudioScopeProbe> probes = new ArrayList<AudioScopeProbe>();
+ private CopyOnWriteArrayList<ChangeListener> changeListeners = new CopyOnWriteArrayList<ChangeListener>();
+ private MultiChannelScopeProbeUnit probeUnit;
+ private double timeToArm;
+ private double period = 0.2;
+ private TriggerModel triggerModel;
+
+ public AudioScopeModel(Synthesizer synth) {
+ this.synthesisEngine = synth;
+ triggerModel = new TriggerModel();
+ }
+
+ public AudioScopeProbe addProbe(UnitOutputPort output, int partIndex) {
+ AudioScopeProbe probe = new AudioScopeProbe(this, output, partIndex);
+ DefaultWaveTraceModel waveTraceModel = new DefaultWaveTraceModel(this, probes.size());
+ probe.setWaveTraceModel(waveTraceModel);
+ probes.add(probe);
+ if (triggerModel.getSource() == null) {
+ triggerModel.setSource(probe);
+ }
+ return probe;
+ }
+
+ public void start() {
+ stop();
+ probeUnit = new MultiChannelScopeProbeUnit(probes.size(), triggerModel);
+ synthesisEngine.add(probeUnit);
+ for (int i = 0; i < probes.size(); i++) {
+ AudioScopeProbe probe = probes.get(i);
+ probe.getSource().connect(probe.getPartIndex(), probeUnit.input, i);
+ }
+ // Connect trigger signal to input of probe.
+ triggerModel.getSource().getSource()
+ .connect(triggerModel.getSource().getPartIndex(), probeUnit.trigger, 0);
+ probeUnit.start();
+
+ // Get synthesizer time in seconds.
+ timeToArm = synthesisEngine.getCurrentTime();
+ probeUnit.arm(timeToArm, this);
+ }
+
+ public void stop() {
+ if (probeUnit != null) {
+ for (int i = 0; i < probes.size(); i++) {
+ probeUnit.input.disconnectAll(i);
+ }
+ probeUnit.trigger.disconnectAll();
+ probeUnit.stop();
+ synthesisEngine.remove(probeUnit);
+ probeUnit = null;
+ }
+ }
+
+ public AudioScopeProbe[] getProbes() {
+ return probes.toArray(new AudioScopeProbe[0]);
+ }
+
+ public Synthesizer getSynthesizer() {
+ return synthesisEngine;
+ }
+
+ @Override
+ public void run() {
+ fireChangeListeners();
+ timeToArm = synthesisEngine.getCurrentTime();
+ timeToArm += period;
+ probeUnit.arm(timeToArm, this);
+ }
+
+ private void fireChangeListeners() {
+ ChangeEvent changeEvent = new ChangeEvent(this);
+ for (ChangeListener listener : changeListeners) {
+ listener.stateChanged(changeEvent);
+ }
+ // debug();
+ }
+
+ public void addChangeListener(ChangeListener changeListener) {
+ changeListeners.add(changeListener);
+ }
+
+ public void removeChangeListener(ChangeListener changeListener) {
+ changeListeners.remove(changeListener);
+ }
+
+ public void setTriggerMode(TriggerMode triggerMode) {
+ triggerModel.getModeModel().setSelectedItem(triggerMode);
+ }
+
+ public void setTriggerSource(AudioScopeProbe probe) {
+ triggerModel.setSource(probe);
+ }
+
+ public double getSample(int bufferIndex, int i) {
+ return probeUnit.getSample(bufferIndex, i);
+ }
+
+ public int getFramesPerBuffer() {
+ return probeUnit.getFramesPerBuffer();
+ }
+
+ public int getFramesCaptured() {
+ return probeUnit.getFramesCaptured();
+ }
+
+ public int getVisibleSize() {
+ int size = 0;
+ if (probeUnit != null) {
+ size = probeUnit.getPostTriggerSize() + PRE_TRIGGER_SIZE;
+ if (size > getFramesCaptured()) {
+ size = getFramesCaptured();
+ }
+ }
+ return size;
+ }
+
+ public int getStartIndex() {
+ // TODO Add pan support here.
+ return getFramesCaptured() - getVisibleSize();
+ }
+
+ public TriggerModel getTriggerModel() {
+ return triggerModel;
+ }
+
+}
diff --git a/src/main/java/com/jsyn/scope/AudioScopeProbe.java b/src/main/java/com/jsyn/scope/AudioScopeProbe.java
new file mode 100644
index 0000000..f1aad65
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/AudioScopeProbe.java
@@ -0,0 +1,94 @@
+/*
+ * 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.scope;
+
+import java.awt.Color;
+
+import javax.swing.JToggleButton.ToggleButtonModel;
+
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.swing.ExponentialRangeModel;
+
+/**
+ * Collect data from the source and make it available to the scope.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class AudioScopeProbe {
+ // private UnitOutputPort output;
+ private WaveTraceModel waveTraceModel;
+ private AudioScopeModel audioScopeModel;
+ private UnitOutputPort source;
+ private int partIndex;
+ private Color color;
+ private ExponentialRangeModel verticalScaleModel;
+ private ToggleButtonModel autoScaleButtonModel;
+ private double MIN_RANGE = 0.01;
+ private double MAX_RANGE = 100.0;
+
+ public AudioScopeProbe(AudioScopeModel audioScopeModel, UnitOutputPort source, int partIndex) {
+ this.audioScopeModel = audioScopeModel;
+ this.source = source;
+ this.partIndex = partIndex;
+
+ verticalScaleModel = new ExponentialRangeModel("VScale", 1000, MIN_RANGE, MAX_RANGE,
+ MIN_RANGE);
+ autoScaleButtonModel = new ToggleButtonModel();
+ autoScaleButtonModel.setSelected(true);
+ }
+
+ public WaveTraceModel getWaveTraceModel() {
+ return waveTraceModel;
+ }
+
+ public void setWaveTraceModel(WaveTraceModel waveTraceModel) {
+ this.waveTraceModel = waveTraceModel;
+ }
+
+ public UnitOutputPort getSource() {
+ return source;
+ }
+
+ public int getPartIndex() {
+ return partIndex;
+ }
+
+ public Color getColor() {
+ return color;
+ }
+
+ public void setColor(Color color) {
+ this.color = color;
+ }
+
+ public void setAutoScaleEnabled(boolean enabled) {
+ autoScaleButtonModel.setSelected(enabled);
+ }
+
+ public void setVerticalScale(double max) {
+ verticalScaleModel.setDoubleValue(max);
+ }
+
+ public ExponentialRangeModel getVerticalScaleModel() {
+ return verticalScaleModel;
+ }
+
+ public ToggleButtonModel getAutoScaleButtonModel() {
+ return autoScaleButtonModel;
+ }
+
+}
diff --git a/src/main/java/com/jsyn/scope/DefaultWaveTraceModel.java b/src/main/java/com/jsyn/scope/DefaultWaveTraceModel.java
new file mode 100644
index 0000000..a123c0b
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/DefaultWaveTraceModel.java
@@ -0,0 +1,48 @@
+/*
+ * 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.scope;
+
+public class DefaultWaveTraceModel implements WaveTraceModel {
+ private AudioScopeModel audioScopeModel;
+ private int bufferIndex;
+
+ public DefaultWaveTraceModel(AudioScopeModel audioScopeModel, int bufferIndex) {
+ this.audioScopeModel = audioScopeModel;
+ this.bufferIndex = bufferIndex;
+ }
+
+ @Override
+ public double getSample(int i) {
+ return audioScopeModel.getSample(bufferIndex, i);
+ }
+
+ @Override
+ public int getSize() {
+ return audioScopeModel.getFramesCaptured();
+ }
+
+ @Override
+ public int getStartIndex() {
+ return audioScopeModel.getStartIndex();
+ }
+
+ @Override
+ public int getVisibleSize() {
+ return audioScopeModel.getVisibleSize();
+ }
+
+}
diff --git a/src/main/java/com/jsyn/scope/MultiChannelScopeProbeUnit.java b/src/main/java/com/jsyn/scope/MultiChannelScopeProbeUnit.java
new file mode 100644
index 0000000..59bb635
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/MultiChannelScopeProbeUnit.java
@@ -0,0 +1,246 @@
+/*
+ * 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.scope;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.scope.AudioScope.TriggerMode;
+import com.jsyn.unitgen.UnitGenerator;
+import com.softsynth.shared.time.ScheduledCommand;
+
+/**
+ * Multi-channel scope probe with an independent trigger input.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class MultiChannelScopeProbeUnit extends UnitGenerator {
+ // Signal that is captured.
+ public UnitInputPort input;
+ // Signal that triggers the probe.
+ public UnitInputPort trigger;
+
+ // I am using ints instead of an enum for performance reasons.
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_ARMED = 1;
+ private static final int STATE_READY = 2;
+ private static final int STATE_TRIGGERED = 3;
+ private int state = STATE_IDLE;
+
+ private int numChannels;
+ private double[][] inputValues;
+ private static final int FRAMES_PER_BUFFER = 4096; // must be power of two
+ private static final int FRAMES_PER_BUFFER_MASK = FRAMES_PER_BUFFER - 1;
+ private Runnable callback;
+
+ private TriggerModel triggerModel;
+ private int autoCountdown;
+ private int countdown;
+ private int postTriggerSize = 512;
+ SignalBuffer captureBuffer;
+ SignalBuffer displayBuffer;
+
+ // Use double buffers. One for capture, one for display.
+ static class SignalBuffer {
+ float[][] buffers;
+ private int writeCursor;
+ private int triggerIndex;
+ private int framesCaptured;
+
+ SignalBuffer(int numChannels) {
+ buffers = new float[numChannels][];
+ for (int j = 0; j < numChannels; j++) {
+ buffers[j] = new float[FRAMES_PER_BUFFER];
+ }
+ }
+
+ void reset() {
+ writeCursor = 0;
+ triggerIndex = 0;
+ framesCaptured = 0;
+ }
+
+ public void saveChannelValue(int j, float value) {
+ buffers[j][writeCursor] = value;
+ }
+
+ public void markTrigger() {
+ triggerIndex = writeCursor;
+ }
+
+ public void bumpCursor() {
+ writeCursor = (writeCursor + 1) & FRAMES_PER_BUFFER_MASK;
+ if (writeCursor >= FRAMES_PER_BUFFER) {
+ writeCursor = 0;
+ }
+ if (framesCaptured < FRAMES_PER_BUFFER) {
+ framesCaptured += 1;
+ }
+ }
+
+ private int convertInternalToExternalIndex(int internalIndex) {
+ if (framesCaptured < FRAMES_PER_BUFFER) {
+ return internalIndex;
+ } else {
+ return (internalIndex - writeCursor) & (FRAMES_PER_BUFFER_MASK);
+ }
+ }
+
+ private int convertExternalToInternalIndex(int externalIndex) {
+ if (framesCaptured < FRAMES_PER_BUFFER) {
+ return externalIndex;
+ } else {
+ return (externalIndex + writeCursor) & (FRAMES_PER_BUFFER_MASK);
+ }
+ }
+
+ public int getTriggerIndex() {
+ return convertInternalToExternalIndex(triggerIndex);
+ }
+
+ public int getFramesCaptured() {
+ return framesCaptured;
+ }
+
+ public float getSample(int bufferIndex, int sampleIndex) {
+ int index = convertExternalToInternalIndex(sampleIndex);
+ return buffers[bufferIndex][index];
+ }
+ }
+
+ public MultiChannelScopeProbeUnit(int numChannels, TriggerModel triggerModel) {
+ this.numChannels = numChannels;
+ captureBuffer = new SignalBuffer(numChannels);
+ displayBuffer = new SignalBuffer(numChannels);
+ this.triggerModel = triggerModel;
+ addPort(trigger = new UnitInputPort(numChannels, "Trigger"));
+ addPort(input = new UnitInputPort(numChannels, "Input"));
+ inputValues = new double[numChannels][];
+ }
+
+ private synchronized void switchBuffers() {
+ SignalBuffer temp = captureBuffer;
+ captureBuffer = displayBuffer;
+ displayBuffer = temp;
+ }
+
+ private void internalArm(Runnable callback) {
+ this.callback = callback;
+ state = STATE_ARMED;
+ captureBuffer.reset();
+ }
+
+ class ScheduledArm implements ScheduledCommand {
+ private Runnable callback;
+
+ ScheduledArm(Runnable callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void run() {
+ internalArm(this.callback);
+ }
+ }
+
+ /** Arm the probe at a future time. */
+ public void arm(double time, Runnable callback) {
+ ScheduledArm command = new ScheduledArm(callback);
+ getSynthesisEngine().scheduleCommand(time, command);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ if (state != STATE_IDLE) {
+ TriggerMode triggerMode = triggerModel.getMode();
+ double triggerLevel = triggerModel.getTriggerLevel();
+ double[] triggerValues = trigger.getValues();
+
+ for (int j = 0; j < numChannels; j++) {
+ inputValues[j] = input.getValues(j);
+ }
+
+ for (int i = start; i < limit; i++) {
+ // Capture one sample from each channel.
+ for (int j = 0; j < numChannels; j++) {
+ captureBuffer.saveChannelValue(j, (float) inputValues[j][i]);
+ }
+ captureBuffer.bumpCursor();
+
+ switch (state) {
+ case STATE_ARMED:
+ if (triggerValues[i] <= triggerLevel) {
+ state = STATE_READY;
+ autoCountdown = 44100;
+ }
+ break;
+
+ case STATE_READY: {
+ boolean triggered = false;
+ if (triggerValues[i] > triggerLevel) {
+ triggered = true;
+ } else if (triggerMode.equals(TriggerMode.AUTO)) {
+ if (--autoCountdown == 0) {
+ triggered = true;
+ }
+ }
+ if (triggered) {
+ captureBuffer.markTrigger();
+ state = STATE_TRIGGERED;
+ countdown = postTriggerSize;
+ }
+ }
+ break;
+
+ case STATE_TRIGGERED:
+ countdown -= 1;
+ if (countdown <= 0) {
+ state = STATE_IDLE;
+ switchBuffers();
+ fireCallback();
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ private void fireCallback() {
+ if (callback != null) {
+ callback.run();
+ }
+ }
+
+ public float getSample(int bufferIndex, int sampleIndex) {
+ return displayBuffer.getSample(bufferIndex, sampleIndex);
+ }
+
+ public int getTriggerIndex() {
+ return displayBuffer.getTriggerIndex();
+ }
+
+ public int getFramesCaptured() {
+ return displayBuffer.getFramesCaptured();
+ }
+
+ public int getFramesPerBuffer() {
+ return FRAMES_PER_BUFFER;
+ }
+
+ public int getPostTriggerSize() {
+ return postTriggerSize;
+ }
+
+}
diff --git a/src/main/java/com/jsyn/scope/TriggerModel.java b/src/main/java/com/jsyn/scope/TriggerModel.java
new file mode 100644
index 0000000..0367d71
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/TriggerModel.java
@@ -0,0 +1,67 @@
+/*
+ * 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.scope;
+
+import javax.swing.DefaultComboBoxModel;
+
+import com.jsyn.scope.AudioScope.TriggerMode;
+import com.jsyn.swing.ExponentialRangeModel;
+
+public class TriggerModel {
+ private ExponentialRangeModel levelModel;
+ private DefaultComboBoxModel<AudioScope.TriggerMode> modeModel;
+ private AudioScopeProbe source;
+
+ public TriggerModel() {
+ modeModel = new DefaultComboBoxModel<AudioScope.TriggerMode>();
+ modeModel.addElement(TriggerMode.AUTO);
+ modeModel.addElement(TriggerMode.NORMAL);
+ levelModel = new ExponentialRangeModel("TriggerLevel", 1000, 0.01, 2.0, 0.04);
+ }
+
+ public AudioScopeProbe getSource() {
+ return source;
+ }
+
+ public void setSource(AudioScopeProbe source) {
+ this.source = source;
+ }
+
+ public ExponentialRangeModel getLevelModel() {
+ return levelModel;
+ }
+
+ public void setLevelModel(ExponentialRangeModel levelModel) {
+ this.levelModel = levelModel;
+ }
+
+ public DefaultComboBoxModel<TriggerMode> getModeModel() {
+ return modeModel;
+ }
+
+ public void setModeModel(DefaultComboBoxModel<TriggerMode> modeModel) {
+ this.modeModel = modeModel;
+ }
+
+ public double getTriggerLevel() {
+ return levelModel.getDoubleValue();
+ }
+
+ public TriggerMode getMode() {
+ return (TriggerMode) modeModel.getSelectedItem();
+ }
+}
diff --git a/src/main/java/com/jsyn/scope/WaveTraceModel.java b/src/main/java/com/jsyn/scope/WaveTraceModel.java
new file mode 100644
index 0000000..e9d8bf9
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/WaveTraceModel.java
@@ -0,0 +1,27 @@
+/*
+ * 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.scope;
+
+public interface WaveTraceModel {
+ int getSize();
+
+ int getVisibleSize();
+
+ int getStartIndex();
+
+ double getSample(int i);
+}
diff --git a/src/main/java/com/jsyn/scope/swing/AudioScopeProbeView.java b/src/main/java/com/jsyn/scope/swing/AudioScopeProbeView.java
new file mode 100644
index 0000000..59526e1
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/swing/AudioScopeProbeView.java
@@ -0,0 +1,45 @@
+/*
+ * 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.scope.swing;
+
+import com.jsyn.scope.AudioScopeProbe;
+
+/**
+ * Wave display associated with a probe.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class AudioScopeProbeView {
+ private AudioScopeProbe probeModel;
+ private WaveTraceView waveTrace;
+
+ public AudioScopeProbeView(AudioScopeProbe probeModel) {
+ this.probeModel = probeModel;
+ waveTrace = new WaveTraceView(probeModel.getAutoScaleButtonModel(),
+ probeModel.getVerticalScaleModel());
+ waveTrace.setModel(probeModel.getWaveTraceModel());
+ }
+
+ public WaveTraceView getWaveTraceView() {
+ return waveTrace;
+ }
+
+ public AudioScopeProbe getModel() {
+ return probeModel;
+ }
+
+}
diff --git a/src/main/java/com/jsyn/scope/swing/AudioScopeView.java b/src/main/java/com/jsyn/scope/swing/AudioScopeView.java
new file mode 100644
index 0000000..ec1afa3
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/swing/AudioScopeView.java
@@ -0,0 +1,112 @@
+/*
+ * 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.scope.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.util.ArrayList;
+
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.scope.AudioScopeModel;
+import com.jsyn.scope.AudioScopeProbe;
+
+public class AudioScopeView extends JPanel {
+ private static final long serialVersionUID = -7507986850757860853L;
+ private AudioScopeModel audioScopeModel;
+ private ArrayList<AudioScopeProbeView> probeViews = new ArrayList<AudioScopeProbeView>();
+ private MultipleWaveDisplay multipleWaveDisplay;
+ private boolean showControls = false;
+ private ScopeControlPanel controlPanel = null;
+
+ public AudioScopeView() {
+ setBackground(Color.GREEN);
+ }
+
+ public void setModel(AudioScopeModel audioScopeModel) {
+ this.audioScopeModel = audioScopeModel;
+ // Create a view for each probe.
+ probeViews.clear();
+ for (AudioScopeProbe probeModel : audioScopeModel.getProbes()) {
+ AudioScopeProbeView audioScopeProbeView = new AudioScopeProbeView(probeModel);
+ probeViews.add(audioScopeProbeView);
+ }
+ setupGUI();
+
+ // Listener for signal change events.
+ audioScopeModel.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ multipleWaveDisplay.repaint();
+ }
+ });
+
+ }
+
+ private void setupGUI() {
+ removeAll();
+ setLayout(new BorderLayout());
+ multipleWaveDisplay = new MultipleWaveDisplay();
+
+ for (AudioScopeProbeView probeView : probeViews) {
+ multipleWaveDisplay.addWaveTrace(probeView.getWaveTraceView());
+ probeView.getModel().setColor(probeView.getWaveTraceView().getColor());
+ }
+
+ add(multipleWaveDisplay, BorderLayout.CENTER);
+
+ setMinimumSize(new Dimension(400, 200));
+ setPreferredSize(new Dimension(600, 250));
+ setMaximumSize(new Dimension(1200, 300));
+ }
+
+ /** @deprecated Use setControlsVisible() instead. */
+ @Deprecated
+ public void setShowControls(boolean show) {
+ setControlsVisible(show);
+ }
+
+ public void setControlsVisible(boolean show) {
+ if (this.showControls) {
+ if (!show && (controlPanel != null)) {
+ remove(controlPanel);
+ }
+ } else {
+ if (show) {
+ if (controlPanel == null) {
+ controlPanel = new ScopeControlPanel(this);
+ }
+ add(controlPanel, BorderLayout.EAST);
+ validate();
+ }
+ }
+
+ this.showControls = show;
+ }
+
+ public AudioScopeModel getModel() {
+ return audioScopeModel;
+ }
+
+ public AudioScopeProbeView[] getProbeViews() {
+ return probeViews.toArray(new AudioScopeProbeView[0]);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/scope/swing/MultipleWaveDisplay.java b/src/main/java/com/jsyn/scope/swing/MultipleWaveDisplay.java
new file mode 100644
index 0000000..0259850
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/swing/MultipleWaveDisplay.java
@@ -0,0 +1,58 @@
+/*
+ * 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.scope.swing;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.util.ArrayList;
+
+import javax.swing.JPanel;
+
+/**
+ * Display multiple waveforms together in different colors.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class MultipleWaveDisplay extends JPanel {
+ private static final long serialVersionUID = -5157397030540800373L;
+
+ private ArrayList<WaveTraceView> waveTraceViews = new ArrayList<WaveTraceView>();
+ private Color[] defaultColors = {
+ Color.BLUE, Color.RED, Color.BLACK, Color.MAGENTA, Color.GREEN, Color.ORANGE
+ };
+
+ public MultipleWaveDisplay() {
+ setBackground(Color.WHITE);
+ }
+
+ public void addWaveTrace(WaveTraceView waveTraceView) {
+ if (waveTraceView.getColor() == null) {
+ waveTraceView.setColor(defaultColors[waveTraceViews.size() % defaultColors.length]);
+ }
+ waveTraceViews.add(waveTraceView);
+ }
+
+ @Override
+ public void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ int width = getWidth();
+ int height = getHeight();
+ for (WaveTraceView waveTraceView : waveTraceViews.toArray(new WaveTraceView[0])) {
+ waveTraceView.drawWave(g, width, height);
+ }
+ }
+}
diff --git a/src/main/java/com/jsyn/scope/swing/ScopeControlPanel.java b/src/main/java/com/jsyn/scope/swing/ScopeControlPanel.java
new file mode 100644
index 0000000..7f3a026
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/swing/ScopeControlPanel.java
@@ -0,0 +1,46 @@
+/*
+ * 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.scope.swing;
+
+import java.awt.GridLayout;
+
+import javax.swing.JPanel;
+
+import com.jsyn.scope.AudioScopeModel;
+
+public class ScopeControlPanel extends JPanel {
+ private static final long serialVersionUID = 7738305116057614812L;
+ private AudioScopeModel audioScopeModel;
+ private ScopeTriggerPanel triggerPanel;
+ private JPanel probeRows;
+
+ public ScopeControlPanel(AudioScopeView audioScopeView) {
+ setLayout(new GridLayout(0, 1));
+ this.audioScopeModel = audioScopeView.getModel();
+ triggerPanel = new ScopeTriggerPanel(audioScopeModel);
+ add(triggerPanel);
+
+ probeRows = new JPanel();
+ probeRows.setLayout(new GridLayout(1, 0));
+ add(probeRows);
+ for (AudioScopeProbeView probeView : audioScopeView.getProbeViews()) {
+ ScopeProbePanel probePanel = new ScopeProbePanel(probeView);
+ probeRows.add(probePanel);
+ }
+ }
+
+}
diff --git a/src/main/java/com/jsyn/scope/swing/ScopeProbePanel.java b/src/main/java/com/jsyn/scope/swing/ScopeProbePanel.java
new file mode 100644
index 0000000..a0dec91
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/swing/ScopeProbePanel.java
@@ -0,0 +1,87 @@
+/*
+ * 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.scope.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.JCheckBox;
+import javax.swing.JPanel;
+import javax.swing.JToggleButton.ToggleButtonModel;
+
+import com.jsyn.scope.AudioScopeProbe;
+import com.jsyn.swing.RotaryTextController;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ScopeProbePanel extends JPanel {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ScopeProbePanel.class);
+ private static final long serialVersionUID = 4511589171299298548L;
+
+ private AudioScopeProbeView audioScopeProbeView;
+ private AudioScopeProbe audioScopeProbe;
+ private RotaryTextController verticalScaleKnob;
+ private JCheckBox autoBox;
+ private ToggleButtonModel autoScaleModel;
+
+ public ScopeProbePanel(AudioScopeProbeView probeView) {
+ this.audioScopeProbeView = probeView;
+ setLayout(new BorderLayout());
+
+ setBorder(BorderFactory.createLineBorder(Color.GRAY, 3));
+
+ // Add a colored box to match the waveform color.
+ JPanel colorPanel = new JPanel();
+ colorPanel.setMinimumSize(new Dimension(40, 40));
+ audioScopeProbe = probeView.getModel();
+ colorPanel.setBackground(audioScopeProbe.getColor());
+ add(colorPanel, BorderLayout.NORTH);
+
+ // Knob for tweaking vertical range.
+ verticalScaleKnob = new RotaryTextController(audioScopeProbeView.getWaveTraceView()
+ .getVerticalRangeModel(), 5);
+ add(verticalScaleKnob, BorderLayout.CENTER);
+ verticalScaleKnob.setTitle("YScale");
+
+ // Auto ranging checkbox.
+ autoBox = new JCheckBox("Auto");
+ autoScaleModel = audioScopeProbeView.getWaveTraceView().getAutoButtonModel();
+ autoScaleModel.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ ToggleButtonModel model = (ToggleButtonModel) e.getSource();
+ boolean enabled = !model.isSelected();
+ LOGGER.debug("Knob enabled = " + enabled);
+ verticalScaleKnob.setEnabled(!model.isSelected());
+ }
+ });
+ autoBox.setModel(autoScaleModel);
+ add(autoBox, BorderLayout.SOUTH);
+
+ verticalScaleKnob.setEnabled(!autoScaleModel.isSelected());
+
+ setMinimumSize(new Dimension(80, 100));
+ setPreferredSize(new Dimension(80, 150));
+ setMaximumSize(new Dimension(120, 200));
+ }
+
+}
diff --git a/src/main/java/com/jsyn/scope/swing/ScopeTriggerPanel.java b/src/main/java/com/jsyn/scope/swing/ScopeTriggerPanel.java
new file mode 100644
index 0000000..9c22aa1
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/swing/ScopeTriggerPanel.java
@@ -0,0 +1,47 @@
+/*
+ * 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.scope.swing;
+
+import java.awt.BorderLayout;
+
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
+import javax.swing.JPanel;
+
+import com.jsyn.scope.AudioScopeModel;
+import com.jsyn.scope.TriggerModel;
+import com.jsyn.scope.AudioScope.TriggerMode;
+import com.jsyn.swing.RotaryTextController;
+
+public class ScopeTriggerPanel extends JPanel {
+ private static final long serialVersionUID = 4511589171299298548L;
+ private JComboBox<DefaultComboBoxModel<TriggerMode>> triggerModeComboBox;
+ private RotaryTextController triggerLevelKnob;
+
+ public ScopeTriggerPanel(AudioScopeModel audioScopeModel) {
+ setLayout(new BorderLayout());
+ TriggerModel triggerModel = audioScopeModel.getTriggerModel();
+ triggerModeComboBox = new JComboBox(triggerModel.getModeModel());
+ add(triggerModeComboBox, BorderLayout.NORTH);
+
+ triggerLevelKnob = new RotaryTextController(triggerModel.getLevelModel(), 5);
+
+ add(triggerLevelKnob, BorderLayout.CENTER);
+ triggerLevelKnob.setTitle("Trigger Level");
+ }
+
+}
diff --git a/src/main/java/com/jsyn/scope/swing/WaveTraceView.java b/src/main/java/com/jsyn/scope/swing/WaveTraceView.java
new file mode 100644
index 0000000..849a6f4
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/swing/WaveTraceView.java
@@ -0,0 +1,122 @@
+/*
+ * 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.scope.swing;
+
+import java.awt.Color;
+import java.awt.Graphics;
+
+import javax.swing.JToggleButton.ToggleButtonModel;
+
+import com.jsyn.scope.WaveTraceModel;
+import com.jsyn.swing.ExponentialRangeModel;
+
+public class WaveTraceView {
+ private static final double AUTO_DECAY = 0.95;
+ private WaveTraceModel waveTraceModel;
+ private Color color;
+ private ExponentialRangeModel verticalScaleModel;
+ private ToggleButtonModel autoScaleButtonModel;
+
+ private double xScaler;
+ private double yScalar;
+ private int centerY;
+
+ public WaveTraceView(ToggleButtonModel autoButtonModel, ExponentialRangeModel verticalRangeModel) {
+ this.verticalScaleModel = verticalRangeModel;
+ this.autoScaleButtonModel = autoButtonModel;
+ }
+
+ public Color getColor() {
+ return color;
+ }
+
+ public void setColor(Color color) {
+ this.color = color;
+ }
+
+ public ExponentialRangeModel getVerticalRangeModel() {
+ return verticalScaleModel;
+ }
+
+ public ToggleButtonModel getAutoButtonModel() {
+ return autoScaleButtonModel;
+ }
+
+ public void setModel(WaveTraceModel waveTraceModel) {
+ this.waveTraceModel = waveTraceModel;
+ }
+
+ public int convertRealToY(double r) {
+ return centerY - (int) (yScalar * r);
+ }
+
+ public void drawWave(Graphics g, int width, int height) {
+ double sampleMax = 0.0;
+ double sampleMin = 0.0;
+ g.setColor(color);
+ int numSamples = waveTraceModel.getVisibleSize();
+ if (numSamples > 0) {
+ xScaler = (double) width / numSamples;
+ // Scale by 0.5 because it is bipolar.
+ yScalar = 0.5 * height / verticalScaleModel.getDoubleValue();
+ centerY = height / 2;
+
+ // Calculate position of first point.
+ int x1 = 0;
+ int offset = waveTraceModel.getStartIndex();
+ double value = waveTraceModel.getSample(offset);
+ int y1 = convertRealToY(value);
+
+ // Draw lines to remaining points.
+ for (int i = 1; i < numSamples; i++) {
+ int x2 = (int) (i * xScaler);
+ value = waveTraceModel.getSample(offset + i);
+ int y2 = convertRealToY(value);
+ g.drawLine(x1, y1, x2, y2);
+ x1 = x2;
+ y1 = y2;
+ // measure min and max for auto
+ if (value > sampleMax) {
+ sampleMax = value;
+ } else if (value < sampleMin) {
+ sampleMin = value;
+ }
+ }
+
+ autoScaleRange(sampleMax);
+ }
+ }
+
+ // Autoscale the vertical range.
+ private void autoScaleRange(double sampleMax) {
+ if (autoScaleButtonModel.isSelected()) {
+ double scaledMax = sampleMax * 1.1;
+ double current = verticalScaleModel.getDoubleValue();
+ if (scaledMax > current) {
+ verticalScaleModel.setDoubleValue(scaledMax);
+ } else {
+ double decayed = current * AUTO_DECAY;
+ if (decayed > verticalScaleModel.getMinimum()) {
+ if (scaledMax < decayed) {
+ verticalScaleModel.setDoubleValue(decayed);
+ }
+ }
+ }
+ }
+ }
+
+}