├── .gitignore ├── LICENSE ├── README.md ├── example.png └── src └── com └── remyoukaour └── spectrogram ├── BufferedRandomAccessFile.java ├── ByteUtils.java ├── DualScrollPane.java ├── FormatUtils.java ├── IndeterminateProgressMonitor.java ├── Player.java ├── SampledData.java ├── SampledDiskData.java ├── SampledMemoryData.java ├── Signal.java ├── SignalIterator.java ├── SignalPanel.java ├── SignalWindow.java ├── Spectrogram.java ├── SpectrogramMain.java ├── Spectrum.java ├── StatusBar.java ├── Waveform.java └── WindowFunction.java /.gitignore: -------------------------------------------------------------------------------- 1 | .settings/ 2 | bin/ 3 | .classpath 4 | .project 5 | Spectrogram.jar -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Remy Oukaour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spectrogram 2 | 3 | ![A screenshot of the spectrogram program.](example.png) 4 | 5 | *A waveform and spectrogram of "[Formula]" from Aphex Twin's album* Windowlicker, 6 | *showing a hidden face.* 7 | 8 | This program can display waveforms and spectrograms for audio files, as well as the reverse process, converting spectrogram images back into audio. 9 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roukaour/spectrogram/9ec8c965bc65abaa7c5f5de4a90c99feac3e35c1/example.png -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/BufferedRandomAccessFile.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.io.*; 4 | 5 | public class BufferedRandomAccessFile extends RandomAccessFile { 6 | private static final int DEFAULT_BUFFER = 2048; 7 | 8 | private final int size; 9 | private final byte[] buffer; 10 | private int at; 11 | 12 | public BufferedRandomAccessFile(String name, String mode) 13 | throws FileNotFoundException { 14 | this(new File(name), mode, DEFAULT_BUFFER); 15 | } 16 | 17 | public BufferedRandomAccessFile(String name, String mode, int size) 18 | throws FileNotFoundException { 19 | this(new File(name), mode, size); 20 | } 21 | 22 | public BufferedRandomAccessFile(File file, String mode) 23 | throws FileNotFoundException { 24 | this(file, mode, DEFAULT_BUFFER); 25 | } 26 | 27 | public BufferedRandomAccessFile(File file, String mode, int size) 28 | throws FileNotFoundException { 29 | super(file, mode); 30 | this.size = size; 31 | this.buffer = new byte[size]; 32 | this.at = 0; 33 | } 34 | 35 | public void writeBuffered(byte b) throws IOException { 36 | buffer[at++] = b; 37 | if (at >= size) { 38 | flushBuffer(); 39 | } 40 | } 41 | 42 | public void writeBuffered(byte[] b) throws IOException { 43 | writeBuffered(b, 0, b.length); 44 | } 45 | 46 | public void writeBuffered(byte[] b, int off, int len) throws IOException { 47 | int s = off + len; 48 | for (int i = off; i < s; i++) { 49 | writeBuffered(b[i]); 50 | } 51 | } 52 | 53 | public void flushBuffer() throws IOException { 54 | write(buffer, 0, at); 55 | at = 0; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/ByteUtils.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | public class ByteUtils { 4 | public static short bytesToShort(byte[] bytes, int off, boolean bigEndian) { 5 | byte b1 = bytes[off], b2 = bytes[off+1]; 6 | if (!bigEndian) { 7 | byte tmp = b1; 8 | b1 = b2; 9 | b2 = tmp; 10 | } 11 | return (short)(((b1 & 0xFF) << 8) | (b2 & 0xFF)); 12 | } 13 | 14 | public static void shortToBytes(short s, boolean bigEndian, byte[] bytes, 15 | int off) { 16 | byte b1 = (byte)((s >> 8) & 0xFF); 17 | byte b2 = (byte)(s & 0xFF); 18 | if (!bigEndian) { 19 | byte tmp = b1; 20 | b1 = b2; 21 | b2 = tmp; 22 | } 23 | bytes[off] = b1; 24 | bytes[off+1] = b2; 25 | } 26 | 27 | public static double bytesToDouble(byte[] bytes, int off) { 28 | long el = 0L; 29 | int shift = 64; 30 | int lim = off + 8; 31 | for (int i = off; i < lim; i++) { 32 | shift -= 8; 33 | el |= (long)(bytes[i] & 0xFF) << shift; 34 | } 35 | return Double.longBitsToDouble(el); 36 | } 37 | 38 | public static void doubleToBytes(double v, byte[] bytes, int off) { 39 | long el = Double.doubleToRawLongBits(v); 40 | int shift = 64; 41 | int lim = off + 8; 42 | for (int i = off; i < lim; i++) { 43 | shift -= 8; 44 | bytes[i] = (byte)((el >> shift) & 0xFF); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/DualScrollPane.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.awt.BorderLayout; 4 | import javax.swing.*; 5 | 6 | public class DualScrollPane extends JPanel { 7 | private static final long serialVersionUID = 8254516895802138993L; 8 | 9 | private final JSplitPane splitPane; 10 | 11 | public DualScrollPane(JScrollPane pane1, JScrollPane pane2, boolean shareH, boolean shareV) { 12 | splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, pane1, pane2); 13 | splitPane.setResizeWeight(0.5); 14 | setLayout(new BorderLayout()); 15 | add(splitPane); 16 | if (shareH) 17 | pane1.getHorizontalScrollBar().setModel(pane2.getHorizontalScrollBar().getModel()); 18 | if (shareV) 19 | pane1.getVerticalScrollBar().setModel(pane2.getVerticalScrollBar().getModel()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/FormatUtils.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.text.DecimalFormat; 4 | 5 | public class FormatUtils { 6 | public static String formatInt(int n) { 7 | return String.format("%,d", n); 8 | } 9 | 10 | public static String formatFactor(int value, int base) { 11 | int v = value > base ? value / base : base / value; 12 | String p = value > base ? "1/" : ""; 13 | return String.format("%s%sx", p, formatInt(v)); 14 | } 15 | 16 | public static String formatCount(int n, String word) { 17 | String s = n != 1 ? "s" : ""; 18 | return String.format("%s %s%s", formatInt(n), word, s); 19 | } 20 | 21 | public static String formatPercent(double v) { 22 | return new DecimalFormat("#.####%").format(v); 23 | } 24 | 25 | public static String formatTime(float seconds) { 26 | int m = (int)seconds / 60; 27 | int s = (int)seconds % 60; 28 | int u = (int)((seconds % 1) * 100); 29 | return String.format("%d:%02d.%02d", m, s, u); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/IndeterminateProgressMonitor.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.awt.Component; 4 | import javax.swing.JProgressBar; 5 | import javax.swing.ProgressMonitor; 6 | 7 | public class IndeterminateProgressMonitor extends ProgressMonitor { 8 | public IndeterminateProgressMonitor(Component parent, Object message, 9 | String note) { 10 | super(parent, message, note, 0, 100); 11 | setMillisToDecideToPopup(0); 12 | setMillisToPopup(0); 13 | setProgress(50); 14 | makeIndeterminate(); 15 | } 16 | 17 | public IndeterminateProgressMonitor(Component parent, Object message, 18 | String note, int min, int max) { 19 | super(parent, message, note, min, max); 20 | } 21 | 22 | public void makeIndeterminate() { 23 | try { 24 | JProgressBar bar = (JProgressBar)getAccessibleContext().getAccessibleChild(1); 25 | bar.setIndeterminate(true); 26 | } 27 | catch (Exception ex) {} 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/Player.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.io.IOException; 4 | import javax.sound.sampled.*; 5 | import javax.swing.JOptionPane; 6 | 7 | public class Player extends Thread { 8 | private static final int BUFFERED_FRAMES = 1024; 9 | 10 | private final SignalWindow window; 11 | private final Signal signal; 12 | 13 | public Player(SignalWindow window, Signal signal) { 14 | this.window = window; 15 | this.signal = signal; 16 | } 17 | 18 | public void run() { 19 | AudioInputStream ain = null; 20 | AudioFormat format = null; 21 | SourceDataLine outputLine = null; 22 | try { 23 | ain = signal.toStream(); 24 | format = ain.getFormat(); 25 | outputLine = AudioSystem.getSourceDataLine(format); 26 | outputLine.open(format); 27 | } 28 | catch (LineUnavailableException ex) { 29 | JOptionPane.showMessageDialog(window, "Error: " + ex, 30 | "Error", JOptionPane.ERROR_MESSAGE); 31 | } 32 | if (ain == null || outputLine == null) 33 | return; 34 | outputLine.start(); 35 | byte[] buffer = new byte[format.getFrameSize() * BUFFERED_FRAMES]; 36 | try { 37 | int n; 38 | while (window.isPlaying() && (n = ain.read(buffer)) != -1) { 39 | outputLine.write(buffer, 0, n); 40 | window.updateCursor(outputLine.getMicrosecondPosition()); 41 | } 42 | } 43 | catch (IOException ex) { 44 | JOptionPane.showMessageDialog(window, "Error: " + ex.getMessage(), 45 | "Error", JOptionPane.ERROR_MESSAGE); 46 | } 47 | outputLine.drain(); 48 | outputLine.close(); 49 | window.updateCursor(SignalPanel.NO_CURSOR); 50 | window.stop(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/SampledData.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import javax.sound.sampled.AudioFormat; 5 | import javax.sound.sampled.AudioInputStream; 6 | 7 | public abstract class SampledData { 8 | private static final int BUFFERED_SAMPLES = 2048; 9 | 10 | public abstract int size(); 11 | public abstract double get(int i); 12 | public abstract double[] get(int start, int length); 13 | 14 | public AudioInputStream toStream(int hertz) { 15 | AudioFormat format = new AudioFormat(hertz, 16, 1, true, true); 16 | int n = size(); 17 | byte[] data = new byte[n * 2]; 18 | int off = 0; 19 | for (int i = 0; i < n; i += BUFFERED_SAMPLES) { 20 | double[] buffer = get(i, BUFFERED_SAMPLES); 21 | for (int j = 0; j < BUFFERED_SAMPLES && i + j < n; j++, off += 2) { 22 | double v = buffer[j] * 32768.0; 23 | short s = v > Short.MAX_VALUE ? Short.MAX_VALUE : 24 | v < Short.MIN_VALUE ? Short.MIN_VALUE : (short)v; 25 | ByteUtils.shortToBytes(s, true, data, off); 26 | } 27 | } 28 | return new AudioInputStream(new ByteArrayInputStream(data), format, n); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/SampledDiskData.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.io.IOException; 4 | import java.io.RandomAccessFile; 5 | import java.util.LinkedHashMap; 6 | import java.util.Map; 7 | 8 | public class SampledDiskData extends SampledData { 9 | private static final int CAPACITY = 24; 10 | private static final int BLOCK_SIZE = 2048; 11 | 12 | private final RandomAccessFile raf; 13 | private final int size; 14 | private final Map cache; 15 | 16 | public SampledDiskData(RandomAccessFile raf, int size) { 17 | this.raf = raf; 18 | this.size = size; 19 | this.cache = new LinkedHashMap(CAPACITY + 1, 1.1f, true) { 20 | private static final long serialVersionUID = 865438240L; 21 | 22 | @SuppressWarnings("unused") 23 | protected boolean removeEldestEntry(Integer eldest) { 24 | return size() > CAPACITY; 25 | } 26 | }; 27 | } 28 | 29 | public int size() { 30 | return size; 31 | } 32 | 33 | public double get(int i) { 34 | try { 35 | raf.seek(i); 36 | return raf.readDouble(); 37 | } 38 | catch (IOException ex) { 39 | return 0.0; 40 | } 41 | } 42 | 43 | public double[] get(int start, int length) { 44 | double[] data = new double[length]; 45 | if (start < 0) { 46 | length += start; 47 | start = 0; 48 | } 49 | if (start + length > size) { 50 | length = size - start; 51 | } 52 | int blockIndex = start / BLOCK_SIZE; 53 | int indexIntoBlock = start % BLOCK_SIZE; 54 | double[] block = getBlock(blockIndex); 55 | int lengthInBlock = BLOCK_SIZE - indexIntoBlock; 56 | if (lengthInBlock < length && size > length) { 57 | System.arraycopy(block, indexIntoBlock, data, 0, lengthInBlock); 58 | int restLength = length - lengthInBlock; 59 | double[] rest = get(start + lengthInBlock, restLength); 60 | System.arraycopy(rest, 0, data, lengthInBlock, restLength); 61 | } 62 | else { 63 | System.arraycopy(block, indexIntoBlock, data, 0, length); 64 | } 65 | return data; 66 | } 67 | 68 | private double[] getBlock(int blockIndex) { 69 | double[] block = cache.get(blockIndex); 70 | if (block != null) 71 | return block; 72 | block = new double[BLOCK_SIZE]; 73 | try { 74 | int start = blockIndex * BLOCK_SIZE; 75 | int n = BLOCK_SIZE; 76 | if (start + n >= size) { 77 | n = size - start; 78 | } 79 | int width = 8; 80 | n *= width; 81 | byte[] bytes = new byte[n]; 82 | raf.seek(start * width); 83 | raf.readFully(bytes); 84 | for (int i = 0, j = 0; j < n; i++, j += width) { 85 | block[i] = ByteUtils.bytesToDouble(bytes, j); 86 | } 87 | } 88 | catch (IOException ex) { 89 | ex.printStackTrace(); 90 | } 91 | cache.put(blockIndex, block); 92 | return block; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/SampledMemoryData.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | public class SampledMemoryData extends SampledData { 4 | private final double[] samples; 5 | 6 | public SampledMemoryData(double[] samples) { 7 | this.samples = samples; 8 | } 9 | 10 | public int size() { 11 | return samples.length; 12 | } 13 | 14 | public double get(int i) { 15 | return samples[i]; 16 | } 17 | 18 | public double[] get(int start, int length) { 19 | double[] data = new double[length]; 20 | if (start < 0) { 21 | length += start; 22 | start = 0; 23 | } 24 | if (start + length > samples.length) { 25 | length = samples.length - start; 26 | } 27 | System.arraycopy(samples, start, data, 0, length); 28 | return data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/Signal.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.awt.Component; 4 | import java.io.*; 5 | import javax.sound.sampled.*; 6 | import javax.swing.*; 7 | 8 | public class Signal { 9 | private static final int BUFFERED_DOUBLES = 2048; 10 | 11 | private final String name; 12 | private final SampledData samples; 13 | private final int hertz; 14 | 15 | public static Signal fromFile(Component parent, File f, boolean useMemory) 16 | throws UnsupportedAudioFileException, IOException { 17 | String name = f.getName(); 18 | SignalIterator iter = new SignalIterator(f); 19 | int length = iter.getRemaining(); 20 | int hertz = iter.getHertz(); 21 | boolean decoded = iter.isDecoded(); 22 | ProgressMonitor monitor = null; 23 | if (decoded) { 24 | monitor = new IndeterminateProgressMonitor(parent, 25 | "Loading " + name + "...", null); 26 | } 27 | else { 28 | ProgressMonitorInputStream pin = (ProgressMonitorInputStream) 29 | iter.getStream(parent, "Loading " + name + "..."); 30 | monitor = pin.getProgressMonitor(); 31 | monitor.setMillisToDecideToPopup(250); 32 | monitor.setMillisToPopup(250); 33 | } 34 | SampledData data = null; 35 | if (useMemory) { 36 | double[] samples = new double[length]; 37 | for (int i = 0; i < length; i++) { 38 | Double v = iter.next(); 39 | if (v == null || monitor.isCanceled()) 40 | throw new IOException("failed to read sample"); 41 | samples[i] = v; 42 | } 43 | iter.close(); 44 | data = new SampledMemoryData(samples); 45 | } 46 | else { 47 | File temp = File.createTempFile("signal_" + name, null); 48 | temp.deleteOnExit(); 49 | int width = 8; 50 | BufferedRandomAccessFile braf = new BufferedRandomAccessFile(temp, 51 | "rw", width * BUFFERED_DOUBLES); 52 | byte[] bytes = new byte[width]; 53 | for (int i = 0; i < length; i++) { 54 | Double v = iter.next(); 55 | if (v == null || monitor.isCanceled()) { 56 | braf.close(); 57 | throw new IOException("failed to read sample"); 58 | } 59 | ByteUtils.doubleToBytes(v, bytes, 0); 60 | braf.writeBuffered(bytes); 61 | } 62 | braf.flushBuffer(); 63 | braf.close(); 64 | iter.close(); 65 | RandomAccessFile raf = new RandomAccessFile(temp, "r"); 66 | data = new SampledDiskData(raf, length); 67 | } 68 | monitor.close(); 69 | return new Signal(name, data, hertz); 70 | } 71 | 72 | protected Signal(String name, SampledData samples, int hertz) { 73 | this.name = name; 74 | this.samples = samples; 75 | this.hertz = hertz; 76 | } 77 | 78 | public String getName() { 79 | return name; 80 | } 81 | 82 | public int getHertz() { 83 | return hertz; 84 | } 85 | 86 | public int getNumSamples() { 87 | return samples.size(); 88 | } 89 | 90 | public double[] getSamples(int start, int length) { 91 | return samples.get(start, length); 92 | } 93 | 94 | public Spectrum getSpectrum(int i, int length) { 95 | return getSpectrum(i, length, null, false); 96 | } 97 | 98 | public Spectrum getSpectrum(int i, int length, WindowFunction window, 99 | boolean showPhase) { 100 | double[] samples = getSamples(i - length, length * 2); 101 | return new Spectrum(samples, i, window, showPhase); 102 | } 103 | 104 | public AudioInputStream toStream() { 105 | return samples.toStream(hertz); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/SignalIterator.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.awt.Component; 4 | import java.io.*; 5 | import java.util.Iterator; 6 | import javax.sound.sampled.*; 7 | import javax.swing.ProgressMonitorInputStream; 8 | import org.kc7bfi.jflac.sound.spi.*; 9 | import org.tritonus.share.sampled.file.TAudioFileFormat; 10 | 11 | public class SignalIterator implements Iterator { 12 | private static final int BUFFERED_FRAMES = 2048; 13 | 14 | private InputStream in; 15 | private final boolean decoded; 16 | private final int hertz; 17 | private final int channels; 18 | private final int bytesPerSample; 19 | private final boolean bigEndian; 20 | private final double maxSample; 21 | private final byte[] buffer; 22 | private int remaining; 23 | 24 | public SignalIterator(File f) throws UnsupportedAudioFileException, IOException { 25 | AudioInputStream ain; 26 | AudioFileFormat fileFormat; 27 | AudioFormat format; 28 | try { 29 | ain = AudioSystem.getAudioInputStream(f); 30 | fileFormat = AudioSystem.getAudioFileFormat(f); 31 | format = ain.getFormat(); 32 | } 33 | catch (UnsupportedAudioFileException ex) { 34 | FlacAudioFileReader r = new FlacAudioFileReader(); 35 | ain = r.getAudioInputStream(f); 36 | fileFormat = r.getAudioFileFormat(ain); 37 | format = fileFormat.getFormat(); 38 | } 39 | long length = -1; 40 | if (fileFormat instanceof TAudioFileFormat) { 41 | AudioFormat newFormat = new AudioFormat(format.getSampleRate(), 16, 42 | format.getChannels(), true, false); 43 | ain = AudioSystem.getAudioInputStream(newFormat, ain); 44 | format = ain.getFormat(); 45 | long duration = (Long)fileFormat.properties().get("duration"); 46 | length = (long)(duration / 1000000 * format.getSampleRate()); 47 | this.decoded = true; 48 | } 49 | else if (fileFormat.getType() == FlacFileFormatType.FLAC) { 50 | /* 51 | AudioFormat newFormat = new AudioFormat(format.getSampleRate(), 16, 52 | format.getChannels(), true, false); 53 | // Flac2PcmAudioInputStream(ain, newFormat, ?) 54 | // how to get the number of samples? 55 | */ 56 | throw new UnsupportedAudioFileException("FLAC"); 57 | } 58 | else { 59 | length = ain.getFrameLength(); 60 | this.decoded = false; 61 | } 62 | if (length <= 0) 63 | throw new IllegalArgumentException("Too short"); 64 | if (length > Integer.MAX_VALUE) 65 | throw new IllegalArgumentException("Too long"); 66 | AudioFormat.Encoding encoding = format.getEncoding(); 67 | if (encoding != AudioFormat.Encoding.PCM_SIGNED) 68 | throw new IllegalArgumentException("Unsupported encoding: " + encoding); 69 | this.hertz = (int)format.getSampleRate(); 70 | this.channels = format.getChannels(); 71 | int bytesPerFrame = format.getFrameSize(); 72 | this.bytesPerSample = bytesPerFrame / channels; 73 | if (bytesPerSample != 2) 74 | throw new IllegalArgumentException("16-bit samples required"); 75 | this.bigEndian = format.isBigEndian(); 76 | this.in = new BufferedInputStream(ain, bytesPerFrame * BUFFERED_FRAMES); 77 | this.maxSample = Math.pow(2, bytesPerSample * 8 - 1); 78 | this.buffer = new byte[bytesPerFrame]; 79 | this.remaining = (int)length; 80 | } 81 | 82 | public boolean isDecoded() { 83 | return decoded; 84 | } 85 | 86 | public InputStream getStream() { 87 | return in; 88 | } 89 | 90 | public InputStream getStream(Component parent, String message) { 91 | if (!(in instanceof ProgressMonitorInputStream)) 92 | in = new ProgressMonitorInputStream(parent, message, in); 93 | return in; 94 | } 95 | 96 | public int getHertz() { 97 | return hertz; 98 | } 99 | 100 | public int getRemaining() { 101 | return remaining; 102 | } 103 | 104 | public void close() throws IOException { 105 | in.close(); 106 | } 107 | 108 | @Override 109 | public boolean hasNext() { 110 | return remaining > 0; 111 | } 112 | 113 | @Override 114 | public Double next() { 115 | try { 116 | in.read(buffer); 117 | } 118 | catch (IOException ex) { 119 | return null; 120 | } 121 | double v = 0.0; 122 | for (int j = 0; j < channels; j++) { 123 | short s = ByteUtils.bytesToShort(buffer, j * bytesPerSample, bigEndian); 124 | v += s / maxSample; 125 | } 126 | v /= channels; 127 | remaining--; 128 | return v; 129 | } 130 | 131 | @Override 132 | public void remove() { 133 | throw new UnsupportedOperationException(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/SignalPanel.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.awt.*; 4 | import javax.swing.JPanel; 5 | 6 | public abstract class SignalPanel extends JPanel { 7 | private static final long serialVersionUID = 7742340141168006910L; 8 | 9 | public static final long NO_CURSOR = -1; 10 | 11 | protected Signal signal; 12 | protected int zoom; 13 | protected int prefHeight; 14 | protected long cursor; 15 | 16 | public SignalPanel() { 17 | this.signal = null; 18 | this.zoom = 1; 19 | this.prefHeight = 0; 20 | this.cursor = -1; 21 | rescale(); 22 | } 23 | 24 | public Signal getSignal() { 25 | return signal; 26 | } 27 | 28 | public void setSignal(Signal signal) { 29 | this.signal = signal; 30 | rescale(); 31 | } 32 | 33 | public int getPrefHeight() { 34 | return prefHeight; 35 | } 36 | 37 | public void setPrefHeight(int prefHeight) { 38 | this.prefHeight = prefHeight; 39 | rescale(); 40 | } 41 | 42 | public int getZoom() { 43 | return zoom; 44 | } 45 | 46 | public void zoomIn() { 47 | if (zoom > 1) 48 | zoom /= 2; 49 | rescale(); 50 | } 51 | 52 | public void zoomOut() { 53 | zoom *= 2; 54 | rescale(); 55 | } 56 | 57 | public void zoomTo(int zoom) { 58 | this.zoom = zoom; 59 | rescale(); 60 | } 61 | 62 | protected void rescale() { 63 | int prefWidth = signal != null ? (int)(signal.getNumSamples() / zoom) : 0; 64 | setPreferredSize(new Dimension(prefWidth, prefHeight)); 65 | revalidate(); 66 | repaint(); 67 | } 68 | 69 | protected void drawCursor(Graphics g) { 70 | if (cursor != NO_CURSOR) { 71 | int cx = (int)(cursor / zoom); 72 | g.setColor(Color.YELLOW); 73 | g.drawLine(cx, 0, cx, getHeight()); 74 | } 75 | } 76 | 77 | public synchronized void updateCursor(long microseconds) { 78 | int h = (int)getHeight(); 79 | if (microseconds == NO_CURSOR) { 80 | cursor = NO_CURSOR; 81 | repaint(); 82 | return; 83 | } 84 | repaint((int)(cursor / zoom) - 1, 0, 3, h); 85 | double seconds = microseconds / 1000000.0; 86 | cursor = (long)(signal.getHertz() * seconds); 87 | repaint((int)(cursor / zoom) - 1, 0, 3, h); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/SignalWindow.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Overlap iFFT blocks instead of taking half the samples 3 | * Optionally use phase info when encoding/decoding spectrogram 4 | * Support FLAC files 5 | * Support bit depths other than 16 6 | * Write a help file 7 | * http://arss.sourceforge.net/ 8 | * http://devrand.org/view/imageSpectrogram 9 | * http://www.cs.unm.edu/~brayer/vision/fourier.html 10 | * http://www.44342.com/dsp-f288-t15339-p1.htm 11 | * http://dsp.ucsd.edu/students/present-students/mik/specanalyzer/01_intro.htm 12 | * http://labrosa.ee.columbia.edu/matlab/pvoc/ 13 | * http://cobweb.ecn.purdue.edu/~malcolm/interval/1994-014/#4 14 | * http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.72.6336&rep=rep1&type=pdf 15 | */ 16 | 17 | package com.remyoukaour.spectrogram; 18 | 19 | import java.awt.*; 20 | import java.awt.event.*; 21 | import java.awt.image.BufferedImage; 22 | import javax.imageio.ImageIO; 23 | import javax.swing.*; 24 | import javax.swing.filechooser.FileFilter; 25 | import java.io.*; 26 | import java.util.Arrays; 27 | import java.util.Collections; 28 | import java.util.HashMap; 29 | import java.util.concurrent.ExecutionException; 30 | import org.kc7bfi.jflac.apps.ExtensionFileFilter; 31 | 32 | public class SignalWindow extends JFrame implements ActionListener, ItemListener { 33 | private static final long serialVersionUID = 5825907294405308517L; 34 | 35 | /*** Constants ***/ 36 | 37 | private static final int DEFAULT_BINS = 2048; 38 | private static double DEFAULT_OVERLAP = 0.5; 39 | private static final WindowFunction DEFAULT_WINDOW = WindowFunction.HAMMING; 40 | private static final String[] 41 | AUDIO_READ_EXTENSIONS = {"wav", "au", "mp3", "ogg", "oga"/*, "flac"*/}, 42 | AUDIO_WRITE_EXTENSIONS = {"wav"}, 43 | IMAGE_EXTENSIONS = {"png"}; 44 | 45 | /*** Members ***/ 46 | 47 | private final String name; 48 | private final Waveform waveform = new Waveform(); 49 | private final Spectrogram spectrogram = new Spectrogram(DEFAULT_BINS, 50 | DEFAULT_OVERLAP, DEFAULT_WINDOW, false, false, false); 51 | private boolean playing = false; 52 | private boolean busy = false; 53 | 54 | /*** Components ***/ 55 | 56 | private final JMenuBar menuBar = new JMenuBar(); 57 | private final JMenu fileMenu = new JMenu("File"); 58 | private final JMenuItem open = new JMenuItem("Open audio...", KeyEvent.VK_O); 59 | private final JMenuItem close = new JMenuItem("Close audio", KeyEvent.VK_C); 60 | private final JMenuItem save = new JMenuItem("Save image...", KeyEvent.VK_S); 61 | private final JMenuItem convert = new JMenuItem("Image to audio...", KeyEvent.VK_A); 62 | private final JMenuItem exit = new JMenuItem("Exit", KeyEvent.VK_X); 63 | private final JMenu viewMenu = new JMenu("View"); 64 | private final JMenuItem zoomIn = new JMenuItem("Zoom in", KeyEvent.VK_I); 65 | private final JMenuItem zoomOut = new JMenuItem("Zoom out", KeyEvent.VK_O); 66 | private final JMenuItem zoomMax = new JMenuItem("Zoom max", KeyEvent.VK_X); 67 | private final JMenuItem zoomFit = new JMenuItem("Zoom fit", KeyEvent.VK_F); 68 | private final JMenu spectrogramMenu = new JMenu("Spectrogram"); 69 | private final JMenu setBins = new JMenu("Frequency bins"); 70 | private final ButtonGroup binsGroup = new ButtonGroup(); 71 | private HashMap binsItems = 72 | new HashMap(); 73 | private final JMenu setOverlap = new JMenu("Spectrum overlap"); 74 | private final ButtonGroup overlapGroup = new ButtonGroup(); 75 | private HashMap overlapItems = 76 | new HashMap(); 77 | private final JMenu setWindow = new JMenu("Window function"); 78 | private final ButtonGroup windowGroup = new ButtonGroup(); 79 | private HashMap windowItems = 80 | new HashMap(); 81 | private final JCheckBoxMenuItem logAxis = new JCheckBoxMenuItem("Log frequency"); 82 | private final JCheckBoxMenuItem fullHeight = new JCheckBoxMenuItem("Full height"); 83 | private final JCheckBoxMenuItem showPhase = new JCheckBoxMenuItem("Show phase"); 84 | private final JMenu playbackMenu = new JMenu("Playback"); 85 | private final JMenuItem play = new JMenuItem("Play", KeyEvent.VK_P); 86 | private final JMenuItem stop = new JMenuItem("Stop", KeyEvent.VK_S); 87 | private final JMenu memoryMenu = new JMenu("Memory"); 88 | private final JCheckBoxMenuItem audioMemory = new JCheckBoxMenuItem("Store audio in memory"); 89 | private final JMenuItem garbageCollect = new JMenuItem("Garbage collect", KeyEvent.VK_G); 90 | private final JMenu helpMenu = new JMenu("Help"); 91 | private final JMenuItem help = new JMenuItem("Help", KeyEvent.VK_H); 92 | private final JMenuItem about = new JMenuItem("About", KeyEvent.VK_A); 93 | private final JScrollPane waveformScroll = new JScrollPane(); 94 | private final JScrollPane spectrogramScroll = new JScrollPane(); 95 | private final DualScrollPane dualScroll = 96 | new DualScrollPane(waveformScroll, spectrogramScroll, true, false); 97 | private final StatusBar statusBar = new StatusBar(); 98 | private final JFileChooser fc = new JFileChooser(); 99 | private final FileFilter audioReadFilter = 100 | new ExtensionFileFilter(AUDIO_READ_EXTENSIONS, "Audio files"); 101 | private final FileFilter audioWriteFilter = 102 | new ExtensionFileFilter(AUDIO_WRITE_EXTENSIONS, "Audio files"); 103 | private final FileFilter imageFilter = 104 | new ExtensionFileFilter(IMAGE_EXTENSIONS, "Image files"); 105 | 106 | /*** Constructors ***/ 107 | 108 | public SignalWindow(String title) { 109 | super(title); 110 | this.name = title; 111 | // configuration 112 | setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 113 | setLocationByPlatform(true); 114 | // components 115 | setupMenuBar(); 116 | setupLayout(); 117 | } 118 | 119 | /*** Setup ***/ 120 | 121 | private void setupMenuBar() { 122 | // mnemonics 123 | fileMenu.setMnemonic(KeyEvent.VK_F); 124 | viewMenu.setMnemonic(KeyEvent.VK_V); 125 | spectrogramMenu.setMnemonic(KeyEvent.VK_S); 126 | playbackMenu.setMnemonic(KeyEvent.VK_P); 127 | memoryMenu.setMnemonic(KeyEvent.VK_M); 128 | helpMenu.setMnemonic(KeyEvent.VK_H); 129 | setBins.setMnemonic(KeyEvent.VK_B); 130 | setOverlap.setMnemonic(KeyEvent.VK_O); 131 | setWindow.setMnemonic(KeyEvent.VK_W); 132 | logAxis.setMnemonic(KeyEvent.VK_L); 133 | fullHeight.setMnemonic(KeyEvent.VK_H); 134 | showPhase.setMnemonic(KeyEvent.VK_P); 135 | audioMemory.setMnemonic(KeyEvent.VK_M); 136 | // accelerators 137 | open.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK)); 138 | close.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, ActionEvent.CTRL_MASK)); 139 | save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK)); 140 | convert.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK)); 141 | exit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK)); 142 | zoomIn.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, ActionEvent.CTRL_MASK)); 143 | zoomOut.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, ActionEvent.CTRL_MASK)); 144 | zoomMax.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, ActionEvent.CTRL_MASK)); 145 | zoomFit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_END, ActionEvent.CTRL_MASK)); 146 | logAxis.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, ActionEvent.CTRL_MASK)); 147 | fullHeight.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, ActionEvent.CTRL_MASK)); 148 | showPhase.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, ActionEvent.CTRL_MASK)); 149 | play.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, ActionEvent.CTRL_MASK)); 150 | stop.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK)); 151 | audioMemory.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, ActionEvent.CTRL_MASK)); 152 | garbageCollect.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, ActionEvent.CTRL_MASK)); 153 | help.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0)); 154 | about.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, ActionEvent.CTRL_MASK)); 155 | // structure 156 | setJMenuBar(menuBar); 157 | menuBar.add(fileMenu); 158 | menuBar.add(viewMenu); 159 | menuBar.add(spectrogramMenu); 160 | menuBar.add(playbackMenu); 161 | menuBar.add(memoryMenu); 162 | menuBar.add(helpMenu); 163 | fileMenu.add(open); 164 | fileMenu.add(close); 165 | fileMenu.add(save); 166 | fileMenu.add(convert); 167 | fileMenu.add(exit); 168 | viewMenu.add(zoomIn); 169 | viewMenu.add(zoomOut); 170 | viewMenu.add(zoomMax); 171 | viewMenu.add(zoomFit); 172 | spectrogramMenu.add(setBins); 173 | spectrogramMenu.add(setOverlap); 174 | spectrogramMenu.add(setWindow); 175 | spectrogramMenu.add(logAxis); 176 | spectrogramMenu.add(fullHeight); 177 | spectrogramMenu.add(showPhase); 178 | playbackMenu.add(play); 179 | playbackMenu.add(stop); 180 | memoryMenu.add(audioMemory); 181 | memoryMenu.add(garbageCollect); 182 | helpMenu.add(help); 183 | helpMenu.add(about); 184 | // listeners 185 | open.addActionListener(this); 186 | close.addActionListener(this); 187 | save.addActionListener(this); 188 | convert.addActionListener(this); 189 | exit.addActionListener(this); 190 | play.addActionListener(this); 191 | stop.addActionListener(this); 192 | zoomIn.addActionListener(this); 193 | zoomOut.addActionListener(this); 194 | zoomMax.addActionListener(this); 195 | zoomFit.addActionListener(this); 196 | logAxis.addItemListener(this); 197 | fullHeight.addItemListener(this); 198 | showPhase.addItemListener(this); 199 | garbageCollect.addActionListener(this); 200 | help.addActionListener(this); 201 | about.addActionListener(this); 202 | // components 203 | setupBinMenus(); 204 | setupOverlapMenus(); 205 | setupWindowMenus(); 206 | } 207 | 208 | private void setupBinMenus() { 209 | int[] binsValues = new int[] {32, 64, 128, 256, 512, 1024, 2048, 4096, 210 | 8000, 8192, 11025, 16384, 22050, 32768, 44100}; 211 | for (int i = 0; i < binsValues.length; i++) { 212 | int value = binsValues[i]; 213 | JRadioButtonMenuItem item = new JRadioButtonMenuItem(FormatUtils.formatInt(value)); 214 | setBins.add(item); 215 | item.addActionListener(this); 216 | binsGroup.add(item); 217 | binsItems.put(item, value); 218 | if (value == DEFAULT_BINS) 219 | item.setSelected(true); 220 | } 221 | } 222 | 223 | private void setupOverlapMenus() { 224 | double[] overlapValues = new double[] {1.0, 0.75, 0.5, 0.25, 0.125, 225 | 0.0625, 0.03125, 0.015625}; 226 | for (int i = 0; i < overlapValues.length; i++) { 227 | double value = overlapValues[i]; 228 | JRadioButtonMenuItem item = new JRadioButtonMenuItem(FormatUtils.formatPercent(1 - value)); 229 | setOverlap.add(item); 230 | item.addActionListener(this); 231 | overlapGroup.add(item); 232 | overlapItems.put(item, value); 233 | if (value == DEFAULT_OVERLAP) 234 | item.setSelected(true); 235 | } 236 | } 237 | 238 | private void setupWindowMenus() { 239 | WindowFunction[] windowValues = WindowFunction.values(); 240 | for (int i = 0; i < windowValues.length; i++) { 241 | WindowFunction value = windowValues[i]; 242 | JRadioButtonMenuItem item = new JRadioButtonMenuItem(value.getName()); 243 | setWindow.add(item); 244 | item.addActionListener(this); 245 | windowGroup.add(item); 246 | windowItems.put(item, value); 247 | if (value == DEFAULT_WINDOW) 248 | item.setSelected(true); 249 | } 250 | } 251 | 252 | private void setupLayout() { 253 | // structure 254 | setLayout(new BorderLayout()); 255 | add(dualScroll, BorderLayout.CENTER); 256 | add(statusBar, BorderLayout.SOUTH); 257 | // configuration 258 | waveformScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); 259 | waveformScroll.setViewportView(waveform); 260 | spectrogramScroll.setViewportView(spectrogram); 261 | } 262 | 263 | /*** Methods ***/ 264 | 265 | private void setSignal(Signal signal) { 266 | setTitle(signal == null ? name : signal.getName() + " - " + name); 267 | waveform.setSignal(signal); 268 | spectrogram.setSignal(signal); 269 | updateStatus(); 270 | } 271 | 272 | public boolean isPlaying() { 273 | return playing; 274 | } 275 | 276 | private void setBusy(boolean busy) { 277 | this.busy = busy; 278 | } 279 | 280 | private void updateStatus() { 281 | String status = ""; 282 | Signal signal = spectrogram.getSignal(); 283 | if (signal != null) { 284 | int samples = signal.getNumSamples(); 285 | int hertz = signal.getHertz(); 286 | int zoom = spectrogram.getZoom(); 287 | int bins = spectrogram.getBins(); 288 | double overlap = spectrogram.getOverlap(); 289 | int zoomFactor = (int)spectrogram.getSpectrumWidth(); 290 | WindowFunction window = spectrogram.getWindow(); 291 | status = String.format("%s (%s, %s Hz) | %s zoom | " + 292 | "%s-bin %s%s spectra (%s overlap, %s window)", 293 | FormatUtils.formatTime(samples / hertz), 294 | FormatUtils.formatCount(samples, "sample"), 295 | FormatUtils.formatInt(hertz), 296 | FormatUtils.formatFactor(zoom, zoomFactor), 297 | FormatUtils.formatInt(bins), 298 | logAxis.isSelected() ? "logarithmic" : "linear", 299 | showPhase.isSelected() ? " phase" : "", 300 | FormatUtils.formatPercent(1 - overlap), 301 | window.getName() 302 | ); 303 | } 304 | statusBar.setText(status); 305 | } 306 | 307 | public synchronized void updateCursor(long microseconds) { 308 | if (waveform == null || spectrogram == null) 309 | return; 310 | waveform.updateCursor(microseconds); 311 | spectrogram.updateCursor(microseconds); 312 | } 313 | 314 | /*** Actions ***/ 315 | 316 | public void actionPerformed(ActionEvent event) { 317 | if (busy) 318 | return; 319 | Object source = event.getSource(); 320 | if (source == open) 321 | open(); 322 | else if (source == close) 323 | close(); 324 | else if (source == save) 325 | save(); 326 | else if (source == convert) 327 | convert(); 328 | else if (source == exit) 329 | exit(); 330 | else if (source == zoomIn) 331 | zoomIn(); 332 | else if (source == zoomOut) 333 | zoomOut(); 334 | else if (source == zoomMax) 335 | zoomMax(); 336 | else if (source == zoomFit) 337 | zoomFit(); 338 | else if (binsItems.containsKey(source)) 339 | setBins(binsItems.get(source)); 340 | else if (overlapItems.containsKey(source)) 341 | setOverlap(overlapItems.get(source)); 342 | else if (windowItems.containsKey(source)) 343 | setWindow(windowItems.get(source)); 344 | else if (source == play) 345 | play(); 346 | else if (source == stop) 347 | stop(); 348 | else if (source == garbageCollect) 349 | garbageCollect(); 350 | else if (source == help) 351 | help(); 352 | else if (source == about) 353 | about(); 354 | } 355 | 356 | public void itemStateChanged(ItemEvent event) { 357 | if (busy) 358 | return; 359 | Object item = event.getItem(); 360 | if (item == logAxis) 361 | logAxis(); 362 | else if (item == fullHeight) 363 | fullHeight(); 364 | else if (item == showPhase) 365 | showPhase(); 366 | } 367 | 368 | private void open() { 369 | fc.setDialogTitle("Open audio"); 370 | fc.setDialogType(JFileChooser.OPEN_DIALOG); 371 | fc.setFileFilter(audioReadFilter); 372 | if (fc.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) 373 | return; 374 | final File file = fc.getSelectedFile(); 375 | final String name = file.getName(); 376 | final boolean useMemory = audioMemory.isSelected(); 377 | SwingWorker worker = new SwingWorker() { 378 | public Signal doInBackground() { 379 | try { 380 | return Signal.fromFile(SignalWindow.this, file, useMemory); 381 | } 382 | catch (InterruptedIOException ex) {} 383 | catch (Exception ex) { 384 | JOptionPane.showMessageDialog(SignalWindow.this, 385 | "Could not open " + name + "!", 386 | "Error", JOptionPane.ERROR_MESSAGE); 387 | } 388 | return null; 389 | } 390 | 391 | public void done() { 392 | Signal signal = null; 393 | try { 394 | signal = get(); 395 | } 396 | catch (InterruptedException ex) {} 397 | catch (ExecutionException ex) { 398 | JOptionPane.showMessageDialog(SignalWindow.this, 399 | "Could not open " + name + "!", 400 | "Error", JOptionPane.ERROR_MESSAGE); 401 | } 402 | if (signal != null) 403 | SignalWindow.this.setSignal(signal); 404 | SignalWindow.this.setBusy(false); 405 | } 406 | }; 407 | setBusy(true); 408 | worker.execute(); 409 | } 410 | 411 | private void save() { 412 | if (spectrogram.getSignal() == null) 413 | return; 414 | fc.setDialogTitle("Save image"); 415 | fc.setDialogType(JFileChooser.SAVE_DIALOG); 416 | fc.setFileFilter(imageFilter); 417 | if (fc.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) 418 | return; 419 | final File file = fc.getSelectedFile(); 420 | final String name = file.getName(); 421 | SwingWorker worker = new SwingWorker() { 422 | public Void doInBackground() { 423 | try { 424 | spectrogram.save(SignalWindow.this, file, IMAGE_EXTENSIONS[0]); 425 | JOptionPane.showMessageDialog(SignalWindow.this, 426 | "Saved " + name + "!", 427 | "Success", JOptionPane.PLAIN_MESSAGE); 428 | } 429 | catch (IOException ex) { 430 | JOptionPane.showMessageDialog(SignalWindow.this, 431 | "Could not save " + name + "!", 432 | "Error", JOptionPane.ERROR_MESSAGE); 433 | } 434 | return null; 435 | } 436 | 437 | public void done() { 438 | try { 439 | get(); 440 | } 441 | catch (InterruptedException ex) {} 442 | catch (ExecutionException ex) { 443 | JOptionPane.showMessageDialog(SignalWindow.this, 444 | "Could not save " + name + "!", 445 | "Error", JOptionPane.ERROR_MESSAGE); 446 | } 447 | SignalWindow.this.setBusy(false); 448 | } 449 | }; 450 | setBusy(true); 451 | worker.execute(); 452 | } 453 | 454 | private void convert() { 455 | // get image 456 | fc.setDialogTitle("Open spectrogram image"); 457 | fc.setDialogType(JFileChooser.OPEN_DIALOG); 458 | fc.setFileFilter(imageFilter); 459 | if (fc.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) 460 | return; 461 | File imageFile = fc.getSelectedFile(); 462 | final String imageName = imageFile.getName(); 463 | // read image 464 | final BufferedImage image; 465 | try { 466 | image = ImageIO.read(imageFile); 467 | } 468 | catch (IOException ex) { 469 | JOptionPane.showMessageDialog(this, 470 | "Could not open " + imageName + "!", 471 | "Error", JOptionPane.ERROR_MESSAGE); 472 | return; 473 | } 474 | // get hertz 475 | int hertz = 0; 476 | do { 477 | Object input = JOptionPane.showInputDialog(this, "Enter sample rate in Hz:", 478 | "Sample rate", JOptionPane.QUESTION_MESSAGE, null, null, "44100"); 479 | hertz = Integer.parseInt((String)input); 480 | if (hertz <= 0) { 481 | JOptionPane.showMessageDialog(this, "Sample rate must be positive!", 482 | "Error", JOptionPane.ERROR_MESSAGE); 483 | } 484 | } while (hertz <= 0); 485 | // get overlap 486 | double overlap = 0.5; 487 | Double overlapValues[] = new Double[overlapItems.size()]; 488 | int i = 0; 489 | for (Double value : overlapItems.values()) { 490 | overlapValues[i++] = value; 491 | } 492 | Arrays.sort(overlapValues, Collections.reverseOrder()); 493 | String overlapOptions[] = new String[overlapValues.length]; 494 | for (i = 0; i < overlapValues.length; i++) { 495 | overlapOptions[i] = FormatUtils.formatPercent(1 - overlapValues[i]); 496 | } 497 | Object input = JOptionPane.showInputDialog(this, "Select spectrum overlap:", 498 | "Spectrum overlap", JOptionPane.QUESTION_MESSAGE, null, 499 | overlapOptions, overlapOptions[2]); 500 | for (i = 0; i < overlapOptions.length; i++) { 501 | if (overlapOptions[i].equals(input)) { 502 | overlap = overlapValues[i]; 503 | break; 504 | } 505 | } 506 | // save audio 507 | fc.setDialogTitle("Save audio"); 508 | fc.setDialogType(JFileChooser.SAVE_DIALOG); 509 | fc.setFileFilter(audioWriteFilter); 510 | if (fc.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) 511 | return; 512 | final File audioFile = fc.getSelectedFile(); 513 | final String audioName = audioFile.getName(); 514 | final int hz = hertz; 515 | final double lap = overlap; 516 | SwingWorker worker = new SwingWorker() { 517 | public Void doInBackground() { 518 | try { 519 | Spectrogram.imageToAudio(SignalWindow.this, image, audioFile, 520 | hz, lap); 521 | JOptionPane.showMessageDialog(SignalWindow.this, 522 | "Converted " + imageName + " to " + audioName + "!", 523 | "Success", JOptionPane.PLAIN_MESSAGE); 524 | } 525 | catch (IOException ex) { 526 | JOptionPane.showMessageDialog(SignalWindow.this, 527 | "Could not convert " + imageName + "!", 528 | "Error", JOptionPane.ERROR_MESSAGE); 529 | } 530 | return null; 531 | } 532 | 533 | public void done() { 534 | try { 535 | get(); 536 | } 537 | catch (InterruptedException ex) {} 538 | catch (ExecutionException ex) { 539 | JOptionPane.showMessageDialog(SignalWindow.this, 540 | "Could not convert " + imageName + "!", 541 | "Error", JOptionPane.ERROR_MESSAGE); 542 | } 543 | SignalWindow.this.setBusy(false); 544 | } 545 | }; 546 | setBusy(true); 547 | worker.execute(); 548 | } 549 | 550 | private void close() { 551 | stop(); 552 | setSignal(null); 553 | } 554 | 555 | private void exit() { 556 | close(); 557 | setVisible(false); 558 | dispose(); 559 | } 560 | 561 | private void zoomIn() { 562 | if (waveform.getSignal() == null || spectrogram.getSignal() == null) 563 | return; 564 | waveform.zoomIn(); 565 | spectrogram.zoomIn(); 566 | updateStatus(); 567 | } 568 | 569 | private void zoomOut() { 570 | if (waveform.getSignal() == null || spectrogram.getSignal() == null) 571 | return; 572 | waveform.zoomOut(); 573 | spectrogram.zoomOut(); 574 | updateStatus(); 575 | } 576 | 577 | private void zoomMax() { 578 | if (waveform.getSignal() == null || spectrogram.getSignal() == null) 579 | return; 580 | waveform.zoomTo(1); 581 | spectrogram.zoomTo(1); 582 | updateStatus(); 583 | } 584 | 585 | private void zoomFit() { 586 | if (waveform.getSignal() == null || spectrogram.getSignal() == null) 587 | return; 588 | int zoom = (int)(spectrogram.getSpectrumWidth()); 589 | waveform.zoomTo(zoom); 590 | spectrogram.zoomTo(zoom); 591 | updateStatus(); 592 | } 593 | 594 | private void setBins(int bins) { 595 | spectrogram.setBins(bins); 596 | updateStatus(); 597 | } 598 | 599 | private void setOverlap(double overlap) { 600 | spectrogram.setOverlap(overlap); 601 | updateStatus(); 602 | } 603 | 604 | private void setWindow(WindowFunction window) { 605 | spectrogram.setWindow(window); 606 | updateStatus(); 607 | } 608 | 609 | private void logAxis() { 610 | spectrogram.setLogAxis(logAxis.isSelected()); 611 | updateStatus(); 612 | } 613 | 614 | private void fullHeight() { 615 | spectrogram.setFullHeight(fullHeight.isSelected()); 616 | } 617 | 618 | private void showPhase() { 619 | spectrogram.showPhase(showPhase.isSelected()); 620 | updateStatus(); 621 | } 622 | 623 | private synchronized void play() { 624 | Signal signal = spectrogram.getSignal(); 625 | if (playing || signal == null) 626 | return; 627 | playing = true; 628 | try { 629 | new Player(this, signal).start(); 630 | } 631 | catch (IllegalArgumentException ex) { 632 | JOptionPane.showMessageDialog(this, "Error: " + ex.getMessage(), 633 | "Error", JOptionPane.ERROR_MESSAGE); 634 | playing = false; 635 | } 636 | } 637 | 638 | public synchronized void stop() { 639 | playing = false; 640 | } 641 | 642 | private void garbageCollect() { 643 | System.gc(); 644 | } 645 | 646 | private void help() { 647 | String message = "" + 648 | "Help is not yet available.
" + 649 | "We apologize for the inconvenience." + 650 | ""; 651 | JOptionPane.showMessageDialog(this, message, "Help", 652 | JOptionPane.PLAIN_MESSAGE); 653 | } 654 | 655 | private void about() { 656 | String message = "" + 657 | "" + name + "

