diff --git a/src/uk/co/majenko/audiobookrecorder/AudiobookRecorder.java b/src/uk/co/majenko/audiobookrecorder/AudiobookRecorder.java index 76a050f..5896564 100644 --- a/src/uk/co/majenko/audiobookrecorder/AudiobookRecorder.java +++ b/src/uk/co/majenko/audiobookrecorder/AudiobookRecorder.java @@ -77,6 +77,7 @@ public class AudiobookRecorder extends JFrame { JSpinner postSentenceGap; JCheckBox locked; JCheckBox attention; + JCheckBox ethereal; JButtonSpacePlay reprocessAudioFFT; JButtonSpacePlay reprocessAudioPeak; @@ -443,6 +444,27 @@ public class AudiobookRecorder extends JFrame { controlsTop.add(attention); + ethereal = new JCheckBox("Ethereal voice"); + ethereal.setFocusable(false); + + ethereal.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + JCheckBox c = (JCheckBox)e.getSource(); + if (c.isSelected()) { + if (selectedSentence != null) { + selectedSentence.setEthereal(true); + } + } else { + if (selectedSentence != null) { + selectedSentence.setEthereal(false); + } + } + bookTreeModel.reload(selectedSentence); + } + }); + + controlsTop.add(ethereal); + controlsTop.add(Box.createHorizontalGlue()); controlsTop.add(new JLabel("Post gap:")); controlsTop.add(postSentenceGap); @@ -1015,6 +1037,7 @@ public class AudiobookRecorder extends JFrame { JMenuItem editData = new JMenuItem("Edit Data..."); editData.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + JTabbedPane tabs = new JTabbedPane(); BookInfoPanel info = new BookInfoPanel( book.getName(), book.getAuthor(), @@ -1022,7 +1045,38 @@ public class AudiobookRecorder extends JFrame { book.getComment(), book.getACX() ); - int r = JOptionPane.showConfirmDialog(AudiobookRecorder.this, info, "Edit Book", JOptionPane.OK_CANCEL_OPTION); + tabs.add("Data", info); + + JPanel effects = new JPanel(); + effects.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 0; + + effects.add(new JLabel("Ethereal Iterations:"), c); + c.gridx = 1; + JSpinner ethIt = new JSpinner(new SteppedNumericSpinnerModel(1, 10, 1, book.getInteger("effects.ethereal.iterations"))); + effects.add(ethIt, c); + c.gridx = 0; + c.gridy++; + + effects.add(new JLabel("Ethereal Attenuation:"), c); + c.gridx = 1; + JSpinner ethAt = new JSpinner(new SteppedNumericSpinnerModel(0, 100, 1, book.getInteger("effects.ethereal.attenuation"))); + effects.add(ethAt, c); + c.gridx = 0; + c.gridy++; + + effects.add(new JLabel("Ethereal Offset:"), c); + c.gridx = 1; + JSpinner ethOf = new JSpinner(new SteppedNumericSpinnerModel(0, 2000, 10, book.getInteger("effects.ethereal.offset"))); + effects.add(ethOf, c); + c.gridx = 0; + c.gridy++; + + tabs.add("Effects", effects); + + int r = JOptionPane.showConfirmDialog(AudiobookRecorder.this, tabs, "Edit Book", JOptionPane.OK_CANCEL_OPTION); if (r != JOptionPane.OK_OPTION) return; String tit = info.getTitle(); @@ -1031,6 +1085,10 @@ public class AudiobookRecorder extends JFrame { String com = info.getComment(); String acx = info.getACX(); + book.set("effects.ethereal.iterations", (Integer)ethIt.getValue()); + book.set("effects.ethereal.attenuation", (Integer)ethAt.getValue()); + book.set("effects.ethereal.offset", (Integer)ethOf.getValue()); + book.setAuthor(aut); book.setGenre(gen); book.setComment(com); @@ -1297,6 +1355,9 @@ public class AudiobookRecorder extends JFrame { File config = new File(bookRoot, "audiobook.abk"); Properties prefs = new Properties(); + prefs.setProperty("effects.ethereal.iterations", book.get("effects.ethereal.iterations")); + prefs.setProperty("effects.ethereal.offset", book.get("effects.ethereal.offset")); + prefs.setProperty("effects.ethereal.attenuation", book.get("effects.ethereal.attenuation")); prefs.setProperty("book.name", book.getName()); prefs.setProperty("book.author", book.getAuthor()); @@ -1330,6 +1391,7 @@ public class AudiobookRecorder extends JFrame { prefs.setProperty(String.format("%s.sentence.%08d.end-offset", keybase, i), Integer.toString(snt.getEndOffset())); 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"); i++; } } @@ -1379,7 +1441,7 @@ public class AudiobookRecorder extends JFrame { public void buildBook(Properties prefs) { - book = new Book(prefs.getProperty("book.name")); + book = new Book(prefs, prefs.getProperty("book.name")); book.setAuthor(prefs.getProperty("book.author")); book.setGenre(prefs.getProperty("book.genre")); @@ -1439,6 +1501,7 @@ public class AudiobookRecorder extends JFrame { postSentenceGap.setValue(s.getPostGap()); locked.setSelected(s.isLocked()); attention.setSelected(s.getAttentionFlag()); + ethereal.setSelected(s.getEthereal()); postSentenceGap.setEnabled(!s.isLocked()); reprocessAudioFFT.setEnabled(!s.isLocked()); @@ -1449,6 +1512,7 @@ public class AudiobookRecorder extends JFrame { postSentenceGap.setValue(0); locked.setSelected(false); attention.setSelected(false); + ethereal.setSelected(false); } } }); @@ -1488,6 +1552,7 @@ public class AudiobookRecorder extends JFrame { s.setEndOffset(Utils.s2i(prefs.getProperty(String.format("chapter.audition.sentence.%08d.end-offset", i)))); 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)))); bookTreeModel.insertNodeInto(s, c, c.getChildCount()); } @@ -1507,6 +1572,7 @@ public class AudiobookRecorder extends JFrame { s.setEndOffset(Utils.s2i(prefs.getProperty(String.format("chapter.open.sentence.%08d.end-offset", i)))); 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)))); bookTreeModel.insertNodeInto(s, c, c.getChildCount()); } @@ -1531,7 +1597,8 @@ public class AudiobookRecorder extends JFrame { s.setStartOffset(Utils.s2i(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.start-offset", cno, i)))); s.setEndOffset(Utils.s2i(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.end-offset", cno, i)))); 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.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)))); bookTreeModel.insertNodeInto(s, c, c.getChildCount()); } } @@ -1552,6 +1619,7 @@ public class AudiobookRecorder extends JFrame { s.setEndOffset(Utils.s2i(prefs.getProperty(String.format("chapter.close.sentence.%08d.end-offset", i)))); 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)))); bookTreeModel.insertNodeInto(s, c, c.getChildCount()); } diff --git a/src/uk/co/majenko/audiobookrecorder/Book.java b/src/uk/co/majenko/audiobookrecorder/Book.java index 2b6265c..702daf2 100644 --- a/src/uk/co/majenko/audiobookrecorder/Book.java +++ b/src/uk/co/majenko/audiobookrecorder/Book.java @@ -29,8 +29,12 @@ public class Book extends DefaultMutableTreeNode { float[] eqChannels = new float[31]; - public Book(String bookname) { + Properties prefs; + + public Book(Properties p, String bookname) { super(bookname); + + prefs = p; name = bookname; equaliser = new Equaliser(); AudiobookRecorder.window.setTitle("AudioBook Recorder :: " + name); @@ -164,4 +168,22 @@ public class Book extends DefaultMutableTreeNode { public AudioFormat getAudioFormat() { return new AudioFormat(getSampleRate(), getResolution(), getChannels(), true, false); } + + public String get(String key) { + if (prefs.getProperty(key) == null) { return Options.get(key); } + return prefs.getProperty(key); + } + + public Integer getInteger(String key) { + if (prefs.getProperty(key) == null) { return Options.getInteger(key); } + return Utils.s2i(prefs.getProperty(key)); + } + + public void set(String key, String value) { + prefs.setProperty(key, value); + } + + public void set(String key, Integer value) { + prefs.setProperty(key, "" + value); + } } diff --git a/src/uk/co/majenko/audiobookrecorder/Options.java b/src/uk/co/majenko/audiobookrecorder/Options.java index 7636008..70938de 100644 --- a/src/uk/co/majenko/audiobookrecorder/Options.java +++ b/src/uk/co/majenko/audiobookrecorder/Options.java @@ -34,6 +34,10 @@ public class Options extends JDialog { JCheckBox enableParsing; JSpinner cacheSize; + JSpinner etherealIterations; + JSpinner etherealAttenuation; + JSpinner etherealOffset; + JTextField havenApiKey; Equaliser equaliser; @@ -319,6 +323,26 @@ public class Options extends JDialog { startupScript = new JTextArea(get("scripts.startup")); startScript.add(startupScript, BorderLayout.CENTER); tabs.add("Startup Script", startScript); + + + + JPanel effects = new JPanel(); + effects.setLayout(new GridBagLayout()); + constraint.gridx = 0; + constraint.gridy = 0; + constraint.gridwidth = 1; + constraint.gridheight = 1; + + etherealOffset = addSpinner(effects, "Ethereal Voice Offset", 0, 2000, 10, getInteger("effects.ethereal.offset"), "ms"); + etherealIterations = addSpinner(effects, "Ethereal Voice Iterations", 1, 10, 1, getInteger("effects.ethereal.iterations"), ""); + etherealAttenuation = addSpinner(effects, "Ethereal Voice Attenuation", 0, 100, 1, getInteger("effects.ethereal.attenuation"), "%"); + + tabs.add("Effects", effects); + + + + + add(tabs, BorderLayout.CENTER); setTitle("Options"); @@ -525,6 +549,10 @@ public class Options extends JDialog { defaultPrefs.put("scripts.startup", ""); + defaultPrefs.put("effects.ethereal.offset", "50"); + defaultPrefs.put("effects.ethereal.iterations", "3"); + defaultPrefs.put("effects.ethereal.attenuation", "50"); + if (prefs == null) { prefs = Preferences.userNodeForPackage(AudiobookRecorder.class); } @@ -631,6 +659,10 @@ public class Options extends JDialog { set("process.haven.apikey", havenApiKey.getText()); set("cache.size", cacheSize.getValue()); + set("effects.ethereal.offset", etherealOffset.getValue()); + set("effects.ethereal.iterations", etherealIterations.getValue()); + set("effects.ethereal.attenuation", etherealAttenuation.getValue()); + for (int i = 0; i < 31; i++) { set("audio.eq." + i, equaliser.getChannel(i)); } diff --git a/src/uk/co/majenko/audiobookrecorder/Sentence.java b/src/uk/co/majenko/audiobookrecorder/Sentence.java index d86a734..77f6250 100644 --- a/src/uk/co/majenko/audiobookrecorder/Sentence.java +++ b/src/uk/co/majenko/audiobookrecorder/Sentence.java @@ -69,6 +69,8 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable { RecordingThread recordingThread; + boolean effectEthereal = false; + static class RecordingThread implements Runnable { boolean running = false; @@ -594,6 +596,8 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable { bytesToRead--; } } + + data = postProcessData(data); return data; } catch (Exception e) { @@ -602,6 +606,69 @@ 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; + + 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; + + } else { + return data; + } + } + public void recognise() { AudiobookRecorder.window.havenQueue.submit(Sentence.this); } @@ -834,4 +901,29 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable { ex.printStackTrace(); } } + + public void setEthereal(boolean e) { + effectEthereal = e; + } + + public boolean getEthereal() { + return effectEthereal; + } + + public double mix(double a, double b) { + double z; + double fa, fb, fz; + fa = a + 32768d; + fb = b + 32768d; + + if (fa < 32768d && fb < 32768d) { + fz = (fa * fb) / 32768d; + } else { + fz = (2d * (fa + fb)) - ((fa * fb) / 32768d) - 65536d; + } + + z = fz - 32768d; + return z; + } + }