diff options
Diffstat (limited to 'src/main/java/com/jsyn/scope')
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); + } + } + } + } + } + +} |