" + 658 | "Copyright © 2012 Remy Oukaour

" + 659 | "This program uses the JTransforms, MP3SPI,
" + 660 | "VorbisSPI, and jFLAC libraries." + 661 | ""; 662 | JOptionPane.showMessageDialog(this, message, "About", 663 | JOptionPane.PLAIN_MESSAGE); 664 | } 665 | } 666 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/Spectrogram.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.awt.*; 4 | import java.awt.image.BufferedImage; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import javax.imageio.ImageIO; 8 | import javax.sound.sampled.*; 9 | import edu.emory.mathcs.jtransforms.fft.DoubleFFT_1D; 10 | 11 | public class Spectrogram extends SignalPanel { 12 | private static final long serialVersionUID = -5442088111430822270L; 13 | 14 | private static final double TAU = Math.PI * 2; 15 | private static final int MAX_COLOR = 0xFF; 16 | 17 | private boolean logAxis, showPhase; 18 | private int bins, lbins[]; 19 | private double overlap, maxPower, step, cf; 20 | private WindowFunction window; 21 | 22 | private static double getPower(int rgb, double maxPower) { 23 | double step = Math.log1p(maxPower) / 4; 24 | double cf = MAX_COLOR / step; 25 | int r = (rgb >> 16) & 0xFF; 26 | int g = (rgb >> 8) & 0xFF; 27 | int b = rgb & 0xFF; 28 | double k = 0.0; 29 | if (r > 0 && g > 0 && b > 0) 30 | k = g / cf + step * 3; 31 | else if (r > 0) 32 | k = r / cf + step * 2; 33 | else if (g > 0) 34 | k = g / cf + step; 35 | else 36 | k = b / cf; 37 | return Math.expm1(k); 38 | } 39 | 40 | public static void imageToAudio(Component parent, BufferedImage image, 41 | File f, int hertz, double overlap) throws IOException { 42 | String name = f.getName(); 43 | int cols = image.getWidth(); 44 | int bins = image.getHeight(); 45 | IndeterminateProgressMonitor monitor = new IndeterminateProgressMonitor(parent, 46 | "Converting image to " + name + "...", null, 0, cols); 47 | monitor.setMillisToDecideToPopup(250); 48 | monitor.setMillisToPopup(250); 49 | double maxPower = bins * bins / 4.0; 50 | //double freqF = (double)hertz / bins / 2; // unused 51 | int sw = (int)(bins * 2 * overlap); 52 | double[] samples = new double[sw * cols]; 53 | double[] col = new double[bins * 2]; 54 | DoubleFFT_1D fft = new DoubleFFT_1D(col.length); 55 | int index = 0; 56 | for (int c = 0; c < cols; c++) { 57 | monitor.setProgress(c); 58 | for (int r = 0; r < bins; r++) { 59 | if (monitor.isCanceled()) 60 | throw new IOException("failed to convert sample"); 61 | int rgb = image.getRGB(c, bins - r - 1); 62 | double power = getPower(rgb, maxPower); 63 | double amplitude = Math.sqrt(power); 64 | //double frequency = r * freqF; // unused 65 | double phase = Math.random() * TAU - Math.PI; 66 | col[2*r] = amplitude * Math.cos(phase); 67 | col[2*r+1] = amplitude * Math.sin(phase); 68 | } 69 | fft.realInverse(col, true); 70 | int start = 0; // keep beginning samples 71 | int end = start + sw; 72 | for (int s = start; s < end; s++) { 73 | samples[index++] = col[s] * 2; 74 | } 75 | } 76 | monitor.makeIndeterminate(); 77 | SampledMemoryData data = new SampledMemoryData(samples); 78 | AudioInputStream stream = data.toStream(hertz); 79 | AudioSystem.write(stream, AudioFileFormat.Type.WAVE, f); 80 | monitor.close(); 81 | } 82 | 83 | public Spectrogram(int bins, double overlap, WindowFunction window, 84 | boolean logAxis, boolean fullHeight, boolean showPhase) { 85 | super(); 86 | this.bins = bins; 87 | this.overlap = overlap; 88 | this.window = window; 89 | this.logAxis = logAxis; 90 | this.showPhase = showPhase; 91 | setFullHeight(fullHeight); 92 | recalculateBinsCache(); 93 | } 94 | 95 | private void recalculateBinsCache() { 96 | this.maxPower = bins * bins / 4.0; 97 | this.step = Math.log1p(maxPower) / 4; 98 | this.cf = MAX_COLOR / step; 99 | this.lbins = new int[bins]; 100 | double bf = (bins - 1) / Math.log(bins); 101 | for (int i = 0; i < bins; i++) { 102 | lbins[i] = (int)(Math.log1p(i) * bf); 103 | } 104 | } 105 | 106 | public int getBins() { 107 | return bins; 108 | } 109 | 110 | public void setBins(int bins) { 111 | this.bins = bins; 112 | recalculateBinsCache(); 113 | repaint(); 114 | } 115 | 116 | public double getOverlap() { 117 | return overlap; 118 | } 119 | 120 | public void setOverlap(double overlap) { 121 | this.overlap = overlap; 122 | repaint(); 123 | } 124 | 125 | public WindowFunction getWindow() { 126 | return window; 127 | } 128 | 129 | public void setWindow(WindowFunction window) { 130 | this.window = window; 131 | repaint(); 132 | } 133 | 134 | public void setLogAxis(boolean log) { 135 | this.logAxis = log; 136 | repaint(); 137 | } 138 | 139 | public void setFullHeight(boolean fullHeight) { 140 | setPrefHeight(fullHeight ? bins : 0); 141 | } 142 | 143 | public void showPhase(boolean showPhase) { 144 | this.showPhase = showPhase; 145 | repaint(); 146 | } 147 | 148 | public double getSpectrumWidth() { 149 | return bins * 2 * overlap; 150 | } 151 | 152 | private Color powerColor(double power) { 153 | double k = Math.log1p(power); 154 | int r = 0, g = 0, b = 0; 155 | if (k < step) { 156 | // black to blue 157 | b = (int)(k * cf); 158 | } 159 | else if (k < step * 2) { 160 | // blue to green 161 | k -= step; 162 | g = (int)(k * cf); 163 | b = MAX_COLOR - g; 164 | } 165 | else if (k < step * 3) { 166 | // green to red 167 | k -= step * 2; 168 | r = (int)(k * cf); 169 | g = MAX_COLOR - r; 170 | } 171 | else { 172 | // red to white 173 | k -= step * 3; 174 | r = MAX_COLOR; 175 | g = (int)(k * cf); 176 | b = g; 177 | } 178 | if (r > MAX_COLOR) r = MAX_COLOR; 179 | if (g > MAX_COLOR) g = MAX_COLOR; 180 | if (b > MAX_COLOR) b = MAX_COLOR; 181 | return new Color(r, g, b); 182 | } 183 | 184 | private Color phaseColor(double phase) { 185 | int c = (int)(phase * MAX_COLOR); 186 | if (c < 0) c = 0; 187 | if (c > MAX_COLOR) c = MAX_COLOR; 188 | return new Color(c, c, c); 189 | } 190 | 191 | public void paintComponent(Graphics g) { 192 | super.paintComponent(g); 193 | if (signal == null) 194 | return; 195 | Color oldColor = g.getColor(); 196 | double xf = getSpectrumWidth(); 197 | double yf = (double)getHeight() / (bins - 1); 198 | int sw = (int)(xf / zoom) + 1; 199 | int sh = (int)Math.ceil(yf); 200 | int inc = (int)xf; 201 | Rectangle bounds = g.getClipBounds(); 202 | int minX = (int)bounds.getX() - sw; 203 | int maxX = (int)(bounds.getX() + bounds.getWidth()); 204 | int n = signal.getNumSamples(); 205 | int limX = n / zoom - sw; 206 | n += inc; // overshoot 207 | for (int i = 0; i < n; i += inc) { 208 | int x = (int)((i - xf / 2) / zoom); 209 | if (x < minX || x > maxX) 210 | continue; 211 | Spectrum spectrum = signal.getSpectrum(i, bins, window, showPhase); 212 | if (x > limX) { 213 | sw -= x - limX; 214 | i = n; 215 | } 216 | for (int j = 0; j < bins; j++) { 217 | int y = (int)(j * yf); 218 | int s = bins - (logAxis ? lbins[j] : j) - 1; 219 | double v = spectrum.get(s); 220 | g.setColor(showPhase ? phaseColor(v) : powerColor(v)); 221 | g.fillRect(x, y, sw, sh); 222 | } 223 | } 224 | drawCursor(g); 225 | g.setColor(oldColor); 226 | } 227 | 228 | public void save(Component parent, File f, String ext) throws IOException { 229 | if (signal == null) 230 | throw new IOException("no signal to save"); 231 | String name = f.getName(); 232 | int n = signal.getNumSamples(); 233 | int sw = (int)getSpectrumWidth(); 234 | int width = n / sw + 1; 235 | IndeterminateProgressMonitor monitor = new IndeterminateProgressMonitor(parent, 236 | "Saving " + name + "...", null, 0, width); 237 | monitor.setMillisToDecideToPopup(250); 238 | monitor.setMillisToPopup(250); 239 | BufferedImage image = new BufferedImage(width, bins, 240 | BufferedImage.TYPE_INT_RGB); 241 | for (int x = 0; x < width; x++) { 242 | monitor.setProgress(x); 243 | Spectrum spectrum = signal.getSpectrum(x * sw, bins, window, showPhase); 244 | for (int y = 0; y < bins; y++) { 245 | if (monitor.isCanceled()) 246 | throw new IOException("failed to get pixel"); 247 | int s = bins - (logAxis ? lbins[y] : y) - 1; 248 | double v = spectrum.get(s); 249 | Color c = showPhase ? phaseColor(v) : powerColor(v); 250 | image.setRGB(x, y, c.getRGB()); 251 | } 252 | } 253 | monitor.makeIndeterminate(); 254 | ImageIO.write(image, ext, f); 255 | monitor.close(); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/SpectrogramMain.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import javax.swing.UIManager; 4 | 5 | public class SpectrogramMain { 6 | private static final String PROGRAM_TITLE = "Spectrogram"; 7 | private static final int PROGRAM_WIDTH = 720; 8 | private static final int PROGRAM_HEIGHT = 405; 9 | 10 | public static void main(String[] args) { 11 | try { 12 | UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 13 | } 14 | catch (Exception ex) {} 15 | SignalWindow gui = new SignalWindow(PROGRAM_TITLE); 16 | gui.setSize(PROGRAM_WIDTH, PROGRAM_HEIGHT); 17 | gui.setVisible(true); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/Spectrum.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import edu.emory.mathcs.jtransforms.fft.DoubleFFT_1D; 4 | 5 | public class Spectrum { 6 | private static int cachedLength = -1; 7 | private static DoubleFFT_1D fft = null; 8 | 9 | private final double[] spectrum; 10 | private final int time; 11 | 12 | public Spectrum(double[] samples, int time) { 13 | this(samples, time, null, false); 14 | } 15 | 16 | public Spectrum(double[] samples, int time, WindowFunction window, 17 | boolean showPhase) { 18 | if (window != null) 19 | window.window(samples); 20 | int n = samples.length; 21 | if (n != cachedLength) { 22 | cachedLength = n; 23 | fft = new DoubleFFT_1D(n); 24 | } 25 | fft.realForward(samples); 26 | n /= 2; 27 | spectrum = new double[n]; 28 | for (int i = 0; i < n; i++) { 29 | double re = samples[2*i], im = samples[2*i+1]; 30 | spectrum[i] = showPhase ? (Math.atan2(im, re) + Math.PI) / 2 : 31 | re * re + im * im; 32 | } 33 | this.time = time; 34 | } 35 | 36 | public double get(int i) { 37 | return spectrum[i]; 38 | } 39 | 40 | public int getTime() { 41 | return time; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/StatusBar.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.awt.*; 4 | import javax.swing.*; 5 | 6 | public class StatusBar extends JPanel { 7 | private static final long serialVersionUID = 3594146783477354882L; 8 | 9 | private final JLabel status = new JLabel(); 10 | 11 | public StatusBar() { 12 | this(""); 13 | } 14 | 15 | public StatusBar(String text) { 16 | setLayout(new BorderLayout()); 17 | add(status, BorderLayout.CENTER); 18 | setPreferredSize(new Dimension(0, 20)); 19 | setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Color.LIGHT_GRAY)); 20 | status.setBorder(BorderFactory.createEmptyBorder(1, 4, 1, 4)); 21 | setText(text); 22 | } 23 | 24 | public String getText() { 25 | return status.getText(); 26 | } 27 | 28 | public void setText(String text) { 29 | status.setText(text); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/Waveform.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | import java.awt.*; 4 | 5 | public class Waveform extends SignalPanel { 6 | public static final long serialVersionUID = 1220411153L; 7 | 8 | private static final int BUFFERED_SAMPLES = 1024; 9 | 10 | public Waveform() { 11 | super(); 12 | } 13 | 14 | public void paintComponent(Graphics g) { 15 | super.paintComponent(g); 16 | if (signal == null) 17 | return; 18 | Color oldColor = g.getColor(); 19 | g.setColor(Color.BLUE); 20 | int hh = getHeight() / 2; 21 | Rectangle bounds = g.getClipBounds(); 22 | int minX = (int)bounds.getX() - 1; 23 | int maxX = (int)(bounds.getX() + bounds.getWidth()); 24 | int pp = hh; 25 | int n = signal.getNumSamples(); 26 | drawing: 27 | for (int i = 0; i < n; i += BUFFERED_SAMPLES) { 28 | double[] data = signal.getSamples(i, BUFFERED_SAMPLES); 29 | for (int j = 0; j < BUFFERED_SAMPLES; j += zoom) { 30 | int x = (i + j) / zoom; 31 | if (x < minX || x > maxX) 32 | continue; 33 | for (int k = 0; k < zoom; k++) { 34 | if (i + j + k >= n) 35 | break drawing; 36 | if (j + k >= BUFFERED_SAMPLES) 37 | break; 38 | double v = data[j + k]; 39 | int p = (int)(v * hh); 40 | g.drawLine(x, hh + pp, x, hh + p); 41 | pp = p; 42 | } 43 | } 44 | } 45 | drawCursor(g); 46 | g.setColor(oldColor); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/com/remyoukaour/spectrogram/WindowFunction.java: -------------------------------------------------------------------------------- 1 | package com.remyoukaour.spectrogram; 2 | 3 | public enum WindowFunction { 4 | RECTANGULAR("Rectangular (Dirichlet)") { 5 | public void window(double[] data) { 6 | // do nothing 7 | } 8 | }, 9 | 10 | TRIANGULAR("Triangular (Bartlett)") { 11 | public void window(double[] data) { 12 | int n = data.length; 13 | double a = (n - 1) / 2.0; 14 | double b = 2.0 / (n - 1); 15 | for (int i = 0; i < n; i++) { 16 | data[i] *= b * (a - Math.abs(i - a)); 17 | } 18 | } 19 | }, 20 | 21 | COSINE("Cosine (sine)") { 22 | public void window(double[] data) { 23 | int n = data.length; 24 | double a = Math.PI / (n - 1); 25 | for (int i = 0; i < n; i++) { 26 | data[i] *= Math.sin(a * i); 27 | } 28 | } 29 | }, 30 | 31 | GAUSSIAN("Gaussian") { 32 | public void window(double[] data) { 33 | int n = data.length; 34 | double a = (n - 1) / 2.0; 35 | double b = a * 0.4; 36 | for (int i = 0; i < n; i++) { 37 | data[i] *= Math.exp(-0.5 * Math.pow((i - a) / b, 2)); 38 | } 39 | } 40 | }, 41 | 42 | LANCZOS("Lanczos (sinc)") { 43 | public void window(double[] data) { 44 | int n = data.length; 45 | double a = TAU / (n - 1); 46 | for (int i = 1; i < n; i++) { 47 | double b = i * a; 48 | data[i] *= Math.sin(b) / b; 49 | } 50 | } 51 | }, 52 | 53 | WELCH("Welch") { 54 | public void window(double[] data) { 55 | int n = data.length; 56 | double a = n / 2.0; 57 | for (int i = 0; i < n; i++) { 58 | data[i] *= 1 - Math.pow((i - a) / a, 2); 59 | } 60 | } 61 | }, 62 | 63 | HANN("Hann") { 64 | public void window(double[] data) { 65 | int n = data.length; 66 | double a = TAU / (n - 1); 67 | for (int i = 0; i < n; i++) { 68 | data[i] *= 0.5 - 0.5 * Math.cos(i * a); 69 | } 70 | } 71 | }, 72 | 73 | HAMMING("Hamming") { 74 | public void window(double[] data) { 75 | // http://cnx.org/content/m0505/latest/ 76 | int n = data.length; 77 | double a = TAU / (n - 1); 78 | for (int i = 0; i < n; i++) { 79 | data[i] *= 0.54 - 0.46 * Math.cos(i * a); 80 | } 81 | } 82 | }, 83 | 84 | BARTLETT_HANN("Bartlett-Hann") { 85 | public void window(double[] data) { 86 | int n = data.length; 87 | double a = n - 1.0; 88 | double b = TAU / (n - 1); 89 | for (int i = 0; i < n; i++) { 90 | data[i] *= 0.62 - 0.48 * Math.abs(i / a - 0.5) - 91 | 0.38 * Math.cos(i * b); 92 | } 93 | } 94 | }, 95 | 96 | BLACKMAN("Blackman") { 97 | public void window(double[] data) { 98 | int n = data.length; 99 | double a = TAU / (n - 1); 100 | double b = 2 * a; 101 | for (int i = 0; i < n; i++) { 102 | data[i] *= 0.42 - 0.5 * Math.cos(i * a) + 0.08 * Math.cos(i * b); 103 | } 104 | } 105 | }, 106 | 107 | NUTTALL("Nuttall") { 108 | public void window(double[] data) { 109 | int n = data.length; 110 | double a = TAU / (n - 1); 111 | double b = 2 * a; 112 | double c = 3 * a; 113 | for (int i = 0; i < n; i++) { 114 | data[i] *= 0.355768 - 0.487396 * Math.cos(i * a) + 115 | 0.144232 * Math.cos(i * b) - 0.012604 * Math.cos(i * c); 116 | } 117 | } 118 | }, 119 | 120 | BLACKMAN_HARRIS("Blackman-Harris") { 121 | public void window(double[] data) { 122 | int n = data.length; 123 | double a = TAU / (n - 1); 124 | double b = 2 * a; 125 | double c = 3 * a; 126 | for (int i = 0; i < n; i++) { 127 | data[i] *= 0.35875 - 0.48829 * Math.cos(i * a) + 128 | 0.14128 * Math.cos(i * b) - 0.01168 * Math.cos(i * c); 129 | } 130 | } 131 | }, 132 | 133 | BLACKMAN_NUTTALL("Blackman-Nuttall") { 134 | public void window(double[] data) { 135 | int n = data.length; 136 | double a = TAU / (n - 1); 137 | double b = 2 * a; 138 | double c = 3 * a; 139 | for (int i = 0; i < n; i++) { 140 | data[i] *= 0.3635819 - 0.4891775 * Math.cos(i * a) + 141 | 0.1365995 * Math.cos(i * b) - 0.0106411 * Math.cos(i * c); 142 | } 143 | } 144 | }, 145 | 146 | FLAT_TOP("Flat top") { 147 | public void window(double[] data) { 148 | int n = data.length; 149 | double a = TAU / (n - 1); 150 | double b = 2 * a; 151 | double c = 3 * a; 152 | double d = 4 * a; 153 | for (int i = 0; i < n; i++) { 154 | data[i] *= 0.21557895 - 0.41663158 * Math.cos(i * a) + 155 | 0.277263158 * Math.cos(i * b) - 156 | 0.083578947 * Math.cos(i * c) + 157 | 0.006947368 * Math.cos(i * d); 158 | } 159 | } 160 | }; 161 | 162 | private static final double TAU = Math.PI * 2; 163 | 164 | private final String name; 165 | 166 | private WindowFunction(String name) { 167 | this.name = name; 168 | } 169 | 170 | public String getName() { 171 | return name; 172 | } 173 | 174 | public abstract void window(double[] data); 175 | } 176 | --------------------------------------------------------------------------------