diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/normalize.png b/resources/uk/co/majenko/audiobookrecorder/icons/normalize.png new file mode 100644 index 0000000..04cb800 Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/normalize.png differ diff --git a/src/uk/co/majenko/audiobookrecorder/AudiobookRecorder.java b/src/uk/co/majenko/audiobookrecorder/AudiobookRecorder.java index 5896564..c0e6ec6 100644 --- a/src/uk/co/majenko/audiobookrecorder/AudiobookRecorder.java +++ b/src/uk/co/majenko/audiobookrecorder/AudiobookRecorder.java @@ -75,12 +75,14 @@ public class AudiobookRecorder extends JFrame { JScrollBar sampleScroll; JSpinner postSentenceGap; + JSpinner gainPercent; JCheckBox locked; JCheckBox attention; JCheckBox ethereal; JButtonSpacePlay reprocessAudioFFT; JButtonSpacePlay reprocessAudioPeak; + JButtonSpacePlay normalizeAudio; Thread playingThread = null; @@ -363,6 +365,7 @@ public class AudiobookRecorder extends JFrame { sampleWaveform.setMarkers(selectedSentence.getStartOffset(), selectedSentence.getEndOffset()); sampleWaveform.setAltMarkers(selectedSentence.getStartCrossing(), selectedSentence.getEndCrossing()); postSentenceGap.setValue(selectedSentence.getPostGap()); + gainPercent.setValue((int)(selectedSentence.getGain() * 100d)); } } }); @@ -375,12 +378,23 @@ public class AudiobookRecorder extends JFrame { sampleWaveform.setMarkers(selectedSentence.getStartOffset(), selectedSentence.getEndOffset()); sampleWaveform.setAltMarkers(selectedSentence.getStartCrossing(), selectedSentence.getEndCrossing()); postSentenceGap.setValue(selectedSentence.getPostGap()); + gainPercent.setValue((int)(selectedSentence.getGain() * 100d)); } } }); + normalizeAudio = new JButtonSpacePlay(Icons.normalize, "Normalize audio", new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (selectedSentence != null) { + selectedSentence.normalize(); + sampleWaveform.setData(selectedSentence.getAudioData()); + } + } + }); + + postSentenceGap = new JSpinner(new SteppedNumericSpinnerModel(0, 5000, 100, 0)); - postSentenceGap.setPreferredSize(new Dimension(75, 20)); + postSentenceGap.setPreferredSize(new Dimension(50, 20)); postSentenceGap.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { @@ -391,6 +405,18 @@ public class AudiobookRecorder extends JFrame { } }); + gainPercent = new JSpinner(new SteppedNumericSpinnerModel(0, 500, 1, 100)); + gainPercent.setPreferredSize(new Dimension(50, 20)); + + gainPercent.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + JSpinner ob = (JSpinner)e.getSource(); + if (selectedSentence != null) { + selectedSentence.setGain((Integer)ob.getValue() / 100d); + sampleWaveform.setData(selectedSentence.getAudioData()); + } + } + }); JToolBar controlsTop = new JToolBar(JToolBar.HORIZONTAL); JToolBar controlsLeft = new JToolBar(JToolBar.VERTICAL); @@ -402,6 +428,7 @@ public class AudiobookRecorder extends JFrame { controlsLeft.add(reprocessAudioFFT); controlsLeft.add(reprocessAudioPeak); + controlsLeft.add(normalizeAudio); locked = new JCheckBox("Phrase locked"); locked.setFocusable(false); @@ -469,6 +496,11 @@ public class AudiobookRecorder extends JFrame { controlsTop.add(new JLabel("Post gap:")); controlsTop.add(postSentenceGap); controlsTop.add(new JLabel("ms")); + + controlsTop.add(new JLabel("Gain:")); + controlsTop.add(gainPercent); + controlsTop.add(new JLabel("%")); + controlsTop.add(Box.createHorizontalGlue()); @@ -1013,7 +1045,19 @@ public class AudiobookRecorder extends JFrame { } }); + JMenuObject normalizeAll = new JMenuObject("Normalize chapter", c, new ActionListener() { + public void actionPerformed(ActionEvent e) { + JMenuObject o = (JMenuObject)e.getSource(); + Chapter c = (Chapter)o.getObject(); + for (Enumeration s = c.children(); s.hasMoreElements();) { + Sentence snt = (Sentence)s.nextElement(); + snt.normalize(); + } + } + }); + menu.add(convertAll); + menu.add(normalizeAll); menu.addSeparator(); menu.add(moveUp); menu.add(moveDown); @@ -1392,6 +1436,7 @@ public class AudiobookRecorder extends JFrame { prefs.setProperty(String.format("%s.sentence.%08d.locked", keybase, i), snt.isLocked() ? "true" : "false"); prefs.setProperty(String.format("%s.sentence.%08d.attention", keybase, i), snt.getAttentionFlag() ? "true" : "false"); prefs.setProperty(String.format("%s.sentence.%08d.ethereal", keybase, i), snt.getEthereal() ? "true" : "false"); + prefs.setProperty(String.format("%s.sentence.%08d.gain", keybase, i), String.format("%.8f", snt.getGain())); i++; } } @@ -1499,6 +1544,7 @@ public class AudiobookRecorder extends JFrame { s.updateCrossings(); sampleWaveform.setAltMarkers(s.getStartCrossing(), s.getEndCrossing()); postSentenceGap.setValue(s.getPostGap()); + gainPercent.setValue((int)(s.getGain() * 100d)); locked.setSelected(s.isLocked()); attention.setSelected(s.getAttentionFlag()); ethereal.setSelected(s.getEthereal()); @@ -1510,6 +1556,7 @@ public class AudiobookRecorder extends JFrame { selectedSentence = null; sampleWaveform.clearData(); postSentenceGap.setValue(0); + gainPercent.setValue(100); locked.setSelected(false); attention.setSelected(false); ethereal.setSelected(false); @@ -1553,6 +1600,7 @@ public class AudiobookRecorder extends JFrame { s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.audition.sentence.%08d.locked", i)))); s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.audition.sentence.%08d.attention", i)))); s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.audition.sentence.%08d.ethereal", i)))); + s.setGain(Utils.s2d(prefs.getProperty(String.format("chapter.audition.sentence.%08d.gain", i)))); bookTreeModel.insertNodeInto(s, c, c.getChildCount()); } @@ -1573,6 +1621,7 @@ public class AudiobookRecorder extends JFrame { s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.open.sentence.%08d.locked", i)))); s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.open.sentence.%08d.attention", i)))); s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.open.sentence.%08d.ethereal", i)))); + s.setGain(Utils.s2d(prefs.getProperty(String.format("chapter.open.sentence.%08d.gain", i)))); bookTreeModel.insertNodeInto(s, c, c.getChildCount()); } @@ -1599,6 +1648,7 @@ public class AudiobookRecorder extends JFrame { s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.locked", cno, i)))); s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.attention", cno, i)))); s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.ethereal", cno, i)))); + s.setGain(Utils.s2d(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.gain", cno, i)))); bookTreeModel.insertNodeInto(s, c, c.getChildCount()); } } @@ -1620,6 +1670,7 @@ public class AudiobookRecorder extends JFrame { s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.close.sentence.%08d.locked", i)))); s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.close.sentence.%08d.attention", i)))); s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.close.sentence.%08d.ethereal", i)))); + s.setGain(Utils.s2d(prefs.getProperty(String.format("chapter.close.sentence.%08d.gain", i)))); bookTreeModel.insertNodeInto(s, c, c.getChildCount()); } @@ -1688,13 +1739,13 @@ public class AudiobookRecorder extends JFrame { } public int getNoiseFloorDB() { - int nf = getNoiseFloor(); - if (nf == 0) return 0; - double r = nf / 32767d; - double l10 = Math.log10(r); - double db = 20d * l10; + int nf = getNoiseFloor(); + if (nf == 0) return 0; + double r = nf / 32767d; + double l10 = Math.log10(r); + double db = 20d * l10; - return (int)db; + return (int)db; } public void recordRoomNoise() { diff --git a/src/uk/co/majenko/audiobookrecorder/Icons.java b/src/uk/co/majenko/audiobookrecorder/Icons.java index 038f6a8..62d9893 100644 --- a/src/uk/co/majenko/audiobookrecorder/Icons.java +++ b/src/uk/co/majenko/audiobookrecorder/Icons.java @@ -32,4 +32,5 @@ public class Icons { static public final ImageIcon zoomOut = new ImageIcon(Icons.class.getResource("icons/zoom-out.png")); static public final ImageIcon dollar = new ImageIcon(Icons.class.getResource("icons/dollar.png")); static public final ImageIcon attention = new ImageIcon(Icons.class.getResource("icons/attention.png")); + static public final ImageIcon normalize = new ImageIcon(Icons.class.getResource("icons/normalize.png")); } diff --git a/src/uk/co/majenko/audiobookrecorder/Sentence.java b/src/uk/co/majenko/audiobookrecorder/Sentence.java index 77f6250..d547c7b 100644 --- a/src/uk/co/majenko/audiobookrecorder/Sentence.java +++ b/src/uk/co/majenko/audiobookrecorder/Sentence.java @@ -47,6 +47,8 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable { boolean inSample; boolean attention = false; + double gain = 1.0d; + String havenJobId = ""; // 0: Not processed @@ -422,6 +424,10 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable { } public int[] getAudioDataS16LE(AudioInputStream s, AudioFormat format) throws IOException { + return getAudioDataS16LE(s, format, true); + } + + public int[] getAudioDataS16LE(AudioInputStream s, AudioFormat format, boolean amplify) throws IOException { long len = s.getFrameLength(); int frameSize = format.getFrameSize(); int chans = format.getChannels(); @@ -441,7 +447,12 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable { } else { sample = (frame[1] << 8) | frame[0]; } - samples[(int)fno] = sample; + if (amplify) { + double amped = (double)sample * gain; + samples[(int)fno] = (int)amped; + } else { + samples[(int)fno] = sample; + } } return samples; @@ -475,6 +486,28 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable { return null; } + public int[] getUnprocessedAudioData() { + File f = getFile(); + try { + AudioInputStream s = AudioSystem.getAudioInputStream(f); + AudioFormat format = getAudioFormat(); + + int[] samples = null; + + switch (format.getSampleSizeInBits()) { + case 16: + samples = getAudioDataS16LE(s, format, false); + break; + } + + s.close(); + return samples; + } catch (Exception e) { + } + return null; + } + + public int getStartCrossing() { return crossStartOffset; } @@ -606,67 +639,108 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable { return null; } - byte[] postProcessData(byte[] data) { - if (effectEthereal) { - AudioFormat format = getAudioFormat(); - int frameSize = format.getFrameSize(); - int channels = format.getChannels(); - int bytesPerChannel = frameSize / channels; + byte[] adjustGain(byte[] data) { + AudioFormat format = getAudioFormat(); + int frameSize = format.getFrameSize(); + int channels = format.getChannels(); + int bytesPerChannel = frameSize / channels; - int frames = data.length / frameSize; + int frames = data.length / frameSize; - int byteNo = 0; + int byteNo = 0; - double fpms = (double)format.getFrameRate() / 1000d; - double doubleOffset = fpms * (double) AudiobookRecorder.window.book.getInteger("effects.ethereal.offset"); - int offset = (int)doubleOffset; - double attenuation = 1d - ((double)AudiobookRecorder.window.book.getInteger("effects.ethereal.attenuation") / 100d); + byte[] out = new byte[data.length]; - int copies = AudiobookRecorder.window.book.getInteger("effects.ethereal.iterations"); + for (int i = 0; i < frames; i++) { + if (channels == 1) { + int l = data[i * frameSize] >= 0 ? data[i * frameSize] : 256 + data[i * frameSize]; + int h = data[(i * frameSize) + 1] >= 0 ? data[(i * frameSize) + 1] : 256 + data[(i * frameSize) + 1]; - byte[] out = new byte[data.length]; + int sample = (h << 8) | l; + if ((sample & 0x8000) == 0x8000) sample |= 0xFFFF0000; - for (int i = 0; i < frames; i++) { - if (channels == 1) { - int l = data[i * frameSize] >= 0 ? data[i * frameSize] : 256 + data[i * frameSize]; - int h = data[(i * frameSize) + 1] >= 0 ? data[(i * frameSize) + 1] : 256 + data[(i * frameSize) + 1]; - - int sample = (h << 8) | l; - if ((sample & 0x8000) == 0x8000) sample |= 0xFFFF0000; + double sampleDouble = (double)sample; + sampleDouble *= gain; + sample = (int)sampleDouble; - double sampleDouble = (double)sample; + if (sample > 32767) sample = 32767; + if (sample < -32768) sample = -32768; + out[i * frameSize] = (byte)(sample & 0xFF); + out[(i * frameSize) + 1] = (byte)((sample & 0xFF00) >> 8); - int used = 0; - for (int j = 0; j < copies; j++) { - if (i + (j * offset) < frames) { - used++; - int lx = data[(i + (j * offset)) * frameSize] >= 0 ? data[(i + (j * offset)) * frameSize] : 256 + data[(i + (j * offset)) * frameSize]; - int hx = data[((i + (j * offset)) * frameSize) + 1] >= 0 ? data[((i + (j * offset)) * frameSize) + 1] : 256 + data[((i + (j * offset)) * frameSize) + 1]; - int futureSample = (hx << 8) | lx; - if ((futureSample & 0x8000) == 0x8000) futureSample |= 0xFFFF0000; - double futureDouble = (double)futureSample; - for (int k = 0; k < copies; k++) { - futureDouble *= attenuation; - } - sampleDouble = mix(sampleDouble, futureDouble); - } - } - sample = (int)sampleDouble; - if (sample > 32767) sample = 32767; - if (sample < -32768) sample = -32768; - out[i * frameSize] = (byte)(sample & 0xFF); - out[(i * frameSize) + 1] = (byte)((sample & 0xFF00) >> 8); - - } else { - return data; - } + } else { + return data; } - - return out; - - } else { - return data; } + + return out; + } + + byte[] postProcessData(byte[] data) { + data = adjustGain(data); + + if (effectEthereal) { + data = processEtherealEffect(data); + } + return data; + } + + byte[] processEtherealEffect(byte[] data) { + AudioFormat format = getAudioFormat(); + int frameSize = format.getFrameSize(); + int channels = format.getChannels(); + int bytesPerChannel = frameSize / channels; + + int frames = data.length / frameSize; + + int byteNo = 0; + + double fpms = (double)format.getFrameRate() / 1000d; + double doubleOffset = fpms * (double) AudiobookRecorder.window.book.getInteger("effects.ethereal.offset"); + int offset = (int)doubleOffset; + double attenuation = 1d - ((double)AudiobookRecorder.window.book.getInteger("effects.ethereal.attenuation") / 100d); + + int copies = AudiobookRecorder.window.book.getInteger("effects.ethereal.iterations"); + + byte[] out = new byte[data.length]; + + for (int i = 0; i < frames; i++) { + if (channels == 1) { + int l = data[i * frameSize] >= 0 ? data[i * frameSize] : 256 + data[i * frameSize]; + int h = data[(i * frameSize) + 1] >= 0 ? data[(i * frameSize) + 1] : 256 + data[(i * frameSize) + 1]; + + int sample = (h << 8) | l; + if ((sample & 0x8000) == 0x8000) sample |= 0xFFFF0000; + + double sampleDouble = (double)sample; + + int used = 0; + for (int j = 0; j < copies; j++) { + if (i + (j * offset) < frames) { + used++; + int lx = data[(i + (j * offset)) * frameSize] >= 0 ? data[(i + (j * offset)) * frameSize] : 256 + data[(i + (j * offset)) * frameSize]; + int hx = data[((i + (j * offset)) * frameSize) + 1] >= 0 ? data[((i + (j * offset)) * frameSize) + 1] : 256 + data[((i + (j * offset)) * frameSize) + 1]; + int futureSample = (hx << 8) | lx; + if ((futureSample & 0x8000) == 0x8000) futureSample |= 0xFFFF0000; + double futureDouble = (double)futureSample; + for (int k = 0; k < copies; k++) { + futureDouble *= attenuation; + } + sampleDouble = mix(sampleDouble, futureDouble); + } + } + sample = (int)sampleDouble; + if (sample > 32767) sample = 32767; + if (sample < -32768) sample = -32768; + out[i * frameSize] = (byte)(sample & 0xFF); + out[(i * frameSize) + 1] = (byte)((sample & 0xFF00) >> 8); + + } else { + return data; + } + } + + return out; } public void recognise() { @@ -691,7 +765,7 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable { public void clearCache() { storedAudioData = null; - System.gc(); +// System.gc(); } public boolean lockedInCache() { @@ -926,4 +1000,44 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable { return z; } + public int getPeakValue() { + int[] samples = getUnprocessedAudioData(); + if (samples == null) { + return 0; + } + int ms = 0; + for (int i = 0; i < samples.length; i++) { + if (Math.abs(samples[i]) > ms) { + ms = Math.abs(samples[i]); + } + } + return ms; + } + + public int getHeadroom() { + int nf = getPeakValue(); + if (nf == 0) return 0; + double r = nf / 32767d; + double l10 = Math.log10(r); + double db = 20d * l10; + + return (int)db; + } + + public void setGain(double g) { + if (g <= 0.0001d) g = 1.0d; + if (g == gain) return; + gain = g; + clearCache(); + } + + public double getGain() { + return gain; + } + + public void normalize() { + int max = getPeakValue(); + setGain(23192d / max); + } + } diff --git a/src/uk/co/majenko/audiobookrecorder/Utils.java b/src/uk/co/majenko/audiobookrecorder/Utils.java index 758f9d6..825e004 100644 --- a/src/uk/co/majenko/audiobookrecorder/Utils.java +++ b/src/uk/co/majenko/audiobookrecorder/Utils.java @@ -47,6 +47,14 @@ public class Utils { return 0.0f; } + public static double s2d(String s) { + try { + return Double.parseDouble(s); + } catch (Exception e) { + } + return 0.0d; + } + public static void browse(String url) { if (Desktop.isDesktopSupported()) { try { diff --git a/src/uk/co/majenko/audiobookrecorder/Waveform.java b/src/uk/co/majenko/audiobookrecorder/Waveform.java index c91974b..54bc96b 100644 --- a/src/uk/co/majenko/audiobookrecorder/Waveform.java +++ b/src/uk/co/majenko/audiobookrecorder/Waveform.java @@ -105,12 +105,26 @@ public class Waveform extends JPanel implements MouseListener, MouseMotionListen if (hcnt > 0) have /= hcnt; if (lcnt > 0) lave /= lcnt; + boolean clip = false; + + if (lmax > 32000) { // -3dB == 23198? + clip = true; + } + + if (hmax > 32000) { // -3dB + clip = true; + } + hmax /= scale; lmax /= scale; have /= scale; lave /= scale; - g.setColor(new Color(0, 20, 200)); + if (clip) { + g.setColor(new Color(200, 20, 0)); + } else { + g.setColor(new Color(0, 20, 200)); + } g.drawLine(n, h/2 + lmax, n, h/2 - hmax); g.setColor(new Color(0, 100, 255)); g.drawLine(n, h/2 + (int)lave, n, h/2 - (int)have);