commit a40f22be166d0f76999e0b0f5a525e00cf75a940 Author: Matt Jenkins Date: Sun Sep 9 19:08:08 2018 +0100 Initial import diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..a9b38ec --- /dev/null +++ b/build.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deps/JTattoo-1.6.10.jar b/deps/JTattoo-1.6.10.jar new file mode 100644 index 0000000..4ebf735 Binary files /dev/null and b/deps/JTattoo-1.6.10.jar differ diff --git a/deps/sphinx4-core-5prealpha-SNAPSHOT.jar b/deps/sphinx4-core-5prealpha-SNAPSHOT.jar new file mode 100644 index 0000000..7261e35 Binary files /dev/null and b/deps/sphinx4-core-5prealpha-SNAPSHOT.jar differ diff --git a/deps/sphinx4-data-5prealpha-SNAPSHOT.jar b/deps/sphinx4-data-5prealpha-SNAPSHOT.jar new file mode 100644 index 0000000..7a48060 Binary files /dev/null and b/deps/sphinx4-data-5prealpha-SNAPSHOT.jar differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/book.png b/resources/uk/co/majenko/audiobookrecorder/icons/book.png new file mode 100644 index 0000000..a2a8e42 Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/book.png differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/chapter.png b/resources/uk/co/majenko/audiobookrecorder/icons/chapter.png new file mode 100644 index 0000000..4e4ebf2 Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/chapter.png differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/new-chapter.png b/resources/uk/co/majenko/audiobookrecorder/icons/new-chapter.png new file mode 100644 index 0000000..f285718 Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/new-chapter.png differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/new.png b/resources/uk/co/majenko/audiobookrecorder/icons/new.png new file mode 100644 index 0000000..b8d7446 Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/new.png differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/open.png b/resources/uk/co/majenko/audiobookrecorder/icons/open.png new file mode 100644 index 0000000..317a357 Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/open.png differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/play.png b/resources/uk/co/majenko/audiobookrecorder/icons/play.png new file mode 100644 index 0000000..2820368 Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/play.png differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/playon.png b/resources/uk/co/majenko/audiobookrecorder/icons/playon.png new file mode 100644 index 0000000..0a33f97 Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/playon.png differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/record-room.png b/resources/uk/co/majenko/audiobookrecorder/icons/record-room.png new file mode 100644 index 0000000..eb15208 Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/record-room.png differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/record.png b/resources/uk/co/majenko/audiobookrecorder/icons/record.png new file mode 100644 index 0000000..672c1c3 Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/record.png differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/save.png b/resources/uk/co/majenko/audiobookrecorder/icons/save.png new file mode 100644 index 0000000..a81e70d Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/save.png differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/sentence.png b/resources/uk/co/majenko/audiobookrecorder/icons/sentence.png new file mode 100644 index 0000000..7a2ad2f Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/sentence.png differ diff --git a/resources/uk/co/majenko/audiobookrecorder/icons/stop.png b/resources/uk/co/majenko/audiobookrecorder/icons/stop.png new file mode 100644 index 0000000..4ecbdf6 Binary files /dev/null and b/resources/uk/co/majenko/audiobookrecorder/icons/stop.png differ diff --git a/src/uk/co/majenko/audiobookrecorder/AudiobookRecorder.java b/src/uk/co/majenko/audiobookrecorder/AudiobookRecorder.java new file mode 100644 index 0000000..dad1da9 --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/AudiobookRecorder.java @@ -0,0 +1,884 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.sound.sampled.*; +import javax.swing.*; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; +import java.nio.file.*; +import javax.swing.tree.*; +import java.lang.reflect.*; +import java.util.*; +import java.util.prefs.*; +import java.io.*; + +public class AudiobookRecorder extends JFrame { + + MainToolBar toolBar; + + JMenuBar menuBar; + JMenu fileMenu; + JMenu bookMenu; + JMenu toolsMenu; + + JMenuItem fileNewBook; + JMenuItem fileOpenBook; + JMenuItem fileExit; + + JMenuItem bookNewChapter; + JMenuItem bookExportAudio; + + JMenuItem toolsOptions; + + FlashPanel centralPanel; + + JPanel statusBar; + + JLabel statusFormat; + + JScrollPane mainScroll; + + Book book = null; + + JTree bookTree; + DefaultTreeModel bookTreeModel; + + Sentence recording = null; + Sentence playing = null; + Sentence roomNoise = null; + Sentence selectedSentence = null; + + JPanel sampleControl; + Waveform sampleWaveform; + + JSpinner startOffset; + JSpinner endOffset; + JSpinner postSentenceGap; + + Thread playingThread = null; + + public static AudiobookRecorder window; + + void buildToolbar(Container ob) { + toolBar = new MainToolBar(this); + toolBar.disableBook(); + toolBar.disableSentence(); + ob.add(toolBar, BorderLayout.NORTH); + } + + void buildMenu(Container ob) { + menuBar = new JMenuBar(); + + fileMenu = new JMenu("File"); + + fileNewBook = new JMenuItem("New Book..."); + fileNewBook.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + createNewBook(); + } + }); + fileOpenBook = new JMenuItem("Open Book..."); + fileExit = new JMenuItem("Exit"); + + fileMenu.add(fileNewBook); + fileMenu.add(fileOpenBook); + fileMenu.addSeparator(); + fileMenu.add(fileExit); + + menuBar.add(fileMenu); + + + bookMenu = new JMenu("Book"); + + bookNewChapter = new JMenuItem("New Chapter"); + bookExportAudio = new JMenuItem("Export Audio..."); + + bookMenu.add(bookNewChapter); + bookMenu.add(bookExportAudio); + + menuBar.add(bookMenu); + + toolsMenu = new JMenu("Tools"); + + toolsOptions = new JMenuItem("Options"); + toolsOptions.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + Options o = new Options(AudiobookRecorder.this); + } + }); + + toolsMenu.add(toolsOptions); + + menuBar.add(toolsMenu); + + ob.add(menuBar, BorderLayout.NORTH); + + setPreferredSize(new Dimension(700, 500)); + setLocationRelativeTo(null); + + pack(); + + setVisible(true); + + } + + public AudiobookRecorder() { + + window = this; + + + Icons.loadIcons(); + + + try { + String clsname = "com.jtattoo.plaf.aluminium.AluminiumLookAndFeel"; + UIManager.setLookAndFeel(clsname); + + Properties p = new Properties(); + p.put("windowDecoration", "off"); + p.put("logoString", "Audiobook"); + p.put("textAntiAliasing", "on"); + + Class cls = Class.forName(clsname); + Class[] cArg = new Class[1]; + cArg[0] = Properties.class; + Method mth = cls.getMethod("setCurrentTheme", cArg); + mth.invoke(cls, p); + + + + } catch (Exception e) { + e.printStackTrace(); + } + + + Options.loadPreferences(); + + setLayout(new BorderLayout()); + + addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + System.exit(0); + } + }); + + buildMenu(this); + + centralPanel = new FlashPanel(); + centralPanel.setLayout(new BorderLayout()); + add(centralPanel, BorderLayout.CENTER); + + sampleControl = new JPanel(); + sampleControl.setLayout(new BorderLayout()); + sampleControl.setPreferredSize(new Dimension(400, 150)); + sampleWaveform = new Waveform(); + + sampleControl.add(sampleWaveform, BorderLayout.CENTER); + + startOffset = new JSpinner(new SteppedNumericSpinnerModel(0, 0, 1, 0)); + startOffset.setPreferredSize(new Dimension(100, 20)); + endOffset = new JSpinner(new SteppedNumericSpinnerModel(0, 0, 1, 0)); + endOffset.setPreferredSize(new Dimension(100, 20)); + postSentenceGap = new JSpinner(new SteppedNumericSpinnerModel(0, 5000, 100, 0)); + postSentenceGap.setPreferredSize(new Dimension(75, 20)); + + startOffset.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + JSpinner ob = (JSpinner)e.getSource(); + if (selectedSentence != null) { + selectedSentence.setStartOffset((Integer)ob.getValue()); + sampleWaveform.setLeftMarker((Integer)ob.getValue()); + } + } + }); + + endOffset.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + JSpinner ob = (JSpinner)e.getSource(); + if (selectedSentence != null) { + selectedSentence.setEndOffset((Integer)ob.getValue()); + sampleWaveform.setRightMarker((Integer)ob.getValue()); + } + } + }); + + postSentenceGap.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + JSpinner ob = (JSpinner)e.getSource(); + if (selectedSentence != null) { + selectedSentence.setPostGap((Integer)ob.getValue()); + } + } + }); + + JPanel controls = new JPanel(); + controls.add(new JLabel("Start Offset:")); + + JButton startFastDown = new JButton("<<"); + startFastDown.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + SteppedNumericSpinnerModel m = (SteppedNumericSpinnerModel)startOffset.getModel(); + int f = (Integer)startOffset.getValue(); + int max = m.getMaximum(); + f -= (max / 10); + if (f < 0) f = 0; + startOffset.setValue(f); + } + }); + controls.add(startFastDown); + + JButton startSlowDown = new JButton("<"); + startSlowDown.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + SteppedNumericSpinnerModel m = (SteppedNumericSpinnerModel)startOffset.getModel(); + int f = (Integer)startOffset.getValue(); + int max = m.getMaximum(); + f -= (max / 100); + if (f < 0) f = 0; + startOffset.setValue(f); + } + }); + controls.add(startSlowDown); + + controls.add(startOffset); + + JButton startSlowUp = new JButton(">"); + startSlowUp.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + SteppedNumericSpinnerModel m = (SteppedNumericSpinnerModel)startOffset.getModel(); + int f = (Integer)startOffset.getValue(); + int max = m.getMaximum(); + f += (max / 100); + if (f > max) f = max; + startOffset.setValue(f); + } + }); + controls.add(startSlowUp); + + JButton startFastUp = new JButton(">>"); + startFastUp.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + SteppedNumericSpinnerModel m = (SteppedNumericSpinnerModel)startOffset.getModel(); + int f = (Integer)startOffset.getValue(); + int max = m.getMaximum(); + f += (max / 10); + if (f > max) f = max; + startOffset.setValue(f); + } + }); + controls.add(startFastUp); + + + controls.add(new JLabel("End Offset:")); + + JButton endFastDown = new JButton("<<"); + endFastDown.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + SteppedNumericSpinnerModel m = (SteppedNumericSpinnerModel)endOffset.getModel(); + int f = (Integer)endOffset.getValue(); + int max = m.getMaximum(); + f -= (max / 10); + if (f < 0) f = 0; + endOffset.setValue(f); + } + }); + controls.add(endFastDown); + + JButton endSlowDown = new JButton("<"); + endSlowDown.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + SteppedNumericSpinnerModel m = (SteppedNumericSpinnerModel)endOffset.getModel(); + int f = (Integer)endOffset.getValue(); + int max = m.getMaximum(); + f -= (max / 100); + if (f < 0) f = 0; + endOffset.setValue(f); + } + }); + controls.add(endSlowDown); + + controls.add(endOffset); + + JButton endSlowUp = new JButton(">"); + endSlowUp.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + SteppedNumericSpinnerModel m = (SteppedNumericSpinnerModel)endOffset.getModel(); + int f = (Integer)endOffset.getValue(); + int max = m.getMaximum(); + f += (max / 100); + if (f > max) f = max; + endOffset.setValue(f); + } + }); + controls.add(endSlowUp); + + JButton endFastUp = new JButton(">>"); + endFastUp.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + SteppedNumericSpinnerModel m = (SteppedNumericSpinnerModel)endOffset.getModel(); + int f = (Integer)endOffset.getValue(); + int max = m.getMaximum(); + f += (max / 10); + if (f > max) f = max; + endOffset.setValue(f); + } + }); + controls.add(endFastUp); + + controls.add(new JLabel("Post gap:")); + controls.add(postSentenceGap); + controls.add(new JLabel("ms")); + + sampleControl.add(controls, BorderLayout.SOUTH); + + centralPanel.add(sampleControl, BorderLayout.SOUTH); + + statusBar = new JPanel(); + add(statusBar, BorderLayout.SOUTH); + + statusFormat = new JLabel(Options.getAudioFormat().toString()); + statusBar.add(statusFormat); + + buildToolbar(centralPanel); + + centralPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("R"), "startRecord"); + centralPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("released R"), "stopRecord"); + + centralPanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("released D"), "deleteLast"); + + centralPanel.getActionMap().put("startRecord", new AbstractAction() { + public void actionPerformed(ActionEvent e) { + if (bookTree.isEditing()) return; + startRecording(); + } + }); + centralPanel.getActionMap().put("stopRecord", new AbstractAction() { + public void actionPerformed(ActionEvent e) { + if (bookTree.isEditing()) return; + stopRecording(); + } + }); + centralPanel.getActionMap().put("deleteLast", new AbstractAction() { + public void actionPerformed(ActionEvent e) { + if (bookTree.isEditing()) return; + deleteLastRecording(); + } + }); + + mainScroll = new JScrollPane(); + centralPanel.add(mainScroll, BorderLayout.CENTER); + + setTitle("AudioBook Recorder"); + } + + public static void main(String args[]) { + AudiobookRecorder frame = new AudiobookRecorder(); + } + + public void createNewBook() { + BookInfoPanel info = new BookInfoPanel("", "", "", ""); + int r = JOptionPane.showConfirmDialog(this, info, "New Book", JOptionPane.OK_CANCEL_OPTION); + if (r != JOptionPane.OK_OPTION) return; + + System.err.println("Name: " + info.getTitle()); + System.err.println("Author: " + info.getAuthor()); + System.err.println("Genre: " + info.getGenre()); + System.err.println("Comment: " + info.getComment()); + + String name = info.getTitle(); + + File bookdir = new File(Options.get("path.storage"), name); + if (bookdir.exists()) { + JOptionPane.showMessageDialog(this, "File already exists.", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + + Properties prefs = new Properties(); + + prefs.setProperty("book.name", info.getTitle()); + prefs.setProperty("book.author", info.getAuthor()); + prefs.setProperty("book.genre", info.getGenre()); + prefs.setProperty("book.comment", info.getComment()); + prefs.setProperty("chapter.open.name", "Opening Credits"); + prefs.setProperty("chapter.open.pre-gap", Options.get("catenation.pre-chapter")); + prefs.setProperty("chapter.open.post-gap", Options.get("catenation.post-chapter")); + prefs.setProperty("chapter.0001.name", "Chapter 1"); + prefs.setProperty("chapter.0001.pre-gap", Options.get("catenation.pre-chapter")); + prefs.setProperty("chapter.0001.post-gap", Options.get("catenation.post-chapter")); + prefs.setProperty("chapter.close.name", "Closing Credits"); + prefs.setProperty("chapter.close.pre-gap", Options.get("catenation.pre-chapter")); + prefs.setProperty("chapter.close.post-gap", Options.get("catenation.post-chapter")); + + buildBook(prefs); + } + + class JMenuObject extends JMenuItem { + Object ob; + + public JMenuObject(String p) { + super(p); + ob = null; + } + + public JMenuObject(String p, Object o) { + super(p); + ob = o; + } + + public void setObject(Object o) { + ob = o; + } + + public Object getObject() { + return ob; + } + } + + void treePopup(MouseEvent e) { + + int selRow = bookTree.getRowForLocation(e.getX(), e.getY()); + TreePath selPath = bookTree.getPathForLocation(e.getX(), e.getY()); + if (selRow != -1) { + + DefaultMutableTreeNode node = (DefaultMutableTreeNode)selPath.getLastPathComponent(); + + if (node instanceof Sentence) { + Sentence s = (Sentence)node; + + bookTree.setSelectionPath(new TreePath(s.getPath())); + + JPopupMenu menu = new JPopupMenu(); + JMenuObject ins = new JMenuObject("Insert sentence above", s); + JMenuObject del = new JMenuObject("Delete sentence", s); + JMenuObject edit = new JMenuObject("Edit text", s); + JMenuObject process = new JMenuObject("Reprocess audio", s); + + del.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + JMenuObject o = (JMenuObject)e.getSource(); + Sentence s = (Sentence)o.getObject(); + s.deleteFiles(); + Chapter c = (Chapter)s.getParent(); + bookTreeModel.removeNodeFromParent(s); + } + }); + + edit.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + JMenuObject o = (JMenuObject)e.getSource(); + Sentence s = (Sentence)o.getObject(); + s.editText(); + Chapter c = (Chapter)s.getParent(); + bookTreeModel.reload(s); + } + }); + + menu.add(del); + menu.add(edit); + menu.add(process); + menu.show(bookTree, e.getX(), e.getY()); + } else if (node instanceof Chapter) { + Chapter c = (Chapter)node; + + bookTree.setSelectionPath(new TreePath(c.getPath())); + + JPopupMenu menu = new JPopupMenu(); + JMenuObject ren = new JMenuObject("Rename chapter", c); + + ren.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + JMenuObject o = (JMenuObject)e.getSource(); + Chapter c = (Chapter)o.getObject(); + c.renameChapter(); + bookTreeModel.reload(c); + } + }); + + menu.add(ren); + menu.show(bookTree, e.getX(), e.getY()); + } + + } + } + + public void startRecording() { + + if (recording != null) return; + if (book == null) return; + + toolBar.disableBook(); + toolBar.disableSentence(); + + DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode)bookTree.getLastSelectedPathComponent(); + + if (selectedNode == null) { + selectedNode = book.getLastChapter(); + bookTree.setSelectionPath(new TreePath(selectedNode.getPath())); + } + + if (selectedNode instanceof Book) { + selectedNode = book.getLastChapter(); + bookTree.setSelectionPath(new TreePath(selectedNode.getPath())); + } + + if (selectedNode instanceof Sentence) { + selectedNode = (DefaultMutableTreeNode)selectedNode.getParent(); + bookTree.setSelectionPath(new TreePath(selectedNode.getPath())); + } + + Chapter c = (Chapter)selectedNode; + + Sentence s = new Sentence(); + bookTreeModel.insertNodeInto(s, c, c.getChildCount()); + + bookTree.expandPath(new TreePath(c.getPath())); + bookTree.setSelectionPath(new TreePath(s.getPath())); + bookTree.scrollPathToVisible(new TreePath(s.getPath())); + + if (s.startRecording()) { + recording = s; + centralPanel.setFlash(true); + } + } + + public void stopRecording() { + if (recording == null) return; + recording.stopRecording(); + + bookTreeModel.reload(book); + + bookTree.expandPath(new TreePath(((DefaultMutableTreeNode)recording.getParent()).getPath())); + bookTree.setSelectionPath(new TreePath(recording.getPath())); + bookTree.scrollPathToVisible(new TreePath(recording.getPath())); + + toolBar.enableBook(); + toolBar.enableSentence(); + + centralPanel.setFlash(false); + recording = null; + saveBookStructure(); + } + + public void deleteLastRecording() { + if (recording != null) return; + if (book == null) return; + + DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode)bookTree.getLastSelectedPathComponent(); + + if (selectedNode == null) { + selectedNode = book.getLastChapter(); + bookTree.setSelectionPath(new TreePath(selectedNode.getPath())); + } + + if (selectedNode instanceof Book) { + selectedNode = book.getLastChapter(); + bookTree.setSelectionPath(new TreePath(selectedNode.getPath())); + } + + if (selectedNode instanceof Sentence) { + selectedNode = (DefaultMutableTreeNode)selectedNode.getParent(); + bookTree.setSelectionPath(new TreePath(selectedNode.getPath())); + } + + Chapter c = (Chapter)selectedNode; + + Sentence s = c.getLastSentence(); + if (s == null) return; + + s.deleteFiles(); + bookTreeModel.removeNodeFromParent(s); + s = c.getLastSentence(); + + bookTree.expandPath(new TreePath(selectedNode.getPath())); + if (s != null) { + bookTree.setSelectionPath(new TreePath(s.getPath())); + bookTree.scrollPathToVisible(new TreePath(s.getPath())); + } + } + + public void addChapter() { + Chapter c = book.addChapter(); + Chapter lc = book.getLastChapter(); + int i = bookTreeModel.getIndexOfChild(book, lc); + bookTreeModel.insertNodeInto(c, book, i+1); + bookTree.scrollPathToVisible(new TreePath(c.getPath())); + } + + @SuppressWarnings("unchecked") + public void saveBookStructure() { + File bookRoot = new File(Options.get("path.storage"), book.getName()); + if (!bookRoot.exists()) { + bookRoot.mkdirs(); + } + + File config = new File(bookRoot, "audiobook.abk"); + Properties prefs = new Properties(); + + + prefs.setProperty("book.name", book.getName()); + prefs.setProperty("book.author", book.getAuthor()); + prefs.setProperty("book.genre", book.getGenre()); + prefs.setProperty("book.comment", book.getComment()); + + for (Enumeration o = book.children(); o.hasMoreElements();) { + + Chapter c = o.nextElement(); + String keybase = "chapter." + c.getId(); + prefs.setProperty(keybase + ".name", c.getName()); + prefs.setProperty(keybase + ".pre-gap", Integer.toString(c.getPreGap())); + prefs.setProperty(keybase + ".post-gap", Integer.toString(c.getPostGap())); + + int i = 0; + for (Enumeration s = c.children(); s.hasMoreElements();) { + Sentence snt = s.nextElement(); + prefs.setProperty(String.format("%s.sentence.%08d.id", keybase, i), snt.getId()); + prefs.setProperty(String.format("%s.sentence.%08d.text", keybase, i), snt.getText()); + prefs.setProperty(String.format("%s.sentence.%08d.post-gap", keybase, i), Integer.toString(snt.getPostGap())); + prefs.setProperty(String.format("%s.sentence.%08d.start-offset", keybase, i), Integer.toString(snt.getStartOffset())); + prefs.setProperty(String.format("%s.sentence.%08d.end-offset", keybase, i), Integer.toString(snt.getEndOffset())); + i++; + } + } + + try { + FileOutputStream fos = new FileOutputStream(config); + prefs.storeToXML(fos, "Audiobook Recorder Description"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void loadBookStructure(File f) { + try { + Properties prefs = new Properties(); + FileInputStream fis = new FileInputStream(f); + prefs.loadFromXML(fis); + + buildBook(prefs); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void buildBook(Properties prefs) { + + book = new Book(prefs.getProperty("book.name")); + + book.setAuthor(prefs.getProperty("book.author")); + book.setGenre(prefs.getProperty("book.genre")); + book.setComment(prefs.getProperty("book.comment")); + + bookTreeModel = new DefaultTreeModel(book); + bookTree = new JTree(bookTreeModel); + bookTree.setEditable(true); + bookTree.setUI(new CustomTreeUI(mainScroll)); + + roomNoise = new Sentence("room-noise", "Room Noise"); + + bookTree.addTreeSelectionListener(new TreeSelectionListener() { + public void valueChanged(TreeSelectionEvent e) { + DefaultMutableTreeNode n = (DefaultMutableTreeNode)bookTree.getLastSelectedPathComponent(); + if (n instanceof Sentence) { + Sentence s = (Sentence)n; + selectedSentence = s; + sampleWaveform.setData(s.getAudioData()); + sampleWaveform.setMarkers(s.getStartOffset(), s.getEndOffset()); + startOffset.setValue(s.getStartOffset()); + endOffset.setValue(s.getEndOffset()); + postSentenceGap.setValue(s.getPostGap()); + + int samples = s.getSampleSize(); + + ((SteppedNumericSpinnerModel)startOffset.getModel()).setMaximum(samples); + ((SteppedNumericSpinnerModel)endOffset.getModel()).setMaximum(samples); + + toolBar.enableSentence(); + } else { + selectedSentence = null; + toolBar.disableSentence(); + sampleWaveform.clearData(); + startOffset.setValue(0); + endOffset.setValue(0); + postSentenceGap.setValue(0); + } + } + }); + + + bookTree.addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent e) { + if (e.isPopupTrigger()) { + treePopup(e); + } + } + + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger()) { + treePopup(e); + } + } + + }); + + mainScroll.setViewportView(bookTree); + + + Chapter c = new Chapter("open", prefs.getProperty("chapter.open.name")); + c.setPostGap(s2i(prefs.getProperty("chapter.open.post-gap"))); + c.setPreGap(s2i(prefs.getProperty("chapter.open.pre-gap"))); + bookTreeModel.insertNodeInto(c, book, 0); + + for (int i = 0; i < 100000000; i++) { + String id = prefs.getProperty(String.format("chapter.open.sentence.%08d.id", i)); + String text = prefs.getProperty(String.format("chapter.open.sentence.%08d.text", i)); + int gap = s2i(prefs.getProperty(String.format("chapter.open.sentence.%08d.post-gap", i))); + if (id == null) break; + Sentence s = new Sentence(id, text); + s.setPostGap(gap); + s.setStartOffset(s2i(prefs.getProperty(String.format("chapter.open.sentence.%08d.start-offset", i)))); + s.setEndOffset(s2i(prefs.getProperty(String.format("chapter.open.sentence.%08d.end-offset", i)))); + bookTreeModel.insertNodeInto(s, c, c.getChildCount()); + } + + for (int cno = 1; cno < 10000; cno++) { + String cname = prefs.getProperty(String.format("chapter.%04d.name", cno)); + if (cname == null) break; + + c = new Chapter(String.format("%04d", cno), cname); + c.setPostGap(s2i(prefs.getProperty(String.format("chapter.%04d.post-gap", cno)))); + c.setPreGap(s2i(prefs.getProperty(String.format("chapter.%04d.pre-gap", cno)))); + bookTreeModel.insertNodeInto(c, book, book.getChildCount()); + + for (int i = 0; i < 100000000; i++) { + String id = prefs.getProperty(String.format("chapter.%04d.sentence.%08d.id", cno, i)); + String text = prefs.getProperty(String.format("chapter.%04d.sentence.%08d.text", cno, i)); + int gap = s2i(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.post-gap", cno, i))); + if (id == null) break; + Sentence s = new Sentence(id, text); + s.setPostGap(gap); + s.setStartOffset(s2i(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.start-offset", cno, i)))); + s.setEndOffset(s2i(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.end-offset", cno, i)))); + bookTreeModel.insertNodeInto(s, c, c.getChildCount()); + } + } + + c = new Chapter("close", prefs.getProperty("chapter.close.name")); + c.setPostGap(s2i(prefs.getProperty("chapter.close.post-gap"))); + c.setPreGap(s2i(prefs.getProperty("chapter.close.pre-gap"))); + bookTreeModel.insertNodeInto(c, book, book.getChildCount()); + + for (int i = 0; i < 100000000; i++) { + String id = prefs.getProperty(String.format("chapter.close.sentence.%08d.id", i)); + String text = prefs.getProperty(String.format("chapter.close.sentence.%08d.text", i)); + int gap = s2i(prefs.getProperty(String.format("chapter.close.sentence.%08d.post-gap", i))); + if (id == null) break; + Sentence s = new Sentence(id, text); + s.setPostGap(gap); + s.setStartOffset(s2i(prefs.getProperty(String.format("chapter.close.sentence.%08d.start-offset", i)))); + s.setEndOffset(s2i(prefs.getProperty(String.format("chapter.close.sentence.%08d.end-offset", i)))); + bookTreeModel.insertNodeInto(s, c, c.getChildCount()); + } + + bookTree.expandPath(new TreePath(book.getPath())); + + toolBar.enableBook(); + } + + public void openBook() { + + + OpenBookPanel info = new OpenBookPanel(); + int r = JOptionPane.showConfirmDialog(this, info, "Open Book", JOptionPane.OK_CANCEL_OPTION); + if (r == JOptionPane.OK_OPTION) { + File f = info.getSelectedFile(); + if (!f.exists()) { + JOptionPane.showMessageDialog(this, "File not found.", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + if (f.isDirectory()) { + JOptionPane.showMessageDialog(this, "File is directory.", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + if (!f.getName().endsWith(".abk")) { + JOptionPane.showMessageDialog(this, "Not a .abk file.", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + loadBookStructure(f); + + } + + + } + + int s2i(String s) { + try { + return Integer.parseInt(s); + } catch (Exception e) { + } + return 0; + } + + public File getBookFolder() { + File bf = new File(Options.get("path.storage"), book.getName()); + if (!bf.exists()) { + bf.mkdirs(); + } + return bf; + } + + public int getNoiseFloor() { + int[] samples = roomNoise.getAudioData(); + 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]); + } + } + + ms *= 10; + ms /= 9; + return ms; + } + + public void recordRoomNoise() { + if (roomNoise.startRecording()) { + + centralPanel.setFlash(true); + java.util.Timer ticker = new java.util.Timer(true); + ticker.schedule(new TimerTask() { + public void run() { + roomNoise.stopRecording(); + centralPanel.setFlash(false); + } + }, 5000); // 5 seconds of recording + + } + } + + public void playSelectedSentence() { + if (selectedSentence == null) return; + + if (playing != null) return; + + playing = selectedSentence; + + + playingThread = new Thread(new Runnable() { + public void run() { + playing.play(); + playing = null; + } + }); + + playingThread.setDaemon(true); + playingThread.start(); + + } +} diff --git a/src/uk/co/majenko/audiobookrecorder/Book.java b/src/uk/co/majenko/audiobookrecorder/Book.java new file mode 100644 index 0000000..94dc766 --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/Book.java @@ -0,0 +1,102 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.swing.*; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import java.io.*; +import java.nio.file.*; +import javax.swing.tree.*; + +public class Book extends DefaultMutableTreeNode { + + String name; + String author; + String genre; + String comment; + + public Book(String bookname) { + super(bookname); + name = bookname; + } + + public void setAuthor(String a) { + author = a; + } + + public void setGenre(String g) { + genre = g; + } + + public void setComment(String c) { + comment = c; + } + + public String getAuthor() { + return author; + } + + public String getGenre() { + return genre; + } + + public String getComment() { + return comment; + } + + public Chapter getClosingCredits() { + return getChapterById("close"); + } + + public Chapter getOpeningCredits() { + return getChapterById("open"); + } + + @SuppressWarnings("unchecked") + public Chapter getChapterById(String id) { + for (Enumerationo = children(); o.hasMoreElements();) { + Object ob = o.nextElement(); + if (ob instanceof Chapter) { + Chapter c = (Chapter)ob; + if (c.getId().equals(id)) { + return c; + } + } + } + return null; + } + + public Chapter getLastChapter() { + Chapter cc = getClosingCredits(); + if (cc == null) return null; + Chapter c = (Chapter)getChildBefore(cc); + if (c == null) return null; + if (c.getId().equals("open")) return null; + return c; + } + + public Chapter getChapter(int n) { + if (n == 0) return null; + return (Chapter)getChildAt(n); + } + + public Chapter addChapter() { + Chapter lc = getLastChapter(); + if (lc == null) return new Chapter("1", "Chapter 1"); + try { + int lcid = Integer.parseInt(lc.getId()); + lcid++; + + Chapter nc = new Chapter(String.format("%04d", lcid), "Chapter " + lcid); + return nc; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public String getName() { + return name; + } +} diff --git a/src/uk/co/majenko/audiobookrecorder/BookInfoPanel.java b/src/uk/co/majenko/audiobookrecorder/BookInfoPanel.java new file mode 100644 index 0000000..6030595 --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/BookInfoPanel.java @@ -0,0 +1,92 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.swing.*; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; + +public class BookInfoPanel extends JPanel { + + JTextField title; + JTextField author; + JTextField genre; + JTextField comment; + + public BookInfoPanel(String t, String a, String g, String c) { + super(); + setLayout(new GridBagLayout()); + GridBagConstraints con = new GridBagConstraints(); + + con.gridx = 0; + con.gridy = 0; + + add(new JLabel("Title:"), con); + con.gridx = 1; + title = new JTextField(t); + title.setPreferredSize(new Dimension(200, 20)); + add(title, con); + + con.gridx = 0; + con.gridy++; + + add(new JLabel("Author:"), con); + con.gridx = 1; + author = new JTextField(a); + author.setPreferredSize(new Dimension(200, 20)); + add(author, con); + + con.gridx = 0; + con.gridy++; + + add(new JLabel("Genre:"), con); + con.gridx = 1; + genre = new JTextField(g); + genre.setPreferredSize(new Dimension(200, 20)); + add(genre, con); + + con.gridx = 0; + con.gridy++; + + add(new JLabel("Comment:"), con); + con.gridx = 1; + comment = new JTextField(c); + comment.setPreferredSize(new Dimension(200, 20)); + add(comment, con); + + con.gridx = 0; + con.gridy++; + + } + + public String getTitle() { + return title.getText(); + } + + public String getAuthor() { + return author.getText(); + } + + public String getGenre() { + return genre.getText(); + } + + public String getComment() { + return comment.getText(); + } + + public void setTitle(String t) { + title.setText(t); + } + + public void setAuthor(String a) { + author.setText(a); + } + + public void setGenre(String g) { + genre.setText(g); + } + + public void setComment(String c) { + comment.setText(c); + } +} diff --git a/src/uk/co/majenko/audiobookrecorder/Chapter.java b/src/uk/co/majenko/audiobookrecorder/Chapter.java new file mode 100644 index 0000000..836cdd6 --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/Chapter.java @@ -0,0 +1,81 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.swing.*; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import java.io.*; +import java.nio.file.*; +import javax.swing.tree.*; + +public class Chapter extends DefaultMutableTreeNode { + + String name; + String id; + + int preGap; + int postGap; + + public Chapter(String i, String chaptername) { + super(chaptername); + + id = i; + name = chaptername; + preGap = Options.getInteger("catenation.pre-chapter"); + postGap = Options.getInteger("catenation.post-chapter"); + + } + + public String getId() { + return id; + } + + public Sentence getLastSentence() { + DefaultMutableTreeNode ls = getLastLeaf(); + if (ls instanceof Sentence) return (Sentence)ls; + return null; + } + + public void renameChapter() { + String n = JOptionPane.showInputDialog(null, "Rename Chapter", name); + if (n != null) { + name = n; + } + } + + public String toString() { + return name; + } + + public void setUserObject(Object o) { + if (o instanceof String) { + String so = (String)o; + name = so; + } + } + + public String getName() { + return name; + } + + public void setName(String n) { + name = n; + } + + public void setPreGap(int g) { + preGap = g; + } + + public int getPreGap() { + return preGap; + } + + public void setPostGap(int g) { + postGap = g; + } + + public int getPostGap() { + return postGap; + } +} diff --git a/src/uk/co/majenko/audiobookrecorder/CustomTreeUI.java b/src/uk/co/majenko/audiobookrecorder/CustomTreeUI.java new file mode 100644 index 0000000..a78289c --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/CustomTreeUI.java @@ -0,0 +1,45 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.swing.*; +import javax.swing.tree.*; +import javax.swing.plaf.*; +import javax.swing.plaf.basic.*; +import java.awt.*; + +public class CustomTreeUI extends BasicTreeUI { + + JScrollPane pane; + + public CustomTreeUI(JScrollPane p) { + super(); + pane = p; + } + + @Override + protected AbstractLayoutCache.NodeDimensions createNodeDimensions() { + return new NodeDimensionsHandler() { + @Override + public Rectangle getNodeDimensions( + Object value, int row, int depth, boolean expanded, + Rectangle size) { + Rectangle dimensions = super.getNodeDimensions(value, row, + depth, expanded, size); + dimensions.width = + pane.getWidth() - getRowX(row, depth); + return dimensions; + } + }; + } + + @Override + protected void paintHorizontalLine(Graphics g, JComponent c, + int y, int left, int right) { + // do nothing. + } + + @Override + protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds, + Insets insets, TreePath path) { + // do nothing. + } +} diff --git a/src/uk/co/majenko/audiobookrecorder/FlashPanel.java b/src/uk/co/majenko/audiobookrecorder/FlashPanel.java new file mode 100644 index 0000000..d606d64 --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/FlashPanel.java @@ -0,0 +1,51 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.swing.*; +import java.awt.*; +import java.util.*; + +public class FlashPanel extends JPanel { + + boolean flash = false; + boolean col = false; + + java.util.Timer ticker; + + public FlashPanel() { + super(); + ticker = new java.util.Timer(true); + ticker.scheduleAtFixedRate(new TimerTask() { + public void run() { + if (flash) { + col = !col; + repaint(); + } + } + }, 0, 500); + } + + public void setFlash(boolean f) { + flash = f; + + for (Component o : getComponents()) { + ((JComponent)o).setVisible(!f); + } + repaint(); + } + + @Override + protected void paintComponent(Graphics g) { + if (flash == false) { + super.paintComponent(g); + return; + } + if (col) { + g.setColor(Color.RED); + } else { + g.setColor(Color.BLACK); + } + Dimension d = getSize(); + g.fillRect(0, 0, d.width, d.height); + } + +} diff --git a/src/uk/co/majenko/audiobookrecorder/Icons.java b/src/uk/co/majenko/audiobookrecorder/Icons.java new file mode 100644 index 0000000..dc9ce1d --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/Icons.java @@ -0,0 +1,38 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.swing.*; + +public class Icons { + static public ImageIcon book; + static public ImageIcon chapter; + static public ImageIcon sentence; + static public ImageIcon play; + static public ImageIcon playon; + static public ImageIcon stop; + static public ImageIcon record; + + static public ImageIcon openBook; + static public ImageIcon newBook; + static public ImageIcon newChapter; + static public ImageIcon recordRoom; + static public ImageIcon save; + + static void loadIcons() { + book = new ImageIcon(Icons.class.getResource("icons/book.png")); + chapter = new ImageIcon(Icons.class.getResource("icons/chapter.png")); + sentence = new ImageIcon(Icons.class.getResource("icons/sentence.png")); + play = new ImageIcon(Icons.class.getResource("icons/play.png")); + playon = new ImageIcon(Icons.class.getResource("icons/playon.png")); + stop = new ImageIcon(Icons.class.getResource("icons/stop.png")); + record = new ImageIcon(Icons.class.getResource("icons/record.png")); + + openBook = new ImageIcon(Icons.class.getResource("icons/open.png")); + newBook = new ImageIcon(Icons.class.getResource("icons/new.png")); + newChapter = new ImageIcon(Icons.class.getResource("icons/new-chapter.png")); + recordRoom = new ImageIcon(Icons.class.getResource("icons/record-room.png")); + save = new ImageIcon(Icons.class.getResource("icons/save.png")); + + } +} + + diff --git a/src/uk/co/majenko/audiobookrecorder/MainToolBar.java b/src/uk/co/majenko/audiobookrecorder/MainToolBar.java new file mode 100644 index 0000000..2f827e8 --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/MainToolBar.java @@ -0,0 +1,116 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.swing.*; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; + +public class MainToolBar extends JToolBar { + + JButton newBook; + JButton openBook; + JButton saveBook; + JButton newChapter; + JButton recordRoomNoise; + JButton playSentence; + JButton playonSentence; + JButton recordSentence; + + AudiobookRecorder root; + + public MainToolBar(AudiobookRecorder r) { + super(); + + root = r; + + newBook = new JButton(Icons.newBook); + newBook.setToolTipText("New Book"); + newBook.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + root.createNewBook(); + } + }); + add(newBook); + + openBook = new JButton(Icons.openBook); + openBook.setToolTipText("Open Book"); + openBook.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + root.openBook(); + } + }); + add(openBook); + + saveBook = new JButton(Icons.save); + saveBook.setToolTipText("Save Book"); + saveBook.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + root.saveBookStructure(); + } + }); + add(saveBook); + + addSeparator(); + + newChapter = new JButton(Icons.newChapter); + newChapter.setToolTipText("New Chapter"); + newChapter.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + root.addChapter(); + } + }); + add(newChapter); + + recordRoomNoise = new JButton(Icons.recordRoom); + recordRoomNoise.setToolTipText("Record Room Noise"); + recordRoomNoise.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + root.recordRoomNoise(); + } + }); + add(recordRoomNoise); + + addSeparator(); + + playSentence = new JButton(Icons.play); + playSentence.setToolTipText("Play sentence"); + playSentence.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + root.playSelectedSentence(); + } + }); + add(playSentence); + + playonSentence = new JButton(Icons.playon); + playonSentence.setToolTipText("Play from sentence"); + add(playonSentence); + + recordSentence = new JButton(Icons.record); + recordSentence.setToolTipText("Re-record sentence"); + add(recordSentence); + + setFloatable(false); + } + + public void enableBook() { + newChapter.setEnabled(true); + recordRoomNoise.setEnabled(true); + } + + public void disableBook() { + newChapter.setEnabled(false); + recordRoomNoise.setEnabled(false); + } + + public void enableSentence() { + playSentence.setEnabled(true); + playonSentence.setEnabled(true); + recordSentence.setEnabled(true); + } + + public void disableSentence() { + playSentence.setEnabled(false); + playonSentence.setEnabled(false); + recordSentence.setEnabled(false); + } +} diff --git a/src/uk/co/majenko/audiobookrecorder/OpenBookPanel.java b/src/uk/co/majenko/audiobookrecorder/OpenBookPanel.java new file mode 100644 index 0000000..30132a2 --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/OpenBookPanel.java @@ -0,0 +1,133 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.swing.*; +import javax.swing.table.*; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.util.*; + +public class OpenBookPanel extends JPanel { + + JScrollPane scroll; + + JTable table; + + class BookInfo { + public String name; + public String author; + public String genre; + public String comment; + + public BookInfo(String n, String a, String g, String c) { + name = n; + author = a; + genre = g; + comment = c; + } + } + + class BookTableModel extends AbstractTableModel { + + ArrayList books; + + public BookTableModel() { + super(); + books = new ArrayList(); + } + + public int getRowCount() { + return books.size(); + } + + public int getColumnCount() { + return 4; + } + + public boolean isCellEditable(int row, int column) { + return false; + } + + public void addBook(BookInfo b) { + books.add(b); + } + + public Object getValueAt(int r, int c) { + if (c > 3) return null; + if (r > books.size()) return null; + BookInfo b = books.get(r); + switch (c) { + case 0: return b.name; + case 1: return b.author; + case 2: return b.genre; + case 4: return b.comment; + } + return null; + } + + public String getColumnName(int i) { + switch(i) { + case 0: return "Name"; + case 1: return "Author"; + case 2: return "Genre"; + case 3: return "Comment"; + } + return null; + } + } + + BookTableModel model; + + public OpenBookPanel() { + super(); + + model = new BookTableModel(); + + setLayout(new BorderLayout()); + + scroll = new JScrollPane(); + add(scroll, BorderLayout.CENTER); + + + try { + File dir = new File(Options.get("path.storage")); + + for (File b : dir.listFiles()) { + if (!b.isDirectory()) continue; + File xml = new File(b, "audiobook.abk"); + if (xml.exists()) { + Properties props = new Properties(); + props.loadFromXML(new FileInputStream(xml)); + + BookInfo book = new BookInfo( + props.getProperty("book.name"), + props.getProperty("book.author"), + props.getProperty("book.genre"), + props.getProperty("book.comment") + ); + + model.addBook(book); + } + } + + table = new JTable(model); + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + scroll.setViewportView(table); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public File getSelectedFile() { + int sel = table.getSelectedRow(); + if (sel == -1) { + return null; + } + + String name = (String)table.getValueAt(sel, 0); + File d = new File(Options.get("path.storage"), name); + File f = new File(d, "audiobook.abk"); + return f; + } +} diff --git a/src/uk/co/majenko/audiobookrecorder/Options.java b/src/uk/co/majenko/audiobookrecorder/Options.java new file mode 100644 index 0000000..5e4cd01 --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/Options.java @@ -0,0 +1,359 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.sound.sampled.*; +import javax.swing.*; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import java.util.prefs.*; +import java.io.*; + +public class Options extends JDialog { + + GridBagConstraints constraint; + + JComboBox mixerList; + JComboBox playbackList; + JComboBox channelList; + JComboBox rateList; + JTextField storageFolder; + JSpinner preChapterGap; + JSpinner postChapterGap; + JSpinner postSentenceGap; + + + static HashMap defaultPrefs; + static Preferences prefs = null; + + static class KVPair implements Comparable { + public String key; + public String value; + + public KVPair(String k, String v) { + key = k; + value = v; + } + + public String toString() { + return value; + } + + public int compareTo(Object o) { + if (o instanceof KVPair) { + KVPair ko = (KVPair)o; + return key.compareTo(ko.key); + } + return 0; + } + } + + JComboBox addDropdown(String label, KVPair[] options, String def) { + JLabel l = new JLabel(label); + constraint.gridx = 0; + constraint.gridwidth = 1; + constraint.gridheight = 1; + constraint.anchor = GridBagConstraints.LINE_START; + add(l, constraint); + + JComboBox o = new JComboBox(options); + constraint.gridx = 1; + add(o, constraint); + + for (KVPair p : options) { + if (p.key.equals(def)) { + o.setSelectedItem(p); + } + } + + constraint.gridy++; + + return o; + } + + JTextField addFilePath(String label, String path) { + JLabel l = new JLabel(label); + constraint.gridx = 0; + constraint.gridwidth = 1; + constraint.gridheight = 1; + constraint.anchor = GridBagConstraints.LINE_START; + add(l, constraint); + + JPanel p = new JPanel(); + p.setLayout(new BorderLayout()); + JTextField a = new JTextField(path); + p.add(a, BorderLayout.CENTER); + JButton b = new JButton("..."); + p.add(b, BorderLayout.EAST); + + constraint.gridx = 1; + + constraint.fill = GridBagConstraints.HORIZONTAL; + add(p, constraint); + + constraint.fill = GridBagConstraints.NONE; + + constraint.gridy++; + return a; + } + + void addSeparator() { + constraint.gridx = 0; + constraint.gridwidth = 2; + + JPanel p = new JPanel(); + p.setLayout(new BorderLayout()); + + JSeparator sep = new JSeparator(SwingConstants.HORIZONTAL); + sep.setPreferredSize(new Dimension(1, 1)); + p.add(sep, BorderLayout.CENTER); + + constraint.fill = GridBagConstraints.HORIZONTAL; + constraint.insets = new Insets(10, 2, 10, 2); + add(p, constraint); + constraint.insets = new Insets(2, 2, 2, 2); + constraint.fill = GridBagConstraints.NONE; + constraint.gridwidth = 1; + constraint.gridy++; + } + + JSpinner addSpinner(String label, int min, int max, int step, int value, String suffix) { + JLabel l = new JLabel(label); + constraint.gridx = 0; + constraint.gridwidth = 1; + constraint.gridheight = 1; + constraint.anchor = GridBagConstraints.LINE_START; + add(l, constraint); + + JPanel p = new JPanel(); + p.setLayout(new BorderLayout()); + JSpinner a = new JSpinner(new SteppedNumericSpinnerModel(min, max, step, value)); + + p.add(a, BorderLayout.CENTER); + JLabel b = new JLabel(suffix); + p.add(b, BorderLayout.EAST); + + constraint.gridx = 1; + + constraint.fill = GridBagConstraints.HORIZONTAL; + add(p, constraint); + + constraint.fill = GridBagConstraints.NONE; + + constraint.gridy++; + return a; + + } + + public Options(JFrame parent) { + loadPreferences(); // Just in case. It should do nothing. + + setLayout(new GridBagLayout()); + + constraint = new GridBagConstraints(); + + constraint.gridx = 0; + constraint.gridy = 0; + constraint.gridwidth = 1; + constraint.gridheight = 1; + + constraint.insets = new Insets(2, 2, 2, 2); + + addSeparator(); + + mixerList = addDropdown("Recording device:", getMixerList(), get("audio.recording.device")); + channelList = addDropdown("Channels:", getChannelCountList(), get("audio.recording.channels")); + rateList = addDropdown("Sample rate:", getSampleRateList(), get("audio.recording.samplerate")); + + addSeparator(); + + playbackList = addDropdown("Playback device:", getMixerList(), get("audio.playback.device")); + addSeparator(); + storageFolder = addFilePath("Storage folder:", get("path.storage")); + + addSeparator(); + + preChapterGap = addSpinner("Default pre-chapter gap:", 0, 5000, 100, getInteger("catenation.pre-chapter"), "ms"); + postChapterGap = addSpinner("Default post-chapter gap:", 0, 5000, 100, getInteger("catenation.post-chapter"), "ms"); + postSentenceGap = addSpinner("Default post-sentence gap:", 0, 5000, 100, getInteger("catenation.post-sentence"), "ms"); + + addSeparator(); + + + + + setTitle("Options"); + + setModalityType(Dialog.ModalityType.APPLICATION_MODAL); + + constraint.gridx = 1; + constraint.gridwidth = 1; + constraint.gridheight = 1; + + JPanel box = new JPanel(); + + JButton ok = new JButton("OK"); + + ok.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + storePreferences(); + Options.this.dispatchEvent(new WindowEvent(Options.this, WindowEvent.WINDOW_CLOSING)); + } + }); + + box.add(ok); + + JButton cancel = new JButton("Cancel"); + cancel.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + Options.this.dispatchEvent(new WindowEvent(Options.this, WindowEvent.WINDOW_CLOSING)); + } + }); + + box.add(cancel); + + constraint.anchor = GridBagConstraints.LINE_END; + + add(box, constraint); + + pack(); + + setLocationRelativeTo(parent); + + setVisible(true); + } + + static KVPair[] getMixerList() { + TreeSet list = new TreeSet(); + + Mixer.Info[] info = AudioSystem.getMixerInfo(); + for (Mixer.Info i : info) { + KVPair p = new KVPair(i.getName(), i.getDescription()); + list.add(p); + } + + return list.toArray(new KVPair[0]); + } + + static KVPair[] getChannelCountList() { + KVPair[] l = new KVPair[2]; + l[0] = new KVPair("1", "Mono"); + l[1] = new KVPair("2", "Stereo"); + return l; + } + + static KVPair[] getSampleRateList() { + KVPair[] l = new KVPair[2]; + l[0] = new KVPair("44100", "44100"); + l[1] = new KVPair("48000", "48000"); + return l; + } + + public static void loadPreferences() { + + defaultPrefs = new HashMap(); + + KVPair[] mixers = getMixerList(); + + defaultPrefs.put("audio.recording.device", mixers[0].key); + defaultPrefs.put("audio.recording.channels", "2"); + defaultPrefs.put("audio.recording.samplerate", "48000"); + defaultPrefs.put("audio.playback.device", mixers[0].key); + defaultPrefs.put("path.storage", (new File(System.getProperty("user.home"), "Recordings")).toString()); + + defaultPrefs.put("catenation.pre-chapter", "2000"); + defaultPrefs.put("catenation.post-chapter", "2000"); + defaultPrefs.put("catenation.post-sentence", "1000"); + + if (prefs == null) { + prefs = Preferences.userNodeForPackage(AudiobookRecorder.class); + } + } + + public static void savePreferences() { + if (prefs != null) { + try { + prefs.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public static String get(String key) { + if (prefs == null) return null; + String def = defaultPrefs.get(key); + String v = prefs.get(key, def); + return v; + } + + public static Integer getInteger(String key) { + try { + Integer i = Integer.parseInt(get(key)); + return i; + } catch (Exception e) { + } + return 0; + } + + public static void set(String key, String value) { + if (prefs == null) return; + prefs.put(key, value); + } + + void storePreferences() { + set("audio.recording.device", ((KVPair)mixerList.getSelectedItem()).key); + set("audio.recording.channels", ((KVPair)channelList.getSelectedItem()).key); + set("audio.recording.samplerate", ((KVPair)rateList.getSelectedItem()).key); + set("audio.playback.device", ((KVPair)playbackList.getSelectedItem()).key); + set("path.storage", storageFolder.getText()); + set("catenation.pre-chapter", "" + preChapterGap.getValue()); + set("catenation.post-chapter", "" + preChapterGap.getValue()); + set("catenation.post-sentence", "" + preChapterGap.getValue()); + + savePreferences(); + } + + public static AudioFormat getAudioFormat() { + String sampleRate = get("audio.recording.samplerate"); + String channels = get("audio.recording.channels"); + + float sr = 48000f; + int chans = 2; + + try { + sr = Float.parseFloat(sampleRate); + } catch (Exception e) { + sr = 48000f; + } + + try { + chans = Integer.parseInt(channels); + } catch (Exception e) { + chans = 2; + } + + AudioFormat af = new AudioFormat(sr, 16, chans, true, false); + return af; + } + + public static Mixer.Info getMixerByName(String name) { + + Mixer.Info[] mixers = AudioSystem.getMixerInfo(); + for (Mixer.Info info : mixers) { + if (info.getName().equals(name)) { + return info; + } + } + + return mixers[0]; + } + + public static Mixer.Info getRecordingMixer() { + return getMixerByName(get("audio.recording.device")); + } + + public static Mixer.Info getPlaybackMixer() { + return getMixerByName(get("audio.playback.device")); + } +} diff --git a/src/uk/co/majenko/audiobookrecorder/Sentence.java b/src/uk/co/majenko/audiobookrecorder/Sentence.java new file mode 100644 index 0000000..8b773f8 --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/Sentence.java @@ -0,0 +1,335 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.swing.*; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import java.io.*; +import java.nio.file.*; +import javax.swing.tree.*; +import javax.sound.sampled.*; + +public class Sentence extends DefaultMutableTreeNode { + + String text; + String id; + int postGap; + int startOffset = 0; + int endOffset = 0; + + int sampleSize = -1; + + boolean isSilence = false; + + boolean recording; + + TargetDataLine line; + AudioInputStream inputStream; + + Thread recordingThread = null; + + + public Sentence() { + super(""); + id = UUID.randomUUID().toString(); + text = id; + setUserObject(text); + postGap = Options.getInteger("catenation.post-sentence"); + } + + public Sentence(String i, String t) { + super(""); + id = i; + text = t; + setUserObject(text); + postGap = Options.getInteger("catenation.post-sentence"); + } + + public boolean startRecording() { + AudioFormat format = new AudioFormat( + Options.getInteger("audio.recording.samplerate"), + 16, + Options.getInteger("audio.recording.channels"), + true, + false + ); + + if (format == null) { + JOptionPane.showMessageDialog(AudiobookRecorder.window, "Sample format not supported", "Error", JOptionPane.ERROR_MESSAGE); + return false; + } + + Mixer.Info mixer = Options.getRecordingMixer(); + + line = null; + + try { + line = AudioSystem.getTargetDataLine(format, mixer); + } catch (Exception e) { + e.printStackTrace(); + } + + if (line == null) { + JOptionPane.showMessageDialog(AudiobookRecorder.window, "Sample format not supported", "Error", JOptionPane.ERROR_MESSAGE); + return false; + } + + inputStream = new AudioInputStream(line); + + try { + line.open(); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + line.start(); + + File audioFile = new File(AudiobookRecorder.window.getBookFolder(), id + ".wav"); + + recordingThread = new Thread(new Runnable() { + public void run() { + try { + AudioSystem.write(inputStream, AudioFileFormat.Type.WAVE, audioFile); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + recordingThread.setDaemon(true); + + recordingThread.start(); + + recording = true; + return true; + } + + public void stopRecording() { + try { + inputStream.close(); + line.stop(); + line.close(); + } catch (Exception e) { + e.printStackTrace(); + } + recording = false; + + if (!id.equals("room-noise")) { + autoTrimSample(); + } + } + + public void autoTrimSample() { + int[] samples = getAudioData(); + int noiseFloor = AudiobookRecorder.window.getNoiseFloor(); + + isSilence = false; + // Find start + for (int i = 0; i < samples.length; i++) { + startOffset = i; + if (Math.abs(samples[i]) > noiseFloor) { + startOffset --; + if (startOffset < 0) startOffset = 0; + break; + } + } + + if (startOffset >= samples.length-1) { // Failed! Silence? + isSilence = true; + startOffset = 0; + } + + for (int i = samples.length-1; i >= 0; i--) { + endOffset = i; + if (Math.abs(samples[i]) > noiseFloor) { + endOffset ++; + if (endOffset >= samples.length-1) endOffset = samples.length-1; + break; + } + } + if (endOffset <= 0) { + isSilence = true; + endOffset = samples.length-1; + } + + } + + public String getId() { + return id; + } + + public void setText(String t) { + text = t; + } + + public String getText() { + return text; + } + + public File getFile() { + return new File(AudiobookRecorder.window.getBookFolder(), id + ".wav"); + } + + public void editText() { + String t = JOptionPane.showInputDialog(null, "Edit Text", text); + + if (t != null) { + text = t; + } + + } + + public String toString() { + return text; + } + + public boolean isRecording() { + return recording; + } + + public void setUserObject(Object o) { + if (o instanceof String) { + String so = (String)o; + text = so; + } + } + + public int getPostGap() { + return postGap; + } + + public void setPostGap(int g) { + postGap = g; + } + + public void deleteFiles() { + File audioFile = new File(AudiobookRecorder.window.getBookFolder(), id + ".wav"); + if (audioFile.exists()) { + audioFile.delete(); + } + } + + public int[] getAudioData() { + File f = getFile(); + try { + AudioInputStream s = AudioSystem.getAudioInputStream(f); + AudioFormat format = s.getFormat(); + + long len = s.getFrameLength(); + int frameSize = format.getFrameSize(); + int chans = format.getChannels(); + int bytes = frameSize / chans; + + byte[] frame = new byte[frameSize]; + int[] samples = new int[(int)len]; + + if (bytes != 2) return null; + + for (long fno = 0; fno < len; fno++) { + s.read(frame); + int sample = 0; + if (chans == 2) { // Stereo + int left = (frame[1] << 8) | frame[0]; + int right = (frame[3] << 8) | frame[2]; + sample = (left + right) / 2; + } else { + sample = (frame[1] << 8) | frame[0]; + } + samples[(int)fno] = sample; + } + s.close(); + sampleSize = samples.length; + return samples; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public int getStartOffset() { + return startOffset; + } + + public void setStartOffset(int o) { + startOffset = o; + } + + public int getEndOffset() { + return endOffset; + } + + public void setEndOffset(int o) { + endOffset = o; + } + + public int getSampleSize() { + if (sampleSize == -1) { + getAudioData(); + } + return sampleSize; + } + + // Open the audio file as an AudioInputStream and automatically + // skip the first startOffset frames. + public AudioInputStream getAudioStream() { + File f = getFile(); + try { + AudioInputStream s = AudioSystem.getAudioInputStream(f); + AudioFormat format = s.getFormat(); + long len = s.getFrameLength(); + int frameSize = format.getFrameSize(); + + s.skip(frameSize * startOffset); + + return s; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public AudioFormat getAudioFormat() { + File f = getFile(); + try { + AudioInputStream s = AudioSystem.getAudioInputStream(f); + AudioFormat format = s.getFormat(); + return format; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public void play() { + File f = getFile(); + try { + AudioInputStream s = AudioSystem.getAudioInputStream(f); + AudioFormat format = s.getFormat(); + long len = s.getFrameLength(); + int frameSize = format.getFrameSize(); + + int pos = startOffset * frameSize; + + SourceDataLine play = AudioSystem.getSourceDataLine(format, Options.getPlaybackMixer()); + play.open(format); + + play.start(); + + byte[] buffer = new byte[1024]; + + s.skip(pos); + + while (pos < endOffset * frameSize) { + int nr = s.read(buffer); + pos += nr; + + play.write(buffer, 0, nr); + }; + play.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/uk/co/majenko/audiobookrecorder/SteppedNumericSpinnerModel.java b/src/uk/co/majenko/audiobookrecorder/SteppedNumericSpinnerModel.java new file mode 100644 index 0000000..d80fe37 --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/SteppedNumericSpinnerModel.java @@ -0,0 +1,78 @@ +package uk.co.majenko.audiobookrecorder; + +import java.util.*; +import javax.swing.*; +import javax.swing.event.*; + +public class SteppedNumericSpinnerModel implements SpinnerModel { + int min; + int max; + int step; + int value; + + ArrayList listeners; + + public SteppedNumericSpinnerModel(int amin, int amax, int astep, int avalue) { + min = amin; + max = amax; + step = astep; + value = avalue; + + listeners = new ArrayList(); + } + + public Object getNextValue() { + Integer v = value; + v += step; + if (v > max) return null; + return v; + } + + public Object getPreviousValue() { + Integer v = value; + v -= step; + if (v < min) return null; + return v; + } + + public Object getValue() { + Integer v = value; + return v; + } + + public void setValue(Object v) { + if (v instanceof Integer) { + Integer i = (Integer)v; + value = i; + ChangeEvent e = new ChangeEvent(this); + for (ChangeListener l : listeners) { + l.stateChanged(e); + } + } + } + + public void addChangeListener(ChangeListener l) { + listeners.add(l); + } + + public void removeChangeListener(ChangeListener l) { + listeners.remove(l); + } + + public void setMinimum(int i) { + min = i; + } + + public void setMaximum(int i) { + max = i; + } + + public int getMaximum() { + return max; + } + + public int getMinimum() { + return min; + } +} + diff --git a/src/uk/co/majenko/audiobookrecorder/Waveform.java b/src/uk/co/majenko/audiobookrecorder/Waveform.java new file mode 100644 index 0000000..b592156 --- /dev/null +++ b/src/uk/co/majenko/audiobookrecorder/Waveform.java @@ -0,0 +1,118 @@ +package uk.co.majenko.audiobookrecorder; + +import javax.swing.*; +import java.awt.*; +import java.util.*; +import java.io.*; +import javax.sound.sampled.*; + +public class Waveform extends JPanel { + + int[] samples = null; + int leftMarker = 0; + int rightMarker = 0; + + public Waveform() { + super(); + } + + public void paintComponent(Graphics g) { + + Dimension size = getSize(); + + int w = size.width; + int h = size.height; + + g.setColor(Color.BLACK); + g.fillRect(0, 0, w, h); + + g.setColor(Color.GREEN); + g.drawRect(0, 0, w, h); + g.drawLine(0, h/2, w, h/2); + + g.setColor(new Color(0, 150, 0)); + for (int x = 0; x < w; x += w/10) { + g.drawLine(x, 0, x, h); + } + + for (int x = 0; x < w; x += 4) { + g.drawLine(x, h/4, x, h/4); + g.drawLine(x, h/4*3, x, h/4*3); + } + + int scale = 32768/(h/2); + + if (samples != null) { + + int num = samples.length; + int step = num / w; + + for (int n = 0; n < w; n++) { + int hcnt = 0; + long have = 0; + int hmax = 0; + + int lcnt = 0; + int lave = 0; + int lmax = 0; + + for (int o = 0; o < step; o++) { + int sample = samples[(n * step) + o]; + if (sample >= 0) { + have += sample; + hcnt++; + if (sample > hmax) hmax = sample; + } else { + sample = Math.abs(sample); + lave += sample; + lcnt++; + if (sample > lmax) lmax = sample; + } + } + + if (hcnt > 0) have /= hcnt; + if (lcnt > 0) lave /= lcnt; + + hmax /= scale; + lmax /= scale; + have /= scale; + lave /= scale; + + g.setColor(new Color(0, 0, 100)); + g.drawLine(n, h/2 + lmax, n, h/2 - hmax); + g.setColor(new Color(0, 0, 200)); + g.drawLine(n, h/2 + (int)lave, n, h/2 - (int)have); + } + + g.setColor(new Color(255, 0, 0)); + g.drawLine(leftMarker/step, 0, leftMarker/step, h); + g.drawLine(rightMarker/step, 0, rightMarker/step, h); + } + } + + public void setMarkers(int l, int r) { + leftMarker = l; + rightMarker = r; + repaint(); + } + + public void setLeftMarker(int l) { + leftMarker = l; + repaint(); + } + + public void setRightMarker(int r) { + rightMarker = r; + repaint(); + } + + public void clearData() { + samples = null; + repaint(); + } + + public void setData(int[] s) { + samples = s; + repaint(); + } +}