Added gain control and normalization, along with peak display
This commit is contained in:
BIN
resources/uk/co/majenko/audiobookrecorder/icons/normalize.png
Normal file
BIN
resources/uk/co/majenko/audiobookrecorder/icons/normalize.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 473 B |
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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,8 +447,13 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable {
|
||||
} else {
|
||||
sample = (frame[1] << 8) | frame[0];
|
||||
}
|
||||
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,8 +639,53 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 byteNo = 0;
|
||||
|
||||
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;
|
||||
sampleDouble *= gain;
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -663,10 +741,6 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable {
|
||||
}
|
||||
|
||||
return out;
|
||||
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user