From 186fa335a66643bec9bc2d465b1046bd33356cd0 Mon Sep 17 00:00:00 2001 From: TrekkiesUnite118 Date: Tue, 1 Aug 2023 02:56:44 -0400 Subject: [PATCH] Updates to allow for extracting audio as well as being able to mux from PCM and WAV files. --- SegaSaturnFilmMuxer/src/gui/FILMMuxer.java | 224 ++++++++++++--- .../src/sega/film/FILMUtility.java | 263 ++++++++++++++++++ 2 files changed, 456 insertions(+), 31 deletions(-) diff --git a/SegaSaturnFilmMuxer/src/gui/FILMMuxer.java b/SegaSaturnFilmMuxer/src/gui/FILMMuxer.java index 87d19d5..19a2fd5 100644 --- a/SegaSaturnFilmMuxer/src/gui/FILMMuxer.java +++ b/SegaSaturnFilmMuxer/src/gui/FILMMuxer.java @@ -1,8 +1,10 @@ package gui; import java.awt.Button; +import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Font; +import java.awt.GridLayout; import java.awt.Label; import java.awt.TextField; import java.awt.event.ActionEvent; @@ -11,6 +13,7 @@ import java.io.IOException; import javax.swing.JFileChooser; import javax.swing.JFrame; +import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; @@ -18,14 +21,14 @@ import sega.film.FILMUtility; import sega.film.FILMfile; +import javax.swing.JTabbedPane; +import javax.swing.JPanel; +import javax.swing.JCheckBox; public class FILMMuxer { private JFrame frame; - private FILMfile file1 = new FILMfile(); - private FILMfile file2 = new FILMfile(); - /** * Launch the application. */ @@ -56,9 +59,9 @@ public FILMMuxer() { */ private void initialize() { frame = new JFrame(); - frame.setBounds(100, 100, 550, 250); + frame.setBounds(100, 100, 560, 360); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.setTitle("Sega Saturn FILM Muxer"); + frame.setTitle("Sega Saturn FILM Tools"); JMenuBar menuBar = new JMenuBar(); frame.setJMenuBar(menuBar); @@ -73,12 +76,23 @@ public void actionPerformed(ActionEvent e) { } }); mnFile.add(mntmQuit); - frame.getContentPane().setLayout(null); + + JTabbedPane tabbedPane = new JTabbedPane(); + tabbedPane.setBounds(0, 183, 532, -183); + frame.getContentPane().setLayout(new GridLayout(1, 1)); + + JPanel panel1 = new JPanel(false); + JLabel filler = new JLabel("Muxer"); + filler.setHorizontalAlignment(JLabel.CENTER); + filler.setFont(new Font("Arial", Font.PLAIN, 11)); + panel1.setLayout(null); + panel1.add(filler); + panel1.setPreferredSize(new Dimension(550, 225)); TextField parseAudioInputFileDirField = new TextField(); parseAudioInputFileDirField.setBounds(157, 41, 343, 22); - frame.getContentPane().add(parseAudioInputFileDirField); + panel1.add(parseAudioInputFileDirField); Button parseAudioInputDirSearchButton = new Button("..."); parseAudioInputDirSearchButton.addActionListener(new ActionListener() { @@ -94,16 +108,16 @@ public void actionPerformed(ActionEvent arg0) { } }); parseAudioInputDirSearchButton.setBounds(502, 41, 22, 22); - frame.getContentPane().add(parseAudioInputDirSearchButton); + panel1.add(parseAudioInputDirSearchButton); - Label inputAudioParseDirLabel = new Label("Audio Source FILM / ADX File"); + Label inputAudioParseDirLabel = new Label("Audio Source File"); inputAudioParseDirLabel.setFont(new Font("Arial", Font.PLAIN, 11)); inputAudioParseDirLabel.setBounds(10, 41, 148, 22); - frame.getContentPane().add(inputAudioParseDirLabel); + panel1.add(inputAudioParseDirLabel); TextField parseVideoInputFileDirField = new TextField(); parseVideoInputFileDirField.setBounds(157, 69, 343, 22); - frame.getContentPane().add(parseVideoInputFileDirField); + panel1.add(parseVideoInputFileDirField); Button parseVideoInputDirSearchButton = new Button("..."); parseVideoInputDirSearchButton.addActionListener(new ActionListener() { @@ -119,26 +133,26 @@ public void actionPerformed(ActionEvent arg0) { } }); parseVideoInputDirSearchButton.setBounds(502, 69, 22, 22); - frame.getContentPane().add(parseVideoInputDirSearchButton); + panel1.add(parseVideoInputDirSearchButton); Label inputVideoParseDirLabel = new Label("Video Source FILM File"); inputVideoParseDirLabel.setFont(new Font("Arial", Font.PLAIN, 11)); inputVideoParseDirLabel.setBounds(10, 69, 125, 22); - frame.getContentPane().add(inputVideoParseDirLabel); + panel1.add(inputVideoParseDirLabel); Label parserTitle = new Label("Sega Saturn FILM Muxer"); parserTitle.setFont(new Font("Arial", Font.BOLD, 14)); parserTitle.setBounds(10, 13, 227, 22); - frame.getContentPane().add(parserTitle); + panel1.add(parserTitle); Label outputParseDirLabel = new Label("Output File Directory"); outputParseDirLabel.setFont(new Font("Arial", Font.PLAIN, 11)); outputParseDirLabel.setBounds(10, 97, 105, 22); - frame.getContentPane().add(outputParseDirLabel); + panel1.add(outputParseDirLabel); TextField parseOutputFileDirField = new TextField(); parseOutputFileDirField.setBounds(157, 97, 343, 22); - frame.getContentPane().add(parseOutputFileDirField); + panel1.add(parseOutputFileDirField); Button parseOutputDirSearchButton = new Button("..."); parseOutputDirSearchButton.addActionListener(new ActionListener() { @@ -154,36 +168,76 @@ public void actionPerformed(ActionEvent e) { } }); parseOutputDirSearchButton.setBounds(502, 97, 22, 22); - frame.getContentPane().add(parseOutputDirSearchButton); + panel1.add(parseOutputDirSearchButton); + + JCheckBox chckbxNewCheckBox_1 = new JCheckBox("Saturn Format PCM"); + chckbxNewCheckBox_1.setBounds(10, 144, 160, 23); + panel1.add(chckbxNewCheckBox_1); + + JCheckBox chckbxNewCheckBox_1_1 = new JCheckBox("Big Endian"); + chckbxNewCheckBox_1_1.setBounds(10, 170, 160, 23); + panel1.add(chckbxNewCheckBox_1_1); + Button parseButton = new Button("Mux Audio and Video"); parseButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { try { + + FILMfile file1 = new FILMfile(); + FILMfile file2 = new FILMfile(); - if(!parseAudioInputFileDirField.getText().endsWith(".ADX") && !parseAudioInputFileDirField.getText().endsWith(".adx")) { + if(parseAudioInputFileDirField.getText().endsWith(".ADX") || parseAudioInputFileDirField.getText().endsWith(".adx")) { - FILMUtility.parse(parseAudioInputFileDirField.getText(), file1); FILMUtility.parse(parseVideoInputFileDirField.getText(), file2); - - System.out.println("Attempting to ReMux files..."); + System.out.println("Attempting to ReMux ADX files..."); parseButton.setEnabled(false); - FILMfile newFilm = FILMUtility.swapAudio(file1, file2); + FILMfile newFilm = FILMUtility.swapAudioFromADXFile(parseAudioInputFileDirField.getText(), file2); - File f = new File(parseAudioInputFileDirField.getText()); + File f = new File(parseVideoInputFileDirField.getText()); FILMUtility.reconstruct(newFilm, parseOutputFileDirField.getText() + "\\NEW_" + f.getName()); parseButton.setEnabled(true); - } else { + + } else if(parseAudioInputFileDirField.getText().endsWith(".PCM") || parseAudioInputFileDirField.getText().endsWith(".pcm")) { + FILMUtility.parse(parseVideoInputFileDirField.getText(), file2); + System.out.println("Attempting to ReMux with PCM files..."); + parseButton.setEnabled(false); + boolean satFormat = chckbxNewCheckBox_1.isSelected(); + boolean bigEndian = chckbxNewCheckBox_1_1.isSelected(); + FILMfile newFilm = FILMUtility.swapAudioFromPCMFile(parseAudioInputFileDirField.getText(), file2, satFormat, bigEndian); + + File f = new File(parseVideoInputFileDirField.getText()); + + FILMUtility.reconstruct(newFilm, parseOutputFileDirField.getText() + "\\NEW_" + f.getName()); + parseButton.setEnabled(true); + + } else if(parseAudioInputFileDirField.getText().endsWith(".WAV") || parseAudioInputFileDirField.getText().endsWith(".wav")) { FILMUtility.parse(parseVideoInputFileDirField.getText(), file2); - System.out.println("Attempting to ReMux files..."); + System.out.println("Attempting to ReMux with WAV files..."); parseButton.setEnabled(false); - FILMfile newFilm = FILMUtility.swapAudioFromADXFile(parseAudioInputFileDirField.getText(), file2); + + boolean satFormat = chckbxNewCheckBox_1.isSelected(); + boolean bigEndian = chckbxNewCheckBox_1_1.isSelected(); + FILMfile newFilm = FILMUtility.swapAudioFromWAVFile(parseAudioInputFileDirField.getText(), file2); File f = new File(parseVideoInputFileDirField.getText()); + FILMUtility.reconstruct(newFilm, parseOutputFileDirField.getText() + "\\NEW_" + f.getName()); + parseButton.setEnabled(true); + + } else { + FILMUtility.parse(parseAudioInputFileDirField.getText(), file1); + FILMUtility.parse(parseVideoInputFileDirField.getText(), file2); + + System.out.println("Attempting to ReMux files..."); + parseButton.setEnabled(false); + FILMfile newFilm = FILMUtility.swapAudio(file1, file2); + + File f = new File(parseAudioInputFileDirField.getText()); + FILMUtility.reconstruct(newFilm, parseOutputFileDirField.getText() + "\\NEW_" + f.getName()); parseButton.setEnabled(true); } @@ -192,13 +246,121 @@ public void actionPerformed(ActionEvent e) { } catch (IOException e1) { e1.printStackTrace(); } - - + } }); parseButton.setFont(new Font("Arial", Font.PLAIN, 20)); - parseButton.setBounds(10, 125, 514, 57); - frame.getContentPane().add(parseButton); - } + parseButton.setBounds(10, 204, 514, 57); + panel1.add(parseButton); + + JPanel panel2 = new JPanel(false); + JLabel filler2 = new JLabel("Audio Extractor"); + filler2.setHorizontalAlignment(JLabel.CENTER); + filler2.setFont(new Font("Arial", Font.PLAIN, 11)); + panel2.setLayout(null); + panel2.add(filler2); + panel2.setPreferredSize(new Dimension(550, 225)); + + + + TextField extractAudioInputFileDirField = new TextField(); + extractAudioInputFileDirField.setBounds(157, 41, 343, 22); + panel2.add(extractAudioInputFileDirField); + + Button extractAudioInputDirSearchButton = new Button("..."); + extractAudioInputDirSearchButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + JFileChooser chooser = new JFileChooser(); + chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + int returnVal = chooser.showOpenDialog(null); + if(returnVal == JFileChooser.APPROVE_OPTION) { + System.out.println("You chose to open this file: " + + chooser.getSelectedFile().getName()); + extractAudioInputFileDirField.setText(chooser.getSelectedFile().getAbsolutePath()); + } + } + }); + extractAudioInputDirSearchButton.setBounds(502, 41, 22, 22); + panel2.add(extractAudioInputDirSearchButton); + + Label extractAudioParseDirLabel = new Label("Audio Source FILM File"); + extractAudioParseDirLabel.setFont(new Font("Arial", Font.PLAIN, 11)); + extractAudioParseDirLabel.setBounds(10, 41, 148, 22); + panel2.add(extractAudioParseDirLabel); + + + Label extractorTitle = new Label("Sega Saturn FILM Audio Extractor"); + extractorTitle.setFont(new Font("Arial", Font.BOLD, 14)); + extractorTitle.setBounds(10, 13, 249, 22); + panel2.add(extractorTitle); + + Label outputExtractDirLabel = new Label("Output File Directory"); + outputExtractDirLabel.setFont(new Font("Arial", Font.PLAIN, 11)); + outputExtractDirLabel.setBounds(10, 69, 125, 22); + panel2.add(outputExtractDirLabel); + + TextField extractOutputFileDirField = new TextField(); + extractOutputFileDirField.setBounds(157, 69, 343, 22); + panel2.add(extractOutputFileDirField); + + Button extractOutputDirSearchButton = new Button("..."); + extractOutputDirSearchButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + JFileChooser chooser = new JFileChooser(); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + int returnVal = chooser.showOpenDialog(null); + if(returnVal == JFileChooser.APPROVE_OPTION) { + System.out.println("You chose to open this file: " + + chooser.getSelectedFile().getName()); + extractOutputFileDirField.setText(chooser.getSelectedFile().getAbsolutePath()); + } + } + }); + extractOutputDirSearchButton.setBounds(502, 69, 22, 22); + panel2.add(extractOutputDirSearchButton); + JCheckBox chckbxNewCheckBox = new JCheckBox("WAV Output"); + chckbxNewCheckBox.setBounds(10, 96, 97, 23); + panel2.add(chckbxNewCheckBox); + + Button extractButton = new Button("Extract Audio"); + extractButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + + try { + + FILMfile file1 = new FILMfile(); + + FILMUtility.parse(extractAudioInputFileDirField.getText(), file1); + + System.out.println("Attempting to Extract audio..."); + extractButton.setEnabled(false); + boolean waveOut = chckbxNewCheckBox.isSelected(); + File f = new File(extractAudioInputFileDirField.getText()); + FILMUtility.extractAudio(file1, extractOutputFileDirField.getText() + "\\NEW_" + f.getName(), waveOut); + + extractButton.setEnabled(true); + + } catch (IOException e1) { + e1.printStackTrace(); + } + + } + }); + extractButton.setFont(new Font("Arial", Font.PLAIN, 20)); + extractButton.setBounds(10, 204, 514, 57); + panel2.add(extractButton); + + // tabbedPane.setEnabledAt(1, true); + tabbedPane.addTab("Muxer", panel1); + + + tabbedPane.addTab("Extractor", panel2); + + + frame.getContentPane().add(tabbedPane); + + frame.setVisible(true); + + } } diff --git a/SegaSaturnFilmMuxer/src/sega/film/FILMUtility.java b/SegaSaturnFilmMuxer/src/sega/film/FILMUtility.java index 696a552..f8ec243 100644 --- a/SegaSaturnFilmMuxer/src/sega/film/FILMUtility.java +++ b/SegaSaturnFilmMuxer/src/sega/film/FILMUtility.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -96,6 +97,135 @@ public static void parse(String inputFile, FILMfile file) throws IOException { } } + public static void extractAudio(FILMfile source, String outputFilePath, boolean waveOut) throws IOException { + List sourceStabs = source.getHeader().getStab().getEntries(); + + boolean isADX = false; + if(source.getHeader().getCompression() == 2) { + isADX = true; + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + for(int i = 0; i < sourceStabs.size(); i++) { + if(isAudioChunk(sourceStabs.get(i))) { + + if(!isADX && source.getHeader().getAudioChannels() == 2) { + int length = source.getChunks().get(i).length; + int halfway = source.getChunks().get(i).length / 2; + byte[] leftData = Arrays.copyOfRange(source.getChunks().get(i), 0, halfway); + byte[] rightData = Arrays.copyOfRange(source.getChunks().get(i), halfway, length); + + int iter = 0; + if(source.getHeader().getAudioResolution() == 16) { + while(iter < leftData.length) { + out.write(leftData[iter]); + out.write(leftData[iter+1]); + + out.write(rightData[iter]); + out.write(rightData[iter+1]); + + iter+=2; + } + } else { + if(source.getHeader().getAudioResolution() == 8) { + while(iter < leftData.length) { + out.write(leftData[iter]); + out.write(rightData[iter]); + iter++; + } + } + } + + + } else { + out.write(source.getChunks().get(i)); + } + + } + } + + if(!isADX && source.getHeader().getAudioResolution() == 16 && waveOut) { + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + + byte[] fileBytes = out.toByteArray(); + ByteBuffer bb = ByteBuffer.allocate(4); + bb.order(ByteOrder.LITTLE_ENDIAN); + outBuffer.write(new String("RIFF").getBytes()); + bb.putInt(0, fileBytes.length + 44); + outBuffer.write(bb.array()); + outBuffer.write(new String("WAVE").getBytes()); + outBuffer.write(new String("fmt ").getBytes()); + bb.putInt(0, 16); + outBuffer.write(bb.array()); + bb = ByteBuffer.allocate(2); + bb.order(ByteOrder.LITTLE_ENDIAN); + bb.putShort(0, (short) 1); + outBuffer.write(bb.array()); + bb.putShort(0, (short) source.getHeader().getAudioChannels()); + outBuffer.write(bb.array()); + bb = ByteBuffer.allocate(4); + bb.order(ByteOrder.LITTLE_ENDIAN); + bb.putInt(0, source.getHeader().getSampleRate()); + outBuffer.write(bb.array()); + + int sampleRate = source.getHeader().getSampleRate(); + int resolution = source.getHeader().getAudioResolution(); + int channels = source.getHeader().getAudioChannels(); + + int waveHeaderValue_1 = ((sampleRate * resolution * channels) / 8 ); + short waveHeaderValue_2 = (short) ((resolution * channels) / 8 ); + + bb.putInt(0, waveHeaderValue_1); + outBuffer.write(bb.array()); + + + bb = ByteBuffer.allocate(2); + bb.order(ByteOrder.LITTLE_ENDIAN); + bb.putShort(0, waveHeaderValue_2); + outBuffer.write(bb.array()); + bb.putShort(0, (short) resolution); + outBuffer.write(bb.array()); + outBuffer.write(new String("data").getBytes()); + bb = ByteBuffer.allocate(4); + bb.order(ByteOrder.LITTLE_ENDIAN); + bb.putInt(0, fileBytes.length); + outBuffer.write(bb.array()); + + outBuffer.write(swapByteOrder(fileBytes)); + + + out = outBuffer; + } + + String[] pieces = outputFilePath.split("\\."); + + if(pieces.length > 1) { + if(isADX) { + pieces[pieces.length - 1] = "ADX"; + } else if(waveOut && source.header.getAudioResolution() == 16){ + pieces[pieces.length - 1] = "WAV"; + } else { + pieces[pieces.length - 1] = "PCM"; + } + + String outputPath = pieces[0]; + + for (int i = 1; i < pieces.length; i++) { + outputPath = outputPath.concat(".").concat(pieces[i]); + } + + outputFilePath = outputPath; + } + + Path path = Paths.get(outputFilePath); + try { + Files.write(path, out.toByteArray()); + } catch (IOException e) { + log.log(Level.SEVERE, "Caught IOException attempting to write bytes to file.", e); + e.printStackTrace(); + } + } + public static FILMfile swapAudioFromADXFile(String adxFilePath, FILMfile dest) throws IOException { if(dest.getHeader().getCompression() == 2) { @@ -131,6 +261,107 @@ public static FILMfile swapAudioFromADXFile(String adxFilePath, FILMfile dest) t } +public static FILMfile swapAudioFromWAVFile(String wavFilePath, FILMfile dest) throws IOException { + if(dest.getHeader().getCompression() == 0 && dest.header.getAudioResolution() == 16 ) { + File f = new File(wavFilePath); + byte[] wavBytes = Files.readAllBytes(f.toPath()); + + byte[] PCMBytes = Arrays.copyOfRange(wavBytes, 44, wavBytes.length); + + return swapPCMData(dest, PCMBytes, false, false); + + }else { + //Can't use 8 bit WAV file, Saturn doesn't support unsigned 8 bit for Cinepak, must but be signed. + //WAV can't hold 8-bit signed PCM. + return dest; + } +} + + public static FILMfile swapAudioFromPCMFile(String pcmFilePath, FILMfile dest, boolean satFormat, boolean bigEndian) throws IOException { + + if(dest.getHeader().getCompression() == 0) { + File f = new File(pcmFilePath); + + byte[] PCMBytes = Files.readAllBytes(f.toPath()); + + return swapPCMData(dest, PCMBytes, satFormat, bigEndian); + } else { + //file doesn't use PCM, abort. + return dest; + } + + } + + +private static FILMfile swapPCMData(FILMfile dest, byte[] PCMBytes, boolean satFormat, boolean bigEndian) throws IOException { + List destStabs = dest.getHeader().getStab().getEntries(); + + List audioStabs = new ArrayList<>(); + + int pcmOffset = 0; + List newChunks = new ArrayList<>(); + for(int i = 0; i < destStabs.size(); i++) { + if(isAudioChunk(destStabs.get(i))) { + audioStabs.add(i); + + byte[] rawAudio = Arrays.copyOfRange(PCMBytes, pcmOffset, pcmOffset + destStabs.get(i).getLength()); + + if(!satFormat) { + + if(dest.header.getAudioResolution() == 16 && !bigEndian) { + rawAudio = swapByteOrder(rawAudio); + } + + if(dest.header.getAudioChannels() == 2) { + + ByteArrayOutputStream merge = new ByteArrayOutputStream(); + ByteArrayOutputStream left = new ByteArrayOutputStream(); + ByteArrayOutputStream right = new ByteArrayOutputStream(); + + int length = rawAudio.length; + int half = length / 2; + + int iter = 0; + if(dest.header.getAudioResolution() == 16) { + while (iter < length) { + left.write(rawAudio[iter]); + left.write(rawAudio[iter + 1]); + + right.write(rawAudio[iter + 2]); + right.write(rawAudio[iter + 3]); + iter +=4; + } + } else { + while (iter < length) { + left.write(rawAudio[iter]); + right.write(rawAudio[iter + 1]); + iter+=2; + } + + } + + merge.write(left.toByteArray()); + merge.write(right.toByteArray()); + + rawAudio = merge.toByteArray(); + + } + } + + newChunks.add(rawAudio); + + pcmOffset += destStabs.get(i).getLength(); + + } else { + newChunks.add(dest.getChunks().get(i)); + } + } + + dest.setChunks(newChunks); + + return dest; +} + public static FILMfile swapAudio(FILMfile source, FILMfile dest) throws IOException { List sourceStabs = source.getHeader().getStab().getEntries(); @@ -320,4 +551,36 @@ private static String bytesToHex(byte[] bytes) { } return new String(hexChars); } + + private static byte[] swapByteOrder(byte[] value) { + final int length = value.length; + byte[] res = new byte[length]; + int i = 0; + while(i < length) { + if(i+1 >= length) { + res[i] = value[i]; + }else { + res[i] = value[i+1]; + res[i+1] = value[i]; + } + + i += 2; + } + return res; + } + + private static byte[] convertSignedToUnsigned(byte[] value) { + final int length = value.length; + byte[] res = new byte[length]; + int i = 0; + while(i < length) { + + int val = value[i] & 0x000000FF; + res[i] = (byte) val; + i++; + } + + return res; + } + }