/*******************************************************************************
* Copyright (c) 2016 Weasis Team and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Nicolas Roduit - initial API and implementation
*******************************************************************************/
package org.weasis.dicom.au;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioFormat.Encoding;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.Timer;
import javax.swing.border.TitledBorder;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.BulkData;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.VR;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.weasis.core.api.gui.util.FileFormatFilter;
import org.weasis.core.api.media.data.MediaSeries;
import org.weasis.core.api.media.data.Series;
import org.weasis.core.api.media.data.TagW;
import org.weasis.core.ui.docking.UIManager;
import org.weasis.core.ui.editor.SeriesViewerEvent;
import org.weasis.core.ui.editor.SeriesViewerEvent.EVENT;
import org.weasis.core.ui.editor.SeriesViewerListener;
import org.weasis.core.ui.editor.image.ViewerPlugin;
import org.weasis.dicom.codec.DicomMediaIO;
import org.weasis.dicom.codec.DicomSpecialElement;
@SuppressWarnings("serial")
public class AuView extends JPanel implements SeriesViewerListener {
private static final Logger LOGGER = LoggerFactory.getLogger(AuView.class);
private Series<?> series;
private Clip clip;
private boolean playing = false; // whether the sound is currently playing
private int audioLength; // Length of the sound.
private int audioPosition = 0; // Current position within the sound
private JButton play; // The Play/Stop button
private JSlider progress; // Shows and sets current position in sound
private JLabel time; // Displays audioPosition as a number
private Timer timer; // Updates slider every 100 milliseconds
public AuView() {
this(null);
}
public AuView(Series series) {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
setPreferredSize(new Dimension(1024, 1024));
setSeries(series);
}
public synchronized Series getSeries() {
return series;
}
public synchronized void setSeries(Series newSeries) {
MediaSeries<?> oldsequence = this.series;
this.series = newSeries;
if (oldsequence == null && newSeries == null) {
return;
}
if (oldsequence != null && oldsequence.equals(newSeries)) {
return;
}
closingSeries(oldsequence);
if (series != null) {
DicomSpecialElement s = null;
List<DicomSpecialElement> specialElements =
(List<DicomSpecialElement>) series.getTagValue(TagW.DicomSpecialElementList);
if (specialElements != null && !specialElements.isEmpty()) {
// Should have only one object by series (if more, they are split in several sub-series in dicomModel)
s = specialElements.get(0);
}
series.setOpen(true);
series.setFocused(true);
series.setSelected(true, null);
try {
showPlayer(s);
} catch (Exception e) {
LOGGER.error("Build audio player", e); //$NON-NLS-1$
}
}
}
private void closingSeries(MediaSeries<?> mediaSeries) {
if (mediaSeries == null) {
return;
}
boolean open = false;
synchronized (UIManager.VIEWER_PLUGINS) {
List<ViewerPlugin<?>> plugins = UIManager.VIEWER_PLUGINS;
pluginList: for (final ViewerPlugin<?> plugin : plugins) {
List<? extends MediaSeries<?>> openSeries = plugin.getOpenSeries();
if (openSeries != null) {
for (MediaSeries<?> s : openSeries) {
if (mediaSeries == s) {
// The sequence is still open in another view or plugin
open = true;
break pluginList;
}
}
}
}
}
mediaSeries.setOpen(open);
// TODO setSelected and setFocused must be global to all view as open
mediaSeries.setSelected(false, null);
mediaSeries.setFocused(false);
}
public void dispose() {
if (series != null) {
closingSeries(series);
series = null;
}
if (clip != null) {
clip.close();
}
}
@Override
public void changingViewContentEvent(SeriesViewerEvent event) {
EVENT type = event.getEventType();
if (EVENT.LAYOUT.equals(type) && event.getSeries() instanceof Series) {
setSeries((Series<?>) event.getSeries());
}
}
// Create a SoundPlayer component for the specified file.
private void showPlayer(final DicomSpecialElement media)
throws IOException, UnsupportedAudioFileException, LineUnavailableException {
try (AudioInputStream audioStream = getAudioInputStream(media)) {
DataLine.Info info = new DataLine.Info(Clip.class, audioStream.getFormat());
clip = (Clip) AudioSystem.getLine(info);
clip.open(audioStream);
}
// Get the clip length in microseconds and convert to milliseconds
audioLength = (int) (clip.getMicrosecondLength() / 1000);
play = new JButton(Messages.getString("AuView.play")); // Play/stop button //$NON-NLS-1$
progress = new JSlider(0, audioLength, 0); // Shows position in sound
time = new JLabel("0"); // Shows position as a # //$NON-NLS-1$
// When clicked, start or stop playing the sound
play.addActionListener(e -> {
if (playing) {
stop();
} else {
play();
}
});
progress.addChangeListener(e -> {
int value = progress.getValue();
time.setText(String.format("%.2f s", value / 1000.0)); //$NON-NLS-1$
// If we're not already there, skip there.
if (value != audioPosition) {
skip(value);
}
});
// This timer calls the tick( ) method 10 times a second to keep
// our slider in sync with the music.
timer = new javax.swing.Timer(100, e -> tick());
// put those controls in a row
Box row = Box.createHorizontalBox();
row.add(play);
row.add(progress);
row.add(time);
// And add them to this component.
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
this.add(row);
addSampledControls();
JButton export = new JButton(Messages.getString("AuView.export_audio")); //$NON-NLS-1$
export.addActionListener(e -> saveAudioFile(media));
this.add(Box.createVerticalStrut(15));
row = Box.createHorizontalBox();
row.add(export);
this.add(row);
}
private void saveAudioFile(DicomSpecialElement media) {
AudioInputStream stream = getAudioInputStream(media);
if (stream != null) {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
fileChooser.setAcceptAllFileFilterUsed(false);
FileFormatFilter filter = new FileFormatFilter("au", "AU"); //$NON-NLS-1$ //$NON-NLS-2$
fileChooser.addChoosableFileFilter(filter);
fileChooser.addChoosableFileFilter(new FileFormatFilter("wav", "WAVE")); //$NON-NLS-1$ //$NON-NLS-2$
fileChooser.setFileFilter(filter);
if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
if (fileChooser.getSelectedFile() != null) {
File file = fileChooser.getSelectedFile();
filter = (FileFormatFilter) fileChooser.getFileFilter();
String extension = filter == null ? ".au" : "." + filter.getDefaultExtension(); //$NON-NLS-1$ //$NON-NLS-2$
String filename = file.getName().endsWith(extension) ? file.getPath() : file.getPath() + extension;
try {
if (".wav".equals(extension)) { //$NON-NLS-1$
AudioSystem.write(stream, AudioFileFormat.Type.WAVE, new File(filename));
} else {
AudioSystem.write(stream, AudioFileFormat.Type.AU, new File(filename));
}
} catch (IOException ex) {
LOGGER.error("Cannot save audio file!", ex); //$NON-NLS-1$
}
}
}
}
}
/** Start playing the sound at the current position */
public void play() {
clip.start();
timer.start();
play.setText(Messages.getString("AuView.stop")); //$NON-NLS-1$
playing = true;
}
/** Stop playing the sound, but retain the current position */
public void stop() {
timer.stop();
clip.stop();
play.setText(Messages.getString("AuView.play")); //$NON-NLS-1$
playing = false;
}
/** Stop playing the sound and reset the position to 0 */
public void reset() {
stop();
clip.setMicrosecondPosition(0);
audioPosition = 0;
progress.setValue(0);
}
/** Skip to the specified position */
public void skip(int position) { // Called when user drags the slider
if (position < 0 || position > audioLength) {
return;
}
audioPosition = position;
clip.setMicrosecondPosition(position * 1000);
progress.setValue(position); // in case skip( ) is called from outside
}
/** Return the length of the sound in ms or ticks */
public int getLength() {
return audioLength;
}
// An internal method that updates the progress bar.
// The Timer object calls it 10 times a second.
// If the sound has finished, it resets to the beginning
void tick() {
if (clip.isActive()) {
audioPosition = (int) (clip.getMicrosecondPosition() / 1000);
progress.setValue(audioPosition);
} else {
reset();
}
}
// For sampled sounds, add sliders to control volume and balance
void addSampledControls() {
try {
FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
if (gainControl != null) {
this.add(createSlider(gainControl));
}
} catch (IllegalArgumentException e) {
// If MASTER_GAIN volume control is unsupported, just skip it
}
try {
// FloatControl.Type.BALANCE is probably the correct control to
// use here, but it doesn't work for me, so I use PAN instead.
FloatControl panControl = (FloatControl) clip.getControl(FloatControl.Type.PAN);
if (panControl != null) {
this.add(createSlider(panControl));
}
} catch (IllegalArgumentException e) {
}
}
// Return a JSlider component to manipulate the supplied FloatControl for sampled audio.
JSlider createSlider(final FloatControl c) {
if (c == null) {
return null;
}
final JSlider s = new JSlider(0, 1000);
final float min = c.getMinimum();
final float max = c.getMaximum();
final float width = max - min;
float fval = c.getValue();
s.setValue((int) ((fval - min) / width * 1000));
java.util.Hashtable<Integer, JLabel> labels = new java.util.Hashtable<>(3);
labels.put(0, new JLabel(c.getMinLabel()));
labels.put(500, new JLabel(c.getMidLabel()));
labels.put(1000, new JLabel(c.getMaxLabel()));
s.setLabelTable(labels);
s.setPaintLabels(true);
s.setBorder(new TitledBorder(c.getType().toString() + " " + c.getUnits())); //$NON-NLS-1$
s.addChangeListener(e -> {
int i = s.getValue();
float f = min + (i * width / 1000.0f);
c.setValue(f);
});
return s;
}
protected AudioInputStream getAudioInputStream(DicomSpecialElement media) {
if (media instanceof DicomAudioElement) {
DicomMediaIO dicomImageLoader = media.getMediaReader();
Attributes attributes = dicomImageLoader.getDicomObject().getNestedDataset(Tag.WaveformSequence);
if (attributes != null) {
VR.Holder holder = new VR.Holder();
Object data = attributes.getValue(Tag.WaveformData, holder);
if (data instanceof BulkData) {
BulkData bulkData = (BulkData) data;
try {
int numChannels = attributes.getInt(Tag.NumberOfWaveformChannels, 0);
double sampleRate = attributes.getDouble(Tag.SamplingFrequency, 0.0);
int bitsPerSample = attributes.getInt(Tag.WaveformBitsAllocated, 0);
String spInterpretation = attributes.getString(Tag.WaveformSampleInterpretation, 0);
// http://medical.nema.org/medical/dicom/current/output/chtml/part03/sect_A.34.html
// SB: signed 8 bit linear
// UB: unsigned 8 bit linear
// MB: 8 bit mu-law (in accordance with ITU-T Recommendation G.711)
// AB: 8 bit A-law (in accordance with ITU-T Recommendation G.711)
// SS: signed 16 bit linear
// US: unsigned 16 bit linear
AudioFormat audioFormat;
if ("MB".equals(spInterpretation) || "AB".equals(spInterpretation)) { //$NON-NLS-1$ //$NON-NLS-2$
int frameSize =
(numChannels == AudioSystem.NOT_SPECIFIED || bitsPerSample == AudioSystem.NOT_SPECIFIED)
? AudioSystem.NOT_SPECIFIED : ((bitsPerSample + 7) / 8) * numChannels;
audioFormat = new AudioFormat("AB".equals(spInterpretation) ? Encoding.ALAW : Encoding.ULAW, //$NON-NLS-1$
(float) sampleRate, bitsPerSample, numChannels, frameSize, (float) sampleRate,
attributes.bigEndian());
} else {
boolean signed =
"UB".equals(spInterpretation) || "US".equals(spInterpretation) ? false : true; //$NON-NLS-1$ //$NON-NLS-2$
audioFormat = new AudioFormat((float) sampleRate, bitsPerSample, numChannels, signed,
attributes.bigEndian());
}
return new AudioInputStream(bulkData.openStream(), audioFormat, bulkData.length() / audioFormat.getFrameSize());
} catch (Exception e) {
LOGGER.error("Get audio stream", e); //$NON-NLS-1$
}
}
}
}
return null;
}
public static void playSound(AudioInputStream audioStream, AudioFormat audioFormat) {
if (audioStream != null && audioFormat != null) {
DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
SourceDataLine sourceLine = null;
try {
sourceLine = (SourceDataLine) AudioSystem.getLine(info);
sourceLine.open(audioFormat);
sourceLine.start();
byte[] bytesBuffer = new byte[8192];
int bytesRead = -1;
while ((bytesRead = audioStream.read(bytesBuffer)) != -1) {
sourceLine.write(bytesBuffer, 0, bytesRead);
}
} catch (Exception e) {
LOGGER.error("Play audio stream", e); //$NON-NLS-1$
} finally {
if (sourceLine != null) {
sourceLine.drain();
sourceLine.close();
}
}
}
}
}