├── SegaSaturnFilmMuxer
├── .gitignore
├── .project
├── .classpath
├── pom.xml
└── src
│ ├── sega
│ ├── film
│ │ ├── FILMfile.java
│ │ ├── STABEntry.java
│ │ ├── STABChunk.java
│ │ ├── FILMHeader.java
│ │ └── FILMUtility.java
│ └── cvid
│ │ ├── CvidChunk.java
│ │ ├── CvidStrip.java
│ │ ├── CvidHeader.java
│ │ ├── CvidDataProcessor.java
│ │ └── MovieToSaturn.java
│ └── gui
│ └── FILMMuxer.java
└── README.md
/SegaSaturnFilmMuxer/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/
2 | /target/
3 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | SegaSaturnFilmMuxer
4 |
5 |
6 |
7 |
8 |
9 | org.eclipse.jdt.core.javabuilder
10 |
11 |
12 |
13 |
14 | org.eclipse.m2e.core.maven2Builder
15 |
16 |
17 |
18 |
19 |
20 | org.eclipse.m2e.core.maven2Nature
21 | org.eclipse.jdt.core.javanature
22 |
23 |
24 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 | SegaSaturnFilmMuxer
4 | SegaSaturnFilmMuxer
5 | 0.0.1-SNAPSHOT
6 |
7 | src
8 |
9 |
10 | maven-compiler-plugin
11 | 3.8.1
12 |
13 | 1.8
14 | 1.8
15 |
16 |
17 |
18 |
19 |
20 |
21 | org.jcodec
22 | jcodec
23 | 0.2.5
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/src/sega/film/FILMfile.java:
--------------------------------------------------------------------------------
1 | package sega.film;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | public class FILMfile {
7 |
8 | FILMHeader header = new FILMHeader();
9 | STABChunk stab = new STABChunk();
10 | List chunks = new ArrayList<>();
11 | /**
12 | * The getter for header.
13 | *
14 | * @return the header.
15 | */
16 | public FILMHeader getHeader() {
17 | return header;
18 | }
19 | /**
20 | * The setter for header.
21 | *
22 | * @param header the header to set.
23 | */
24 | public void setHeader(FILMHeader header) {
25 | this.header = header;
26 | }
27 | /**
28 | * The getter for stab.
29 | *
30 | * @return the stab.
31 | */
32 | public STABChunk getStab() {
33 | return stab;
34 | }
35 | /**
36 | * The setter for stab.
37 | *
38 | * @param stab the stab to set.
39 | */
40 | public void setStab(STABChunk stab) {
41 | this.stab = stab;
42 | }
43 | /**
44 | * The getter for chunks.
45 | *
46 | * @return the chunks.
47 | */
48 | public List getChunks() {
49 | return chunks;
50 | }
51 | /**
52 | * The setter for chunks.
53 | *
54 | * @param chunks the chunks to set.
55 | */
56 | public void setChunks(List chunks) {
57 | this.chunks = chunks;
58 | }
59 |
60 |
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/src/sega/cvid/CvidChunk.java:
--------------------------------------------------------------------------------
1 | package sega.cvid;
2 |
3 | import java.nio.ByteBuffer;
4 |
5 | public class CvidChunk {
6 |
7 | short chunkType;
8 | short chunkSize;
9 | byte[] data;
10 |
11 | public byte[] toByteArray() {
12 |
13 | ByteBuffer bb = ByteBuffer.allocate(chunkSize);
14 | bb.putShort(chunkType);
15 | bb.putShort(chunkSize);
16 | bb.put(data);
17 |
18 | return bb.array();
19 | }
20 |
21 | /**
22 | * The getter for chunkType.
23 | *
24 | * @return the chunkType.
25 | */
26 | public short getChunkType() {
27 | return chunkType;
28 | }
29 | /**
30 | * The setter for chunkType.
31 | *
32 | * @param chunkType the chunkType to set.
33 | */
34 | public void setChunkType(short chunkType) {
35 | this.chunkType = chunkType;
36 | }
37 | /**
38 | * The getter for chunkSize.
39 | *
40 | * @return the chunkSize.
41 | */
42 | public short getChunkSize() {
43 | return chunkSize;
44 | }
45 | /**
46 | * The setter for chunkSize.
47 | *
48 | * @param chunkSize the chunkSize to set.
49 | */
50 | public void setChunkSize(short chunkSize) {
51 | this.chunkSize = chunkSize;
52 | }
53 | /**
54 | * The getter for data.
55 | *
56 | * @return the data.
57 | */
58 | public byte[] getData() {
59 | return data;
60 | }
61 | /**
62 | * The setter for data.
63 | *
64 | * @param data the data to set.
65 | */
66 | public void setData(byte[] data) {
67 | this.data = data;
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/src/sega/film/STABEntry.java:
--------------------------------------------------------------------------------
1 | package sega.film;
2 |
3 | public class STABEntry {
4 |
5 |
6 | private int offset;
7 | private int length;
8 | private byte[] sampleInfo1 = new byte[4];
9 | private byte[] sampleInfo2 = new byte[4];
10 |
11 | /**
12 | * The getter for offset.
13 | *
14 | * @return the offset.
15 | */
16 | public int getOffset() {
17 | return offset;
18 | }
19 | /**
20 | * The setter for offset.
21 | *
22 | * @param offset the offset to set.
23 | */
24 | public void setOffset(int offset) {
25 | this.offset = offset;
26 | }
27 | /**
28 | * The getter for length.
29 | *
30 | * @return the length.
31 | */
32 | public int getLength() {
33 | return length;
34 | }
35 | /**
36 | * The setter for length.
37 | *
38 | * @param length the length to set.
39 | */
40 | public void setLength(int length) {
41 | this.length = length;
42 | }
43 | /**
44 | * The getter for sampleInfo1.
45 | *
46 | * @return the sampleInfo1.
47 | */
48 | public byte[] getSampleInfo1() {
49 | return sampleInfo1;
50 | }
51 | /**
52 | * The setter for sampleInfo1.
53 | *
54 | * @param sampleInfo1 the sampleInfo1 to set.
55 | */
56 | public void setSampleInfo1(byte[] sampleInfo1) {
57 | this.sampleInfo1 = sampleInfo1;
58 | }
59 | /**
60 | * The getter for sampleInfo2.
61 | *
62 | * @return the sampleInfo2.
63 | */
64 | public byte[] getSampleInfo2() {
65 | return sampleInfo2;
66 | }
67 | /**
68 | * The setter for sampleInfo2.
69 | *
70 | * @param sampleInfo2 the sampleInfo2 to set.
71 | */
72 | public void setSampleInfo2(byte[] sampleInfo2) {
73 | this.sampleInfo2 = sampleInfo2;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/src/sega/film/STABChunk.java:
--------------------------------------------------------------------------------
1 | package sega.film;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | public class STABChunk {
7 |
8 | private String stabString = "STAB";
9 | private int length;
10 | private int framerateFrequency;
11 | private int numOfEntries;
12 | List entries = new ArrayList<>();
13 |
14 |
15 | /**
16 | * The getter for stabString.
17 | *
18 | * @return the stabString.
19 | */
20 | public String getStabString() {
21 | return stabString;
22 | }
23 | /**
24 | * The setter for stabString.
25 | *
26 | * @param stabString the stabString to set.
27 | */
28 | public void setStabString(String stabString) {
29 | this.stabString = stabString;
30 | }
31 | /**
32 | * The getter for length.
33 | *
34 | * @return the length.
35 | */
36 | public int getLength() {
37 | return length;
38 | }
39 | /**
40 | * The setter for length.
41 | *
42 | * @param length the length to set.
43 | */
44 | public void setLength(int length) {
45 | this.length = length;
46 | }
47 | /**
48 | * The getter for framerateFrequency.
49 | *
50 | * @return the framerateFrequency.
51 | */
52 | public int getFramerateFrequency() {
53 | return framerateFrequency;
54 | }
55 | /**
56 | * The setter for framerateFrequency.
57 | *
58 | * @param framerateFrequency the framerateFrequency to set.
59 | */
60 | public void setFramerateFrequency(int framerateFrequency) {
61 | this.framerateFrequency = framerateFrequency;
62 | }
63 | /**
64 | * The getter for numOfEntries.
65 | *
66 | * @return the numOfEntries.
67 | */
68 | public int getNumOfEntries() {
69 | return numOfEntries;
70 | }
71 | /**
72 | * The setter for numOfEntries.
73 | *
74 | * @param numOfEntries the numOfEntries to set.
75 | */
76 | public void setNumOfEntries(int numOfEntries) {
77 | this.numOfEntries = numOfEntries;
78 | }
79 | /**
80 | * The getter for entries.
81 | *
82 | * @return the entries.
83 | */
84 | public List getEntries() {
85 | return entries;
86 | }
87 | /**
88 | * The setter for entries.
89 | *
90 | * @param entries the entries to set.
91 | */
92 | public void setEntries(List entries) {
93 | this.entries = entries;
94 | }
95 |
96 | public void addEntry(STABEntry entry ) {
97 | this.entries.add(entry);
98 | }
99 |
100 |
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/src/sega/cvid/CvidStrip.java:
--------------------------------------------------------------------------------
1 | package sega.cvid;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.util.ArrayList;
5 | import java.util.Arrays;
6 | import java.util.List;
7 |
8 | public class CvidStrip {
9 |
10 | byte flags;
11 | int size;
12 | int padding;
13 | short height;
14 | short width;
15 | List chunks = new ArrayList<>();
16 |
17 |
18 | public byte[] toByteArray() {
19 |
20 | ByteBuffer bb = ByteBuffer.allocate(size);
21 | bb.put(flags);
22 |
23 | ByteBuffer sizeBuffer = ByteBuffer.allocate(4);
24 | sizeBuffer.putInt(size);
25 | byte[] tempSize = sizeBuffer.array();
26 |
27 | byte[] sizeArray = Arrays.copyOfRange(tempSize, 1, 4);
28 | bb.put(sizeArray);
29 | bb.putInt(padding);
30 | bb.putShort(height);
31 | bb.putShort(width);
32 |
33 | for(int i = 0; i < chunks.size(); i++) {
34 | bb.put(chunks.get(i).toByteArray());
35 | }
36 |
37 | return bb.array();
38 | }
39 |
40 |
41 | /**
42 | * The getter for flags.
43 | *
44 | * @return the flags.
45 | */
46 | public byte getFlags() {
47 | return flags;
48 | }
49 | /**
50 | * The setter for flags.
51 | *
52 | * @param flags the flags to set.
53 | */
54 | public void setFlags(byte flags) {
55 | this.flags = flags;
56 | }
57 | /**
58 | * The getter for size.
59 | *
60 | * @return the size.
61 | */
62 | public int getSize() {
63 | return size;
64 | }
65 | /**
66 | * The setter for size.
67 | *
68 | * @param size the size to set.
69 | */
70 | public void setSize(int size) {
71 | this.size = size;
72 | }
73 | /**
74 | * The getter for padding.
75 | *
76 | * @return the padding.
77 | */
78 | public int getPadding() {
79 | return padding;
80 | }
81 | /**
82 | * The setter for padding.
83 | *
84 | * @param padding the padding to set.
85 | */
86 | public void setPadding(int padding) {
87 | this.padding = padding;
88 | }
89 | /**
90 | * The getter for height.
91 | *
92 | * @return the height.
93 | */
94 | public short getHeight() {
95 | return height;
96 | }
97 | /**
98 | * The setter for height.
99 | *
100 | * @param height the height to set.
101 | */
102 | public void setHeight(short height) {
103 | this.height = height;
104 | }
105 | /**
106 | * The getter for width.
107 | *
108 | * @return the width.
109 | */
110 | public short getWidth() {
111 | return width;
112 | }
113 | /**
114 | * The setter for width.
115 | *
116 | * @param width the width to set.
117 | */
118 | public void setWidth(short width) {
119 | this.width = width;
120 | }
121 | /**
122 | * The getter for chunks.
123 | *
124 | * @return the chunks.
125 | */
126 | public List getChunks() {
127 | return chunks;
128 | }
129 | /**
130 | * The setter for chunks.
131 | *
132 | * @param chunks the chunks to set.
133 | */
134 | public void setChunks(List chunks) {
135 | this.chunks = chunks;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/src/sega/cvid/CvidHeader.java:
--------------------------------------------------------------------------------
1 | package sega.cvid;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.util.ArrayList;
5 | import java.util.Arrays;
6 | import java.util.List;
7 |
8 | public class CvidHeader {
9 |
10 | byte flag;
11 | int size;
12 | short width;
13 | short height;
14 | short numOfStrips;
15 | List strips = new ArrayList<>();
16 | int actualSize;
17 |
18 | public byte[] toByteArray() {
19 |
20 | ByteBuffer bb = ByteBuffer.allocate(actualSize);
21 | bb.put(flag);
22 |
23 | ByteBuffer sizeBuffer = ByteBuffer.allocate(4);
24 | sizeBuffer.putInt(size);
25 | byte[] tempSize = sizeBuffer.array();
26 |
27 | byte[] sizeArray = Arrays.copyOfRange(tempSize, 1, 4);
28 | bb.put(sizeArray);
29 | bb.putShort(width);
30 | bb.putShort(height);
31 | bb.putShort(numOfStrips);
32 | byte[] padding = new byte[2];
33 | bb.put(padding);
34 |
35 | for(int i = 0; i < strips.size(); i++) {
36 | bb.put(strips.get(i).toByteArray());
37 | }
38 |
39 | return bb.array();
40 | }
41 |
42 | /**
43 | * The getter for flag.
44 | *
45 | * @return the flag.
46 | */
47 | public byte getFlag() {
48 | return flag;
49 | }
50 | /**
51 | * The setter for flag.
52 | *
53 | * @param flag the flag to set.
54 | */
55 | public void setFlag(byte flag) {
56 | this.flag = flag;
57 | }
58 | /**
59 | * The getter for size.
60 | *
61 | * @return the size.
62 | */
63 | public int getSize() {
64 | return size;
65 | }
66 | /**
67 | * The setter for size.
68 | *
69 | * @param size the size to set.
70 | */
71 | public void setSize(int size) {
72 | this.size = size;
73 | }
74 | /**
75 | * The getter for width.
76 | *
77 | * @return the width.
78 | */
79 | public short getWidth() {
80 | return width;
81 | }
82 | /**
83 | * The setter for width.
84 | *
85 | * @param width the width to set.
86 | */
87 | public void setWidth(short width) {
88 | this.width = width;
89 | }
90 | /**
91 | * The getter for height.
92 | *
93 | * @return the height.
94 | */
95 | public short getHeight() {
96 | return height;
97 | }
98 | /**
99 | * The setter for height.
100 | *
101 | * @param height the height to set.
102 | */
103 | public void setHeight(short height) {
104 | this.height = height;
105 | }
106 | /**
107 | * The getter for numOfStrips.
108 | *
109 | * @return the numOfStrips.
110 | */
111 | public short getNumOfStrips() {
112 | return numOfStrips;
113 | }
114 | /**
115 | * The setter for numOfStrips.
116 | *
117 | * @param numOfStrips the numOfStrips to set.
118 | */
119 | public void setNumOfStrips(short numOfStrips) {
120 | this.numOfStrips = numOfStrips;
121 | }
122 | /**
123 | * The getter for strips.
124 | *
125 | * @return the strips.
126 | */
127 | public List getStrips() {
128 | return strips;
129 | }
130 | /**
131 | * The setter for strips.
132 | *
133 | * @param strips the strips to set.
134 | */
135 | public void setStrips(List strips) {
136 | this.strips = strips;
137 | }
138 | /**
139 | * The getter for actualSize.
140 | *
141 | * @return the actualSize.
142 | */
143 | public int getActualSize() {
144 | return actualSize;
145 | }
146 | /**
147 | * The setter for actualSize.
148 | *
149 | * @param actualSize the actualSize to set.
150 | */
151 | public void setActualSize(int actualSize) {
152 | this.actualSize = actualSize;
153 | }
154 |
155 |
156 |
157 | }
158 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/src/sega/cvid/CvidDataProcessor.java:
--------------------------------------------------------------------------------
1 | package sega.cvid;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.nio.ByteOrder;
5 | import java.util.Arrays;
6 |
7 | public class CvidDataProcessor {
8 |
9 | /**
10 | * Parses and processes a chunk of CVID Data to make the necessary adjustments to align everything
11 | * to 8-byte boundaries.
12 | *
13 | * @param data
14 | * @return
15 | */
16 | public static CvidHeader parse(byte[] data) {
17 | CvidHeader cvidHeader = new CvidHeader();
18 | int offset = 0;
19 | ByteBuffer bb = ByteBuffer.wrap(data, offset, 1);
20 | bb.order(ByteOrder.BIG_ENDIAN);
21 | cvidHeader.setFlag(bb.get());
22 | offset++;
23 | bb = ByteBuffer.wrap(data, offset, 3);
24 | byte[] tempArray = bb.array();
25 |
26 | byte[] sizeArray = new byte[4];
27 | sizeArray[1] = tempArray[0];
28 | sizeArray[2] = tempArray[1];
29 | sizeArray[3] = tempArray[2];
30 |
31 | bb = ByteBuffer.wrap(sizeArray, 0, 4);
32 | cvidHeader.setSize(bb.getInt());
33 | offset+=3;
34 | bb = ByteBuffer.wrap(data, offset, 2);
35 | cvidHeader.setWidth(bb.getShort());
36 | offset+=2;
37 | bb = ByteBuffer.wrap(data, offset, 2);
38 | cvidHeader.setHeight(bb.getShort());
39 | offset+=2;
40 | bb = ByteBuffer.wrap(data, offset, 2);
41 | cvidHeader.setNumOfStrips(bb.getShort());
42 | offset+=2;
43 |
44 | for(int i = 0; i < cvidHeader.getNumOfStrips(); i++) {
45 | CvidStrip strip = new CvidStrip();
46 | bb = ByteBuffer.wrap(data, offset, 1);
47 | strip.setFlags(bb.get());
48 | offset++;
49 |
50 | tempArray = Arrays.copyOfRange(data, offset, offset+3);
51 | sizeArray = new byte[4];
52 | sizeArray[1] = tempArray[0];
53 | sizeArray[2] = tempArray[1];
54 | sizeArray[3] = tempArray[2];
55 |
56 | ByteBuffer sizeBuffer = ByteBuffer.wrap(sizeArray);
57 | int testSize = sizeBuffer.getInt();
58 |
59 | strip.setSize(testSize);
60 | offset+=3;
61 | bb = ByteBuffer.wrap(data, offset, 4);
62 | strip.setPadding(bb.getInt());
63 | offset+=4;
64 | bb = ByteBuffer.wrap(data, offset, 2);
65 | strip.setHeight(bb.getShort());
66 | offset+=2;
67 | bb = ByteBuffer.wrap(data, offset, 2);
68 | strip.setWidth(bb.getShort());
69 | offset+=2;
70 |
71 | int bytesRemaining = strip.getSize() - 22;
72 | while(bytesRemaining > 0) {
73 |
74 | CvidChunk chunk = new CvidChunk();
75 | bb = ByteBuffer.wrap(data, offset, 2);
76 | chunk.setChunkType(bb.getShort());
77 | offset+=2;
78 | bb = ByteBuffer.wrap(data, offset, 2);
79 |
80 | chunk.setChunkSize(bb.getShort());
81 | offset+=2;
82 |
83 | int chunkSize = Short.toUnsignedInt(chunk.getChunkSize());
84 |
85 |
86 | byte[] chunkArray = Arrays.copyOfRange(data, offset, offset + chunkSize - 4);
87 |
88 | offset += chunkSize - 4;
89 |
90 |
91 | chunk.setData(chunkArray);
92 |
93 | int remainder = (chunkSize % 4);
94 | if(remainder != 0) {
95 | chunkSize += remainder;
96 | chunk.setChunkSize((short) chunkSize);
97 | }
98 | strip.getChunks().add(chunk);
99 | bytesRemaining -= chunkSize;
100 | }
101 |
102 | int newStripSize = 12;
103 | for(CvidChunk c : strip.getChunks()) {
104 | newStripSize += Short.toUnsignedInt(c.getChunkSize());
105 | }
106 | strip.setSize(newStripSize);
107 | cvidHeader.getStrips().add(strip);
108 | }
109 |
110 | int newDataSize = 12;
111 | for(CvidStrip s : cvidHeader.getStrips()) {
112 | newDataSize += s.getSize();
113 | }
114 | cvidHeader.setSize(newDataSize - 8);
115 | cvidHeader.setActualSize(newDataSize);
116 | return cvidHeader;
117 | }
118 |
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SegaSaturnFilmMuxer
2 |
3 |
4 | This tool will take the Audio and Video from 2 different sources and remux them to create a new FILM file of the combined streams. This is useful if you need to modify only one aspect of an FMV in a Saturn game such as the following situations:
5 |
6 | 1. You have a Japanese FMV that you want to replace the Audio with a different langauges dub while preserving the quality of the original Japanese Video encode.
7 |
8 | 2. You have a newly encoded FMV but you want to preserve the quality or compression scheme of the original FMV from the game.
9 |
10 | The tool is able to support both uncompressed PCM audio as well as ADX compressed audio.
11 |
12 |
13 | # Requirements for use
14 |
15 |
16 | Currently the tool does have the following requirements for use:
17 | 1. If using 2 videos as sources, both files video streams must have the exact same amount of frames and match in resolution and frame rate.
18 |
19 | 2. If swapping audio for translation/modification purposes or if using uncompressed audio, both files audio streams should have the same specifications (8-bit/16-bit, Mono/Stereo, Sample Rate, length, etc.). Audio Compression however does not matter.
20 |
21 | 3. Being written in Java, a Java Runtime Environment must be installed. It should work with Java 8 or higher.
22 |
23 |
24 | NOTES FOR ADX AUDIO:
25 |
26 | Muxing existing ADX audio from a source FILM file with a new video stream from another FILM file is supported and requires he user follow the requirements stated above.
27 |
28 | If you are swapping in new ADX audio to mux with an existing Video Stream, you only need to provide the ADX file for the Source Audio file. The one caveat is that the new ADX file must be the exact same specifications (Sample Rate, Stereo/Mono, etc.) as the original you are replacing. It should also be the exact same size in bytes. This is to keep the file as close to the original specifications the game is expecting.
29 |
30 | If you are muxing ADX into a file that does not already use ADX audio, the specifications of the ADX audio file will be used. Keep in mind that this may push the video file beyond it's bitrate limit. This feature is also highly experimental as there is no real way to test these files yet beyond injecting them into games that already use ADX Cinepak.
31 |
32 | NOTES for PCM AUDIO:
33 |
34 | Uncompressed PCM Audio is supported in the following file formats:
35 |
36 | .PCM File - 8-bit and 16-bit Audio are supported. These files are assumed to be raw headerless files. The default setting assumes they are in Little Endian format. If stereo it assumes they channels are in the standard interleaved format.
37 |
38 | - If your file is already in Big Endian format you need to check the Big Endian box.
39 | - If your PCM file is one that was extracted from an existing FILM file and is in the raw Saturn Format, then check the Saturn Format box.
40 |
41 | .WAV File - Only 16-bit is supported. This is becasue Saturn FILM files use 8-bit SIGNED PCM. The WAV format assumes 8bit is unsigned, so Signed 8-bit is an invalid format.
42 |
43 | NOTE:
44 | - When using WAV or ADX audio, the SaturnFormat and BigEndian checkboxes are ignored as these aren't releavnt to these file formats.
45 |
46 | # How to use
47 |
48 | 1. If you need to encode a new video, encode it as you normally would following the Sega Saturn Cinepak Encoding process making sure to adhear to the requirements listed above.
49 |
50 | * If you want to preserve the original video but replace the audio and dont want to use a WAV or PCM file, encode a copy of the source video with your new audio swapped in.
51 | * If you want to preserve the original audio, encode your modified video with uncompressed PCM audio that matches the specifications of the original source video.
52 |
53 | 2. Run the tool and select the source FILM, PCM, WAV or ADX file for your audio, the source FILM file for your video, and the output directory.
54 |
55 | 4. If using PCM check any special options check boxes if required. (Big Endian, Saturn format, Etc.)
56 |
57 | 3. Click the "Mux Audio and Video" button.
58 |
59 | Your new file will be in the specified output directory with the naming convention "NEW_".
60 |
61 | # Audio Extraction
62 |
63 | Audio Extraction is a new feature added. Both PCM and ADX audio can be extracted from a source FILM file and can be found under the extract tab.
64 |
65 | * If your source uses ADX, the file will be extracted as a .ADX file.
66 | * If it uses 8-bit Uncompressed PCM, it will be extracted as a headerless .PCM file ready to be imported into Audacity as RAW Audio.
67 | * If it's 16-bit PCM, you can either extract it as a Headerless .PCM file ready to import into Audacity as RAW Audio, or you can export it as a WAV file by checking the WAV Output box.
68 |
69 | # MovieToSaturn
70 |
71 | This feature serves as a replacement for the original Mac OS 7 Application Sega made to convert Quicktime Movie Files to Sega FILM files.
72 |
73 | Usage is fairly simple. Simply select your Cinepak video file and click Create FILM file. If successful it will create a .CPK file of the same name in the same directory. Beside the button the stats of the video bitrate will be displayed. Anything above 300KB/s will have playback issues on Saturn. Any spikes could also cause playback issues.
74 |
75 | Currently the following video types are supported:
76 |
77 | * Standard Modern Quicktime MOV files. (NOTE: The format has apparently changed and parsing older ones from Quicktime 4 or older isn't working correclty at the moment with stereo audio.)
78 | * Cinepak Codec at 24-bit RGB
79 | * 8-bit and 16-bit PCM Audio in both Mono and Stereo not exceeding 44100Hz.
80 |
81 | Chroma Key processing is also supported. Simply put the RGB values for the color you want the player to set to the background color. This currently isn't tested but should work as far as the file encoding goes.
82 |
83 | Quicktime MOV files can be encoded and created with either FFMPEG or VirtualDub 2:
84 | * Select Cinepak as the video compression codec.
85 | * Set the bitrate to be below 300KB/s (Don't forget to factor in your audio bitrate!)
86 | * Adjust the framerate to the desired frame rate.
87 | * Set the output colordepth to 24-bit RGB
88 | * Set the Audio to 8-bit or 16-bit PCM
89 | * Samplerate cannot exceed 44100Hz
90 | * Channels can only be Mono or Stereo.
91 | * Save as .MOV file (not fast start!)
92 |
93 | This is still a work in progress. Every possibly video frame rate, audio rate, etc. has not been tested so there may still be issues.
94 |
95 | # Why use this instead of FFMPEG?
96 |
97 | While FFMPEG does technically support Sega FILM files, it does not generate the STAB chunk correctly. While some games may be lenient and still play these files, thay may present issues (Video glitches, Cracks and Pops in audio, other errors, etc.). Other games may instead just flat out refuse to play these files or crash completely. A good example of this is Sakura Wars 2. When doing research on various different ADX Cinepak files, this was a game I found that flat out would not play any file I threw at it that used ADX audio that FFMPEG created.
98 |
99 | This tool will instead generate a correct file with a compliant STAB chunk that any game should be able to play. To show that it works, here is a test I did taking the Disc 2 intro for Sakura Wars 2, and adding subtitles that I did for the first game and patching it into Disc 1:
100 |
101 | https://www.youtube.com/watch?v=hf_0NowZuV8
102 |
103 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/src/sega/film/FILMHeader.java:
--------------------------------------------------------------------------------
1 | package sega.film;
2 |
3 | public class FILMHeader {
4 |
5 | private String filmString = "FILM";
6 | private int headerSize;
7 | private String version = "1.09";
8 | private String fsdcString = "FDSC";
9 | private int fsdcLength = 32;
10 | private String fourCC = "cvid";
11 | private int height;
12 | private int width;
13 | private byte bpp = 24;
14 | private byte audioChannels;
15 | private byte audioResolution;
16 | private byte compression;
17 | private short sampleRate;
18 | private short unknown;
19 | private byte chromaKeyEnable;
20 | private byte chromaKeyBlue;
21 | private byte chromaKeyGreen;
22 | private byte chromaKeyRed;
23 | private STABChunk stab = new STABChunk();
24 |
25 | /**
26 | * The getter for filmString.
27 | *
28 | * @return the filmString.
29 | */
30 | public String getFilmString() {
31 | return filmString;
32 | }
33 | /**
34 | * The setter for filmString.
35 | *
36 | * @param filmString the filmString to set.
37 | */
38 | public void setFilmString(String filmString) {
39 | this.filmString = filmString;
40 | }
41 | /**
42 | * The getter for version.
43 | *
44 | * @return the version.
45 | */
46 | public String getVersion() {
47 | return version;
48 | }
49 | /**
50 | * The setter for version.
51 | *
52 | * @param version the version to set.
53 | */
54 | public void setVersion(String version) {
55 | this.version = version;
56 | }
57 | /**
58 | * The getter for fsdcString.
59 | *
60 | * @return the fsdcString.
61 | */
62 | public String getFsdcString() {
63 | return fsdcString;
64 | }
65 | /**
66 | * The setter for fsdcString.
67 | *
68 | * @param fsdcString the fsdcString to set.
69 | */
70 | public void setFsdcString(String fsdcString) {
71 | this.fsdcString = fsdcString;
72 | }
73 | /**
74 | * The getter for headerSize.
75 | *
76 | * @return the headerSize.
77 | */
78 | public int getHeaderSize() {
79 | return headerSize;
80 | }
81 | /**
82 | * The setter for headerSize.
83 | *
84 | * @param headerSize the headerSize to set.
85 | */
86 | public void setHeaderSize(int headerSize) {
87 | this.headerSize = headerSize;
88 | }
89 | /**
90 | * The getter for fsdcLength.
91 | *
92 | * @return the fsdcLength.
93 | */
94 | public int getFsdcLength() {
95 | return fsdcLength;
96 | }
97 | /**
98 | * The setter for fsdcLength.
99 | *
100 | * @param fsdcLength the fsdcLength to set.
101 | */
102 | public void setFsdcLength(int fsdcLength) {
103 | this.fsdcLength = fsdcLength;
104 | }
105 | /**
106 | * The getter for fourCC.
107 | *
108 | * @return the fourCC.
109 | */
110 | public String getFourCC() {
111 | return fourCC;
112 | }
113 | /**
114 | * The setter for fourCC.
115 | *
116 | * @param fourCC the fourCC to set.
117 | */
118 | public void setFourCC(String fourCC) {
119 | this.fourCC = fourCC;
120 | }
121 | /**
122 | * The getter for height.
123 | *
124 | * @return the height.
125 | */
126 | public int getHeight() {
127 | return height;
128 | }
129 | /**
130 | * The setter for height.
131 | *
132 | * @param height the height to set.
133 | */
134 | public void setHeight(int height) {
135 | this.height = height;
136 | }
137 | /**
138 | * The getter for width.
139 | *
140 | * @return the width.
141 | */
142 | public int getWidth() {
143 | return width;
144 | }
145 | /**
146 | * The setter for width.
147 | *
148 | * @param width the width to set.
149 | */
150 | public void setWidth(int width) {
151 | this.width = width;
152 | }
153 | /**
154 | * The getter for bpp.
155 | *
156 | * @return the bpp.
157 | */
158 | public byte getBpp() {
159 | return bpp;
160 | }
161 | /**
162 | * The setter for bpp.
163 | *
164 | * @param bpp the bpp to set.
165 | */
166 | public void setBpp(byte bpp) {
167 | this.bpp = bpp;
168 | }
169 | /**
170 | * The getter for audioChannels.
171 | *
172 | * @return the audioChannels.
173 | */
174 | public byte getAudioChannels() {
175 | return audioChannels;
176 | }
177 | /**
178 | * The setter for audioChannels.
179 | *
180 | * @param audioChannels the audioChannels to set.
181 | */
182 | public void setAudioChannels(byte audioChannels) {
183 | this.audioChannels = audioChannels;
184 | }
185 | /**
186 | * The getter for audioResolution.
187 | *
188 | * @return the audioResolution.
189 | */
190 | public byte getAudioResolution() {
191 | return audioResolution;
192 | }
193 | /**
194 | * The setter for audioResolution.
195 | *
196 | * @param audioResolution the audioResolution to set.
197 | */
198 | public void setAudioResolution(byte audioResolution) {
199 | this.audioResolution = audioResolution;
200 | }
201 | /**
202 | * The getter for compression.
203 | *
204 | * @return the compression.
205 | */
206 | public byte getCompression() {
207 | return compression;
208 | }
209 | /**
210 | * The setter for compression.
211 | *
212 | * @param compression the compression to set.
213 | */
214 | public void setCompression(byte compression) {
215 | this.compression = compression;
216 | }
217 | /**
218 | * The getter for sampleRate.
219 | *
220 | * @return the sampleRate.
221 | */
222 | public short getSampleRate() {
223 | return sampleRate;
224 | }
225 | /**
226 | * The setter for sampleRate.
227 | *
228 | * @param sampleRate the sampleRate to set.
229 | */
230 | public void setSampleRate(short sampleRate) {
231 | this.sampleRate = sampleRate;
232 | }
233 | /**
234 | * The getter for unknown.
235 | *
236 | * @return the unknown.
237 | */
238 | public short getUnknown() {
239 | return unknown;
240 | }
241 | /**
242 | * The setter for unknown.
243 | *
244 | * @param unknown the unknown to set.
245 | */
246 | public void setUnknown(short unknown) {
247 | this.unknown = unknown;
248 | }
249 | /**
250 | * The getter for chromaKeyEnable.
251 | *
252 | * @return the chromaKeyEnable.
253 | */
254 | public byte getChromaKeyEnable() {
255 | return chromaKeyEnable;
256 | }
257 | /**
258 | * The setter for chromaKeyEnable.
259 | *
260 | * @param chromaKeyEnable the chromaKeyEnable to set.
261 | */
262 | public void setChromaKeyEnable(byte chromaKeyEnable) {
263 | this.chromaKeyEnable = chromaKeyEnable;
264 | }
265 | /**
266 | * The getter for chromaKeyBlue.
267 | *
268 | * @return the chromaKeyBlue.
269 | */
270 | public byte getChromaKeyBlue() {
271 | return chromaKeyBlue;
272 | }
273 | /**
274 | * The setter for chromaKeyBlue.
275 | *
276 | * @param chromaKeyBlue the chromaKeyBlue to set.
277 | */
278 | public void setChromaKeyBlue(byte chromaKeyBlue) {
279 | this.chromaKeyBlue = chromaKeyBlue;
280 | }
281 | /**
282 | * The getter for chromaKeyGreen.
283 | *
284 | * @return the chromaKeyGreen.
285 | */
286 | public byte getChromaKeyGreen() {
287 | return chromaKeyGreen;
288 | }
289 | /**
290 | * The setter for chromaKeyGreen.
291 | *
292 | * @param chromaKeyGreen the chromaKeyGreen to set.
293 | */
294 | public void setChromaKeyGreen(byte chromaKeyGreen) {
295 | this.chromaKeyGreen = chromaKeyGreen;
296 | }
297 | /**
298 | * The getter for chromaKeyRed.
299 | *
300 | * @return the chromaKeyRed.
301 | */
302 | public byte getChromaKeyRed() {
303 | return chromaKeyRed;
304 | }
305 | /**
306 | * The setter for chromaKeyRed.
307 | *
308 | * @param chromaKeyRed the chromaKeyRed to set.
309 | */
310 | public void setChromaKeyRed(byte chromaKeyRed) {
311 | this.chromaKeyRed = chromaKeyRed;
312 | }
313 | /**
314 | * The getter for stab.
315 | *
316 | * @return the stab.
317 | */
318 | public STABChunk getStab() {
319 | return stab;
320 | }
321 | /**
322 | * The setter for stab.
323 | *
324 | * @param stab the stab to set.
325 | */
326 | public void setStab(STABChunk stab) {
327 | this.stab = stab;
328 | }
329 |
330 | public void printHeader() {
331 | System.out.println(filmString);
332 | System.out.println(headerSize);
333 | System.out.println(version);
334 | System.out.println(fsdcString);
335 | System.out.println(fsdcLength);
336 | System.out.println(fourCC);
337 | System.out.println(height);
338 | System.out.println(width);
339 | System.out.println(bpp);
340 | System.out.println(audioChannels);
341 | System.out.println(audioResolution);
342 | System.out.println(compression);
343 | System.out.println(sampleRate & 0xffff);
344 | }
345 |
346 |
347 | }
348 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/src/sega/cvid/MovieToSaturn.java:
--------------------------------------------------------------------------------
1 | package sega.cvid;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.nio.ByteBuffer;
6 | import java.util.ArrayList;
7 | import java.util.Arrays;
8 | import java.util.List;
9 |
10 | import org.jcodec.common.io.SeekableByteChannel;
11 | import org.jcodec.common.model.Packet;
12 | import org.jcodec.api.UnsupportedFormatException;
13 | import org.jcodec.common.AudioCodecMeta;
14 | import org.jcodec.common.DemuxerTrack;
15 | import org.jcodec.common.Format;
16 | import org.jcodec.common.JCodecUtil;
17 | import org.jcodec.common.SeekableDemuxerTrack;
18 | import org.jcodec.common.VideoCodecMeta;
19 | import org.jcodec.common.io.NIOUtils;
20 | import org.jcodec.containers.mp4.MP4Util;
21 | import org.jcodec.containers.mp4.demuxer.MP4Demuxer;
22 | import sega.film.FILMUtility;
23 | import sega.film.FILMfile;
24 | import sega.film.STABChunk;
25 | import sega.film.STABEntry;
26 |
27 | import static org.jcodec.common.Format.MOV;
28 |
29 | public class MovieToSaturn {
30 |
31 | private static SeekableDemuxerTrack videoTrack;
32 | private static DemuxerTrack audioTrack;
33 | private static FILMfile film;
34 | private static STABChunk stab;
35 |
36 | private static byte[] combinedAudioBuffer;
37 |
38 | private static int AUDIO_SAMPLE_INFO_1 = 0xFFFFFFFF;
39 | private static int AUDIO_SAMPLE_INFO_2 = 0X00000001;
40 |
41 | private static int audioBitRate = 0;
42 | private static int videoBitRate = 0;
43 | private static int totalBitrate = 0;
44 | private static int maxVideoBitratePerSecond = 0;
45 |
46 | private static int audioChunkIntroSize = 0;
47 | private static int audioChunkSize1 = 0;
48 | private static int audioChunkSize2 = 0;
49 | private static int totalAudioSize = 0;
50 |
51 | private static int filmOffset = 0;
52 | private static int audioOffset = 0;
53 | private static int stabTableEntries = 0;
54 |
55 | private static boolean hasAudio = false;
56 | private static boolean swapToBigEndian = false;
57 | private static boolean firstChunk = true;
58 | private static boolean size1 = true;
59 | private static int remainingAudioTime = 0;
60 | private static int remainingAudioBytes = 0;
61 | private static List framePackets = new ArrayList<>();
62 | private static List audioPackets = new ArrayList<>();
63 |
64 | private static String statsMessage = "";
65 |
66 | public static void movieToSaturn(File fileToConvert, boolean enableChromaKey, byte red, byte blue, byte green) throws IOException, UnsupportedFormatException {
67 |
68 | //Initialize Variables
69 | film = new FILMfile();
70 | stab = new STABChunk();
71 | hasAudio = false;
72 | swapToBigEndian = false;
73 | size1 = true;
74 | firstChunk = true;
75 | filmOffset = 0;
76 | audioOffset = 0;
77 | stabTableEntries = 0;
78 | framePackets = new ArrayList<>();
79 | audioPackets = new ArrayList<>();
80 | audioChunkIntroSize = 0;
81 | audioChunkSize1 = 0;
82 | audioChunkSize2 = 0;
83 | remainingAudioBytes= 0;
84 | remainingAudioTime = 0;
85 | totalAudioSize = 0;
86 | maxVideoBitratePerSecond = 0;
87 |
88 | //Set Chroma Key values
89 | if(enableChromaKey) {
90 | byte chromaEnable = (byte) 0x80;
91 | film.getHeader().setChromaKeyEnable(chromaEnable);
92 | }
93 |
94 | film.getHeader().setChromaKeyRed(red);
95 | film.getHeader().setChromaKeyGreen(green);
96 | film.getHeader().setChromaKeyBlue(blue);
97 |
98 | //Open file and parse out streams
99 | String filePath = fileToConvert.getPath();
100 | String fileName = fileToConvert.getName();
101 |
102 | SeekableByteChannel _in = NIOUtils.readableChannel(fileToConvert);
103 |
104 | ByteBuffer header = ByteBuffer.allocate(65536);
105 | _in.read(header);
106 | MP4Util.parseFullMovieChannel(_in);
107 | String audioCodec = null;
108 | String videoCodec = null;
109 |
110 | header.flip();
111 | Format detectFormat = JCodecUtil.detectFormatBuffer(header);
112 | if (detectFormat == null) {
113 | throw new UnsupportedFormatException("Could not detect the format of the input video.");
114 | }
115 |
116 | long videoDuration = 0;
117 |
118 | //If file is a MOV file, start parsing, otherwise throw an exception.
119 | if (MOV == detectFormat) {
120 |
121 | //Get out the video track data.
122 | MP4Demuxer d1 = MP4Demuxer.createMP4Demuxer(_in);
123 | videoTrack = (SeekableDemuxerTrack) d1.getVideoTrack();
124 | VideoCodecMeta videoMeta = videoTrack.getMeta().getVideoCodecMeta();
125 | videoCodec = d1.getMovie().getVideoTrack().getStsd().getBoxes().get(0).getFourcc();
126 | videoDuration = d1.getMovie().getVideoTrack().getDuration();
127 |
128 | //If not Cinepak throw exception.
129 | if(!videoCodec.toUpperCase().trim().equals("CVID")) {
130 | throw new UnsupportedFormatException("Only Cinepak is supported! (CVID)");
131 | }
132 |
133 | //Set video header info.
134 | film.getHeader().setHeight(videoMeta.getSize().getHeight());
135 | film.getHeader().setWidth(videoMeta.getSize().getWidth());
136 | film.getHeader().setAudioChannels((byte) 0);
137 | film.getHeader().setAudioResolution((byte) 0);
138 | film.getHeader().setSampleRate((short) 0);
139 |
140 | Packet p = videoTrack.nextFrame();
141 | stab.setFramerateFrequency(p.getTimescale());
142 |
143 | //Get each frame and calculate total video duration for bitrate calculations later.
144 | long totalVidDuration = 0;
145 | while(p != null) {
146 |
147 | totalVidDuration += p.getDuration();
148 | framePackets.add(p);
149 | p = videoTrack.nextFrame();
150 | }
151 |
152 | //If we have audio tracks, parse and process them.
153 | if(d1.getAudioTracks().size() > 0) {
154 |
155 | hasAudio = true;
156 | audioTrack = d1.getAudioTracks().get(0);
157 |
158 | AudioCodecMeta audioMeta = audioTrack.getMeta().getAudioCodecMeta();
159 | audioCodec = d1.getMovie().getAudioTracks().get(0).getStsd().getBoxes().get(0).getFourcc();
160 |
161 | //If audio isn't 8-bit or 16-bit PCM, throw an exception.
162 | if(!audioCodec.toUpperCase().trim().equals("TWOS")
163 | && !audioCodec.toUpperCase().trim().equals("SOWT")
164 | && !audioCodec.toUpperCase().trim().equals("RAW")) {
165 | throw new UnsupportedFormatException("Audio must be 16-bit PCM or 8-bit PCM (twos, sowt, or raw)");
166 | }
167 |
168 | //If little endian we need to swap the bits later.
169 | swapToBigEndian = false;
170 | if(audioCodec.toUpperCase().trim().equals("SOWT")) {
171 | swapToBigEndian = true;
172 | }
173 |
174 | //Set Audio values.
175 | film.getHeader().setAudioChannels((byte) audioMeta.getChannelCount());
176 | int audioResolution = audioMeta.getSampleSize() * 8;
177 | film.getHeader().setAudioResolution((byte) audioResolution);
178 | Integer intSample = audioMeta.getSampleRate();
179 | film.getHeader().setSampleRate(intSample.shortValue());
180 |
181 | //Parse and create combined audio track data.
182 | createCombinedAudioBuffer(d1.getAudioTracks().get(0));
183 |
184 | /*
185 | * Ok this is really jank looking but part of this comes from AVIToSaturn's old source code.
186 | * It calculates audio chunk sizes that are approximately the same as what MovieToSaturn created.
187 | *
188 | * The idea is you want the first chunk to be 1/2 a second of data, and the remaining chunks to be 1/4 a second.
189 | * However they also need to be aligned to 8-byte boundaries.
190 | */
191 |
192 | audioChunkIntroSize = ((((audioMeta.getChannelCount() * audioMeta.getSampleSize()) * (intSample / 2)) >>1) & ~3) * 2;
193 |
194 | audioChunkSize1 = ((((audioMeta.getChannelCount() * audioMeta.getSampleSize()) * (intSample / 4)) >> 1) & ~3) * 2;
195 |
196 | audioChunkSize2 = audioMeta.getSampleSize() * ((intSample/ ( 4 / audioMeta.getChannelCount()) - audioChunkSize1) + audioChunkSize1 );
197 |
198 | audioBitRate = ((audioMeta.getChannelCount() * audioMeta.getSampleSize()) * intSample) / 1024;
199 |
200 | //If not divisible by 8 we need to adjust it.
201 |
202 | int remainder = audioChunkSize2 % 8;
203 | if(remainder != 0) {
204 | audioChunkSize2 += (8 - remainder);
205 | }
206 |
207 | //Set remaining audio bytes to total size of audio.
208 | remainingAudioBytes = combinedAudioBuffer.length;
209 | }
210 |
211 | //Start building stab chunks
212 | STABEntry stabEntry = new STABEntry();
213 |
214 | ByteBuffer sampleInfo1 = ByteBuffer.allocate(4);
215 | ByteBuffer sampleInfo2 = ByteBuffer.allocate(4);
216 |
217 | //If we have audio, the first chunk needs to be the first audio chunk.
218 | if(hasAudio) {
219 | processAudioChunk();
220 | }
221 |
222 | int totalCvidDataSize = 0;
223 | int totalCvidDataSizePerSecond = 0;
224 | int durationCounter = 0;
225 |
226 | //Proccess each CVID Frame.
227 | for(int i = 0; i < framePackets.size(); i++) {
228 |
229 | stabEntry = new STABEntry();
230 | sampleInfo1 = ByteBuffer.allocate(4);
231 | sampleInfo2 = ByteBuffer.allocate(4);
232 |
233 | int pts = (int) framePackets.get(i).getPts();
234 | int duration = (int) framePackets.get(i).getDuration();
235 |
236 | //Need to set the flag if it's not a keyframe.
237 | if(!framePackets.get(i).isKeyFrame()) {
238 | pts |= 1 << 31;
239 | }
240 |
241 | sampleInfo1.putInt(pts);
242 | sampleInfo2.putInt(duration);
243 |
244 | //Process the CVID frames to adjust for 8-byte boundary sizes.
245 | CvidHeader cvidData = CvidDataProcessor.parse(framePackets.get(i).data.array());
246 |
247 | //Start adding up bitrate.
248 | totalCvidDataSize += cvidData.actualSize;
249 | totalCvidDataSizePerSecond += cvidData.actualSize;
250 | durationCounter += duration;
251 | //If it's been 1 second calculate the bitrate for the last second and see if it's the biggest we've seen.
252 | if(durationCounter >= stab.getFramerateFrequency()) {
253 | if(totalCvidDataSizePerSecond > maxVideoBitratePerSecond) {
254 | maxVideoBitratePerSecond = totalCvidDataSizePerSecond;
255 | }
256 | totalCvidDataSizePerSecond = 0;
257 | durationCounter = 0;
258 | }
259 |
260 | //Set stabe Entry values.
261 | stabEntry.setLength(cvidData.actualSize);
262 | stabEntry.setOffset(filmOffset);
263 | stabEntry.setSampleInfo1(sampleInfo1.array());
264 | stabEntry.setSampleInfo2(sampleInfo2.array());
265 |
266 | //Add Stab entry and cvid chunk to film file and increment.
267 | stab.addEntry(stabEntry);
268 | film.getChunks().add(cvidData.toByteArray());
269 | filmOffset += cvidData.actualSize;
270 | stabTableEntries++;
271 | //Calculate remaining audio time.
272 | remainingAudioTime -= duration;
273 | //This again comes from AviToSaturn and calculates the correct frame to interleave the next audio chunk.
274 | if (hasAudio && remainingAudioBytes > 0 && remainingAudioTime <= stab.getFramerateFrequency() * (3/8)) {
275 | processAudioChunk();
276 | }
277 | }
278 |
279 | //If when we're all done we still have some audio process it.
280 | while(remainingAudioBytes > 0) {
281 | processAudioChunk();
282 | }
283 |
284 | //Calculate bitrates.
285 | videoBitRate = (int) (totalCvidDataSize / ( videoDuration/ 1000)) / 1024;
286 |
287 | totalBitrate = videoBitRate + audioBitRate;
288 | maxVideoBitratePerSecond = maxVideoBitratePerSecond / 1024;
289 |
290 | //Set status message.
291 | statsMessage = new String ("Video Bitrate: " + videoBitRate + "KB/s
"
292 | + "Max Video Bitrate Bitrate: " + maxVideoBitratePerSecond + "KB/s
"
293 | + "Audio Bitrate: " + audioBitRate + "KB/s
"
294 | +"Total Average Bitrate: " + totalBitrate + "KB/s ");
295 |
296 |
297 | //Finish up creating film file.
298 |
299 | stab.setNumOfEntries(stabTableEntries);
300 | stab.setLength((stabTableEntries + 1)* 16);
301 | film.getHeader().setHeaderSize(stab.getLength() + 48);
302 | film.getHeader().setStab(stab);
303 |
304 | String pieces[] = fileName.split("\\.(?=[^\\.]+$)");
305 |
306 | //Write new file.
307 |
308 | String newName = pieces[0].concat(".CPK");
309 | String output = filePath.replace(fileName, newName);
310 |
311 | FILMUtility.reconstruct(film, output);
312 |
313 |
314 | } else {
315 | throw new UnsupportedFormatException("Container format is not supported by JCodec");
316 | }
317 |
318 | }
319 |
320 | public static String getStatsMessage() {
321 | return statsMessage;
322 | }
323 |
324 | /**
325 | * Combines the audio chunks into one large buffer. Also swaps endianness.
326 | *
327 | * @param demux Track to demux
328 | * @throws IOException
329 | */
330 | private static void createCombinedAudioBuffer(DemuxerTrack demux) throws IOException {
331 | Packet ap = demux.nextFrame();
332 | while(ap != null) {
333 | audioPackets.add(ap);
334 | totalAudioSize += ap.getData().array().length;
335 | ap = demux.nextFrame();
336 | }
337 | ByteBuffer buffer = ByteBuffer.allocate(totalAudioSize);
338 |
339 | for(int i = 0; i < audioPackets.size(); i++) {
340 | buffer.put((audioPackets.get(i).getData().array()));
341 | }
342 |
343 | byte[] combinedAudio = buffer.array();
344 |
345 | int audioSampleSize = film.getHeader().getAudioResolution() / 8;
346 |
347 | //Swap to big Endian.
348 | if(swapToBigEndian) {
349 | if(audioSampleSize == 2) {
350 |
351 | byte[] sample = new byte[2];
352 |
353 | ByteBuffer audioBuffer = ByteBuffer.allocate(totalAudioSize);
354 | for(int i = 0; i < totalAudioSize; i += 2) {
355 | sample[1] = combinedAudio[i];
356 | sample[0] = combinedAudio[i + 1];
357 |
358 | audioBuffer.put(sample);
359 | }
360 | combinedAudioBuffer = audioBuffer.array();
361 | }
362 | } else {
363 | combinedAudioBuffer = combinedAudio;
364 | }
365 |
366 |
367 | }
368 |
369 | /**
370 | * Processes gets and processes the next audio chunk. Then adds it to the Film File.
371 | *
372 | * Normal PCM Data has the left and right samples interleaved.
373 | * Sega FILM has all left samples first then all the right samples next. We need to convert to this format.
374 | *
375 | * Finally 8-bit PCM is usually unsigned, Sega FILM expects it to be signed. So this will convert it.
376 | *
377 | */
378 | private static void processAudioChunk() {
379 | int audioChunkSize = 0;
380 |
381 | //Determin which size to use.
382 | if(firstChunk) {
383 | audioChunkSize = audioChunkIntroSize;
384 | } else {
385 | if(size1) {
386 | audioChunkSize = audioChunkSize1;
387 | size1 = !size1;
388 | } else {
389 | audioChunkSize = audioChunkSize2;
390 | size1 = !size1;
391 | }
392 | }
393 |
394 | //More jank from AviToSaturn to deal with the last chunk not aligning to an 8-byte boundary.
395 | if(audioChunkSize > remainingAudioBytes) {
396 | audioChunkSize = remainingAudioBytes;
397 | int remainder = audioChunkSize % 8;
398 | if(remainder != 0) {
399 |
400 | audioChunkSize = (remainingAudioBytes + (8 - remainder)) & ~3;
401 | }
402 |
403 | }
404 |
405 | byte[] audioChunk = Arrays.copyOfRange(combinedAudioBuffer, audioOffset, audioOffset + audioChunkSize);
406 | int audioSampleSize = film.getHeader().getAudioResolution() / 8;
407 | int leftSize = 0;
408 | int rightSize = 0;
409 |
410 | //Need to deinterleave
411 | if(film.getHeader().getAudioChannels() == 2) {
412 |
413 | ByteBuffer leftAudio;
414 | ByteBuffer rightAudio;
415 | if(audioSampleSize == 2) {
416 | leftAudio = ByteBuffer.allocate((audioChunkSize / 2));
417 | rightAudio = ByteBuffer.allocate((audioChunkSize / 2));
418 |
419 |
420 | for(int i = 0; i < audioChunk.length; i += 4) {
421 |
422 | leftAudio.put(leftSize, audioChunk[i]);
423 | leftSize++;
424 | leftAudio.put(leftSize, audioChunk[i + 1]);
425 | leftSize++;
426 |
427 | rightAudio.put(rightSize, audioChunk[i + 2]);
428 | rightSize++;
429 | rightAudio.put(rightSize, audioChunk[i + 3]);
430 | rightSize++;
431 |
432 | }
433 |
434 | byte[] leftAudioArray = Arrays.copyOfRange(leftAudio.array(), 0, leftSize);
435 | byte[] rightAudioArray = Arrays.copyOfRange(rightAudio.array(), 0, rightSize);
436 |
437 | leftAudio = ByteBuffer.wrap(leftAudioArray);
438 | rightAudio = ByteBuffer.wrap(rightAudioArray);
439 |
440 |
441 | } else {
442 | leftAudio = ByteBuffer.allocate((audioChunkSize / 2) + 1);
443 | rightAudio = ByteBuffer.allocate((audioChunkSize / 2) + 1);
444 |
445 | byte[] leftSample = new byte[1];
446 | byte[] rightSample = new byte[1];
447 |
448 | //Convert to signed and interleave
449 | for(int i = 0; i < audioChunkSize; i++) {
450 | leftSample[0] = audioChunk[i];
451 | byte lb = leftSample[0];
452 | int lbVal = lb & 0xFF;
453 | lbVal -= 128;
454 | Integer lbInt = new Integer(lbVal);
455 | leftAudio.put(lbInt.byteValue());
456 | leftSize++;
457 |
458 | if(i + 1 < audioChunkSize) {
459 | i++;
460 | rightSample[0] = audioChunk[i];
461 | byte rb = rightSample[0];
462 | int rbVal = rb & 0xFF;
463 | rbVal -= 128;
464 | Integer rbInt = new Integer(rbVal);
465 | rightAudio.put(rbInt.byteValue());
466 | rightSize++;
467 | }
468 | }
469 | byte[] leftAudioArray = Arrays.copyOfRange(leftAudio.array(), 0, leftSize);
470 | byte[] rightAudioArray = Arrays.copyOfRange(rightAudio.array(), 0, rightSize);
471 |
472 | leftAudio = ByteBuffer.wrap(leftAudioArray);
473 | rightAudio = ByteBuffer.wrap(rightAudioArray);
474 |
475 | }
476 | ByteBuffer buffer = ByteBuffer.allocate(audioChunkSize );
477 |
478 | buffer.put(leftAudio.array());
479 | buffer.put(rightAudio.array());
480 |
481 | audioChunk = buffer.array();
482 | } else if (audioSampleSize == 1) {
483 | //Need to convert 8-bit to signed
484 | ByteBuffer buffer = ByteBuffer.allocate(audioChunkSize );
485 |
486 | byte[] sample = new byte[1];
487 |
488 | for(int i = 0; i < audioChunkSize; i ++) {
489 | sample[0] = audioChunk[i];
490 | byte sb = sample[0];
491 | int sbVal = sb & 0xFF;
492 | sbVal -= 128;
493 | Integer sbInt = new Integer(sbVal);
494 | buffer.put(sbInt.byteValue());
495 |
496 | }
497 | audioChunk = buffer.array();
498 | }
499 |
500 | //Increment Audio offset.
501 | audioOffset += audioChunk.length;
502 |
503 | //Create stab entries.
504 | ByteBuffer sampleInfo1 = ByteBuffer.allocate(4);
505 | ByteBuffer sampleInfo2 = ByteBuffer.allocate(4);
506 | sampleInfo1.putInt(AUDIO_SAMPLE_INFO_1);
507 | sampleInfo2.putInt(AUDIO_SAMPLE_INFO_2);
508 |
509 | STABEntry stabEntry = new STABEntry();
510 |
511 | stabEntry.setLength(audioChunk.length);
512 | stabEntry.setOffset(filmOffset);
513 | stabEntry.setSampleInfo1(sampleInfo1.array());
514 | stabEntry.setSampleInfo2(sampleInfo2.array());
515 | filmOffset += audioChunk.length;
516 |
517 | //Add audio chunk and entries to film file.
518 | film.getChunks().add(audioChunk);
519 | stab.addEntry(stabEntry);
520 | stabTableEntries++;
521 |
522 | //More jank from AviToSaturn to increment remainingAudioTime to properly calculate the next time we need to do an audio frame.
523 | if(firstChunk) {
524 | remainingAudioTime = (stab.getFramerateFrequency() / 2) - (3 * (stab.getFramerateFrequency() / 8));
525 | firstChunk = false;
526 | }else {
527 | remainingAudioTime += stab.getFramerateFrequency() / 4;
528 | }
529 | remainingAudioBytes -= audioChunk.length;
530 | }
531 |
532 | }
533 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/src/gui/FILMMuxer.java:
--------------------------------------------------------------------------------
1 | package gui;
2 |
3 | import java.awt.Button;
4 | import java.awt.Dimension;
5 | import java.awt.EventQueue;
6 | import java.awt.Font;
7 | import java.awt.GridLayout;
8 | import java.awt.Label;
9 | import java.awt.TextField;
10 | import java.awt.event.ActionEvent;
11 | import java.awt.event.ActionListener;
12 | import java.io.File;
13 | import java.io.IOException;
14 | import java.text.NumberFormat;
15 |
16 | import javax.swing.JFileChooser;
17 | import javax.swing.JFormattedTextField;
18 | import javax.swing.JFrame;
19 | import javax.swing.JLabel;
20 | import javax.swing.JMenu;
21 | import javax.swing.JMenuBar;
22 | import javax.swing.JMenuItem;
23 | import javax.swing.UIManager;
24 | import javax.swing.filechooser.FileFilter;
25 | import javax.swing.text.NumberFormatter;
26 |
27 | import sega.cvid.MovieToSaturn;
28 | import sega.film.FILMUtility;
29 | import sega.film.FILMfile;
30 |
31 | import javax.swing.JTabbedPane;
32 | import javax.swing.JPanel;
33 | import javax.swing.JCheckBox;
34 |
35 | public class FILMMuxer {
36 |
37 | private JFrame frame;
38 | private String message = "";
39 |
40 | /**
41 | * Launch the application.
42 | */
43 | public static void main(String[] args) {
44 | EventQueue.invokeLater(new Runnable() {
45 | public void run() {
46 | try {
47 | UIManager.setLookAndFeel(
48 | UIManager.getSystemLookAndFeelClassName());
49 | FILMMuxer window = new FILMMuxer();
50 | window.frame.setVisible(true);
51 | } catch (Exception e) {
52 | e.printStackTrace();
53 | }
54 | }
55 | });
56 | }
57 |
58 | /**
59 | * Create the application.
60 | */
61 | public FILMMuxer() {
62 | initialize();
63 | }
64 |
65 | /**
66 | * Initialize the contents of the frame.
67 | */
68 | private void initialize() {
69 | frame = new JFrame();
70 | frame.setBounds(100, 100, 560, 360);
71 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
72 | frame.setTitle("Sega Saturn FILM Tools");
73 |
74 | JMenuBar menuBar = new JMenuBar();
75 | frame.setJMenuBar(menuBar);
76 |
77 | JMenu mnFile = new JMenu("File");
78 | menuBar.add(mnFile);
79 |
80 | JMenuItem mntmQuit = new JMenuItem("Quit");
81 | mntmQuit.addActionListener(new ActionListener() {
82 | public void actionPerformed(ActionEvent e) {
83 | System.exit(0);
84 | }
85 | });
86 | mnFile.add(mntmQuit);
87 |
88 | JTabbedPane tabbedPane = new JTabbedPane();
89 | tabbedPane.setBounds(0, 183, 532, -183);
90 | frame.getContentPane().setLayout(new GridLayout(1, 1));
91 |
92 | JPanel panel1 = new JPanel(false);
93 | JLabel filler = new JLabel("Muxer");
94 | filler.setHorizontalAlignment(JLabel.CENTER);
95 | filler.setFont(new Font("Arial", Font.PLAIN, 11));
96 | panel1.setLayout(null);
97 | panel1.add(filler);
98 | panel1.setPreferredSize(new Dimension(550, 225));
99 |
100 |
101 | TextField parseAudioInputFileDirField = new TextField();
102 | parseAudioInputFileDirField.setBounds(157, 41, 343, 22);
103 | panel1.add(parseAudioInputFileDirField);
104 |
105 | Button parseAudioInputDirSearchButton = new Button("...");
106 | parseAudioInputDirSearchButton.addActionListener(new ActionListener() {
107 | public void actionPerformed(ActionEvent arg0) {
108 | JFileChooser chooser = new JFileChooser();
109 | chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
110 | int returnVal = chooser.showOpenDialog(null);
111 | if(returnVal == JFileChooser.APPROVE_OPTION) {
112 | System.out.println("You chose to open this file: " +
113 | chooser.getSelectedFile().getName());
114 | parseAudioInputFileDirField.setText(chooser.getSelectedFile().getAbsolutePath());
115 | }
116 | }
117 | });
118 | parseAudioInputDirSearchButton.setBounds(502, 41, 22, 22);
119 | panel1.add(parseAudioInputDirSearchButton);
120 |
121 | Label inputAudioParseDirLabel = new Label("Audio Source File");
122 | inputAudioParseDirLabel.setFont(new Font("Arial", Font.PLAIN, 11));
123 | inputAudioParseDirLabel.setBounds(10, 41, 148, 22);
124 | panel1.add(inputAudioParseDirLabel);
125 |
126 | TextField parseVideoInputFileDirField = new TextField();
127 | parseVideoInputFileDirField.setBounds(157, 69, 343, 22);
128 | panel1.add(parseVideoInputFileDirField);
129 |
130 | Button parseVideoInputDirSearchButton = new Button("...");
131 | parseVideoInputDirSearchButton.addActionListener(new ActionListener() {
132 | public void actionPerformed(ActionEvent arg0) {
133 | JFileChooser chooser = new JFileChooser();
134 | chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
135 | int returnVal = chooser.showOpenDialog(null);
136 | if(returnVal == JFileChooser.APPROVE_OPTION) {
137 | System.out.println("You chose to open this file: " +
138 | chooser.getSelectedFile().getName());
139 | parseVideoInputFileDirField.setText(chooser.getSelectedFile().getAbsolutePath());
140 | }
141 | }
142 | });
143 | parseVideoInputDirSearchButton.setBounds(502, 69, 22, 22);
144 | panel1.add(parseVideoInputDirSearchButton);
145 |
146 | Label inputVideoParseDirLabel = new Label("Video Source FILM File");
147 | inputVideoParseDirLabel.setFont(new Font("Arial", Font.PLAIN, 11));
148 | inputVideoParseDirLabel.setBounds(10, 69, 125, 22);
149 | panel1.add(inputVideoParseDirLabel);
150 |
151 | Label parserTitle = new Label("Sega Saturn FILM Muxer");
152 | parserTitle.setFont(new Font("Arial", Font.BOLD, 14));
153 | parserTitle.setBounds(10, 13, 227, 22);
154 | panel1.add(parserTitle);
155 |
156 | Label outputParseDirLabel = new Label("Output File Directory");
157 | outputParseDirLabel.setFont(new Font("Arial", Font.PLAIN, 11));
158 | outputParseDirLabel.setBounds(10, 97, 105, 22);
159 | panel1.add(outputParseDirLabel);
160 |
161 | TextField parseOutputFileDirField = new TextField();
162 | parseOutputFileDirField.setBounds(157, 97, 343, 22);
163 | panel1.add(parseOutputFileDirField);
164 |
165 | Button parseOutputDirSearchButton = new Button("...");
166 | parseOutputDirSearchButton.addActionListener(new ActionListener() {
167 | public void actionPerformed(ActionEvent e) {
168 | JFileChooser chooser = new JFileChooser();
169 | chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
170 | int returnVal = chooser.showOpenDialog(null);
171 | if(returnVal == JFileChooser.APPROVE_OPTION) {
172 | System.out.println("You chose to open this file: " +
173 | chooser.getSelectedFile().getName());
174 | parseOutputFileDirField.setText(chooser.getSelectedFile().getAbsolutePath());
175 | }
176 | }
177 | });
178 | parseOutputDirSearchButton.setBounds(502, 97, 22, 22);
179 | panel1.add(parseOutputDirSearchButton);
180 |
181 | JCheckBox chckbxNewCheckBox_1 = new JCheckBox("Saturn Format PCM");
182 | chckbxNewCheckBox_1.setBounds(10, 144, 160, 23);
183 | panel1.add(chckbxNewCheckBox_1);
184 |
185 | JCheckBox chckbxNewCheckBox_1_1 = new JCheckBox("Big Endian");
186 | chckbxNewCheckBox_1_1.setBounds(10, 170, 160, 23);
187 | panel1.add(chckbxNewCheckBox_1_1);
188 |
189 |
190 | Button parseButton = new Button("Mux Audio and Video");
191 | parseButton.addActionListener(new ActionListener() {
192 | public void actionPerformed(ActionEvent e) {
193 |
194 | try {
195 |
196 | FILMfile file1 = new FILMfile();
197 | FILMfile file2 = new FILMfile();
198 |
199 | if(parseAudioInputFileDirField.getText().endsWith(".ADX") || parseAudioInputFileDirField.getText().endsWith(".adx")) {
200 |
201 | FILMUtility.parse(parseVideoInputFileDirField.getText(), file2);
202 | System.out.println("Attempting to ReMux ADX files...");
203 | parseButton.setEnabled(false);
204 | FILMfile newFilm = FILMUtility.swapAudioFromADXFile(parseAudioInputFileDirField.getText(), file2);
205 |
206 | File f = new File(parseVideoInputFileDirField.getText());
207 |
208 | FILMUtility.reconstruct(newFilm, parseOutputFileDirField.getText() + "\\NEW_" + f.getName());
209 | parseButton.setEnabled(true);
210 |
211 | } else if(parseAudioInputFileDirField.getText().endsWith(".PCM") || parseAudioInputFileDirField.getText().endsWith(".pcm")) {
212 | FILMUtility.parse(parseVideoInputFileDirField.getText(), file2);
213 | System.out.println("Attempting to ReMux with PCM files...");
214 | parseButton.setEnabled(false);
215 |
216 | boolean satFormat = chckbxNewCheckBox_1.isSelected();
217 | boolean bigEndian = chckbxNewCheckBox_1_1.isSelected();
218 | FILMfile newFilm = FILMUtility.swapAudioFromPCMFile(parseAudioInputFileDirField.getText(), file2, satFormat, bigEndian);
219 |
220 | File f = new File(parseVideoInputFileDirField.getText());
221 |
222 | FILMUtility.reconstruct(newFilm, parseOutputFileDirField.getText() + "\\NEW_" + f.getName());
223 | parseButton.setEnabled(true);
224 |
225 | } else if(parseAudioInputFileDirField.getText().endsWith(".WAV") || parseAudioInputFileDirField.getText().endsWith(".wav")) {
226 | FILMUtility.parse(parseVideoInputFileDirField.getText(), file2);
227 | System.out.println("Attempting to ReMux with WAV files...");
228 | parseButton.setEnabled(false);
229 |
230 | boolean satFormat = chckbxNewCheckBox_1.isSelected();
231 | boolean bigEndian = chckbxNewCheckBox_1_1.isSelected();
232 | FILMfile newFilm = FILMUtility.swapAudioFromWAVFile(parseAudioInputFileDirField.getText(), file2);
233 |
234 | File f = new File(parseVideoInputFileDirField.getText());
235 |
236 | FILMUtility.reconstruct(newFilm, parseOutputFileDirField.getText() + "\\NEW_" + f.getName());
237 | parseButton.setEnabled(true);
238 |
239 | } else {
240 | FILMUtility.parse(parseAudioInputFileDirField.getText(), file1);
241 | FILMUtility.parse(parseVideoInputFileDirField.getText(), file2);
242 |
243 | System.out.println("Attempting to ReMux files...");
244 | parseButton.setEnabled(false);
245 | FILMfile newFilm = FILMUtility.swapAudio(file1, file2);
246 |
247 | File f = new File(parseAudioInputFileDirField.getText());
248 |
249 | FILMUtility.reconstruct(newFilm, parseOutputFileDirField.getText() + "\\NEW_" + f.getName());
250 | parseButton.setEnabled(true);
251 | }
252 |
253 |
254 | } catch (IOException e1) {
255 | e1.printStackTrace();
256 | }
257 |
258 | }
259 | });
260 | parseButton.setFont(new Font("Arial", Font.PLAIN, 20));
261 | parseButton.setBounds(10, 204, 514, 57);
262 | panel1.add(parseButton);
263 |
264 | JPanel panel2 = new JPanel(false);
265 | JLabel filler2 = new JLabel("Audio Extractor");
266 | filler2.setHorizontalAlignment(JLabel.CENTER);
267 | filler2.setFont(new Font("Arial", Font.PLAIN, 11));
268 | panel2.setLayout(null);
269 | panel2.add(filler2);
270 | panel2.setPreferredSize(new Dimension(550, 225));
271 |
272 |
273 |
274 | TextField extractAudioInputFileDirField = new TextField();
275 | extractAudioInputFileDirField.setBounds(157, 41, 343, 22);
276 | panel2.add(extractAudioInputFileDirField);
277 |
278 | Button extractAudioInputDirSearchButton = new Button("...");
279 | extractAudioInputDirSearchButton.addActionListener(new ActionListener() {
280 | public void actionPerformed(ActionEvent arg0) {
281 | JFileChooser chooser = new JFileChooser();
282 | chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
283 | int returnVal = chooser.showOpenDialog(null);
284 | if(returnVal == JFileChooser.APPROVE_OPTION) {
285 | System.out.println("You chose to open this file: " +
286 | chooser.getSelectedFile().getName());
287 | extractAudioInputFileDirField.setText(chooser.getSelectedFile().getAbsolutePath());
288 | }
289 | }
290 | });
291 | extractAudioInputDirSearchButton.setBounds(502, 41, 22, 22);
292 | panel2.add(extractAudioInputDirSearchButton);
293 |
294 | Label extractAudioParseDirLabel = new Label("Audio Source FILM File");
295 | extractAudioParseDirLabel.setFont(new Font("Arial", Font.PLAIN, 11));
296 | extractAudioParseDirLabel.setBounds(10, 41, 148, 22);
297 | panel2.add(extractAudioParseDirLabel);
298 |
299 |
300 | Label extractorTitle = new Label("Sega Saturn FILM Audio Extractor");
301 | extractorTitle.setFont(new Font("Arial", Font.BOLD, 14));
302 | extractorTitle.setBounds(10, 13, 249, 22);
303 | panel2.add(extractorTitle);
304 |
305 | Label outputExtractDirLabel = new Label("Output File Directory");
306 | outputExtractDirLabel.setFont(new Font("Arial", Font.PLAIN, 11));
307 | outputExtractDirLabel.setBounds(10, 69, 125, 22);
308 | panel2.add(outputExtractDirLabel);
309 |
310 | TextField extractOutputFileDirField = new TextField();
311 |
312 | extractOutputFileDirField.setBounds(157, 69, 343, 22);
313 | panel2.add(extractOutputFileDirField);
314 |
315 | Button extractOutputDirSearchButton = new Button("...");
316 | extractOutputDirSearchButton.addActionListener(new ActionListener() {
317 | public void actionPerformed(ActionEvent e) {
318 | JFileChooser chooser = new JFileChooser();
319 | chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
320 | int returnVal = chooser.showOpenDialog(null);
321 | if(returnVal == JFileChooser.APPROVE_OPTION) {
322 | System.out.println("You chose to open this file: " +
323 | chooser.getSelectedFile().getName());
324 | extractOutputFileDirField.setText(chooser.getSelectedFile().getAbsolutePath());
325 | }
326 | }
327 | });
328 | extractOutputDirSearchButton.setBounds(502, 69, 22, 22);
329 | panel2.add(extractOutputDirSearchButton);
330 | JCheckBox chckbxNewCheckBox = new JCheckBox("WAV Output");
331 | chckbxNewCheckBox.setBounds(10, 96, 97, 23);
332 | panel2.add(chckbxNewCheckBox);
333 |
334 | Button extractButton = new Button("Extract Audio");
335 | extractButton.addActionListener(new ActionListener() {
336 | public void actionPerformed(ActionEvent e) {
337 |
338 | try {
339 |
340 | FILMfile file1 = new FILMfile();
341 |
342 | FILMUtility.parse(extractAudioInputFileDirField.getText(), file1);
343 |
344 | System.out.println("Attempting to Extract audio...");
345 | extractButton.setEnabled(false);
346 | boolean waveOut = chckbxNewCheckBox.isSelected();
347 | File f = new File(extractAudioInputFileDirField.getText());
348 | FILMUtility.extractAudio(file1, extractOutputFileDirField.getText() + "\\" + f.getName(), waveOut);
349 |
350 | extractButton.setEnabled(true);
351 |
352 | } catch (IOException e1) {
353 | e1.printStackTrace();
354 | }
355 |
356 | }
357 | });
358 | extractButton.setFont(new Font("Arial", Font.PLAIN, 20));
359 | extractButton.setBounds(10, 204, 514, 57);
360 | panel2.add(extractButton);
361 |
362 |
363 | JPanel panel3 = new JPanel(false);
364 | JLabel filler3 = new JLabel("MovieToSaturn");
365 | filler3.setHorizontalAlignment(JLabel.CENTER);
366 | filler3.setFont(new Font("Arial", Font.PLAIN, 11));
367 | panel3.setLayout(null);
368 | panel3.add(filler3);
369 | panel3.setPreferredSize(new Dimension(550, 225));
370 |
371 |
372 | TextField sourceVideoFileDirField = new TextField();
373 | sourceVideoFileDirField.setBounds(157, 41, 343, 22);
374 | panel3.add(sourceVideoFileDirField);
375 |
376 | Button sourceVideoFileDirSearchButton = new Button("...");
377 | sourceVideoFileDirSearchButton.addActionListener(new ActionListener() {
378 | public void actionPerformed(ActionEvent arg0) {
379 | JFileChooser chooser = new JFileChooser();
380 | chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
381 | chooser.setFileFilter(new FileFilter() {
382 |
383 | public String getDescription() {
384 | return "Cinepak Video File (*.mov)";
385 | }
386 |
387 | public boolean accept(File f) {
388 | if (f.isDirectory()) {
389 | return true;
390 | } else {
391 | String filename = f.getName().toLowerCase();
392 | return filename.endsWith(".mov") ;
393 | }
394 | }
395 | });
396 | int returnVal = chooser.showOpenDialog(null);
397 | if(returnVal == JFileChooser.APPROVE_OPTION) {
398 | System.out.println("You chose to open this file: " +
399 | chooser.getSelectedFile().getName());
400 | sourceVideoFileDirField.setText(chooser.getSelectedFile().getAbsolutePath());
401 | }
402 | }
403 | });
404 | sourceVideoFileDirSearchButton.setBounds(502, 41, 22, 22);
405 | panel3.add(sourceVideoFileDirSearchButton);
406 |
407 | Label sourceVideoFileDirLabel = new Label("Source Video File: ");
408 | sourceVideoFileDirLabel.setFont(new Font("Arial", Font.PLAIN, 11));
409 | sourceVideoFileDirLabel.setBounds(10, 41, 148, 22);
410 | panel3.add(sourceVideoFileDirLabel);
411 |
412 |
413 | Label movieToSaturnTitle = new Label("MovieToSaturn");
414 | movieToSaturnTitle.setFont(new Font("Arial", Font.BOLD, 14));
415 | movieToSaturnTitle.setBounds(10, 13, 249, 22);
416 | panel3.add(movieToSaturnTitle);
417 |
418 | Label chromaKeyConstraints = new Label("(Enter values per channel between 0-255)");
419 | chromaKeyConstraints.setFont(new Font("Arial", Font.PLAIN, 11));
420 | chromaKeyConstraints.setBounds(275, 100, 225, 22);
421 | panel3.add(chromaKeyConstraints);
422 |
423 | JCheckBox enableChromaKeyCheckbox = new JCheckBox("Enable Chroma Key Processing");
424 | enableChromaKeyCheckbox.setBounds(10, 76, 256, 23);
425 | panel3.add(enableChromaKeyCheckbox);
426 |
427 | NumberFormat format = NumberFormat.getInstance();
428 | NumberFormatter formatter = new NumberFormatter(format);
429 | formatter.setValueClass(Integer.class);
430 | formatter.setMinimum(0);
431 | formatter.setMaximum(255);
432 | formatter.setAllowsInvalid(false);
433 | // If you want the value to be committed on each keystroke instead of focus lost
434 | formatter.setCommitsOnValidEdit(true);
435 |
436 | Label redLabel = new Label("Red: ");
437 | redLabel.setFont(new Font("Arial", Font.PLAIN, 11));
438 | redLabel.setBounds(10, 100, 35, 22);
439 | panel3.add(redLabel);
440 |
441 | JFormattedTextField redField = new JFormattedTextField(formatter);
442 | redField.setBounds(50, 100, 40, 22);
443 | panel3.add(redField);
444 |
445 | Label greenLabel = new Label("Green: ");
446 | greenLabel.setFont(new Font("Arial", Font.PLAIN, 11));
447 | greenLabel.setBounds(95, 100, 50, 22);
448 | panel3.add(greenLabel);
449 |
450 | JFormattedTextField greenField = new JFormattedTextField(formatter);
451 | greenField.setBounds(145, 100, 40, 22);
452 | panel3.add(greenField);
453 |
454 | Label blueLabel = new Label("Blue: ");
455 | blueLabel.setFont(new Font("Arial", Font.PLAIN, 11));
456 | blueLabel.setBounds(190, 100, 35, 22);
457 | panel3.add(blueLabel);
458 |
459 | JFormattedTextField blueField = new JFormattedTextField(formatter);
460 | blueField.setBounds(225, 100, 40, 22);
461 | panel3.add(blueField);
462 |
463 |
464 | JLabel MessageLabel = new JLabel(message);
465 |
466 | MessageLabel.setFont(new Font("Arial", Font.PLAIN, 12));
467 | MessageLabel.setBounds(10, 140, 344, 100);
468 | panel3.add(MessageLabel);
469 |
470 | Button createButton = new Button("Create FILM File");
471 | createButton.addActionListener(new ActionListener() {
472 | public void actionPerformed(ActionEvent e) {
473 |
474 | try {
475 |
476 | System.out.println("Attempting to create film file...");
477 | createButton.setEnabled(false);
478 | boolean enableChromaKey = enableChromaKeyCheckbox.isSelected();
479 | File f = new File(sourceVideoFileDirField.getText());
480 | byte red = 0;
481 | byte green = 0;
482 | byte blue = 0;
483 |
484 | if(!redField.getText().isEmpty()) {
485 | red = Integer.valueOf(redField.getText()).byteValue();
486 | }
487 | if(!greenField.getText().isEmpty()) {
488 | green = Integer.valueOf(greenField.getText()).byteValue();
489 | }
490 | if(!blueField.getText().isEmpty()) {
491 | blue = Integer.valueOf(blueField.getText()).byteValue();
492 | }
493 |
494 | MovieToSaturn.movieToSaturn(f, enableChromaKey, red, blue, green);
495 | MessageLabel.setText(MovieToSaturn.getStatsMessage());
496 | createButton.setEnabled(true);
497 |
498 | } catch (Exception e1) {
499 | MessageLabel.setText(e1.getMessage());
500 | createButton.setEnabled(true);
501 | }
502 |
503 | }
504 | });
505 | createButton.setFont(new Font("Arial", Font.PLAIN, 20));
506 | createButton.setBounds(355, 204, 164, 57);
507 | panel3.add(createButton);
508 |
509 |
510 |
511 | // tabbedPane.setEnabledAt(1, true);
512 | tabbedPane.addTab("Muxer", panel1);
513 |
514 |
515 | tabbedPane.addTab("Extractor", panel2);
516 |
517 | tabbedPane.addTab("MovieToSaturn", panel3);
518 |
519 |
520 | frame.getContentPane().add(tabbedPane);
521 |
522 | frame.setVisible(true);
523 |
524 | }
525 | }
526 |
--------------------------------------------------------------------------------
/SegaSaturnFilmMuxer/src/sega/film/FILMUtility.java:
--------------------------------------------------------------------------------
1 | package sega.film;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.File;
5 | import java.io.IOException;
6 | import java.nio.ByteBuffer;
7 | import java.nio.ByteOrder;
8 | import java.nio.file.Files;
9 | import java.nio.file.Path;
10 | import java.nio.file.Paths;
11 | import java.util.ArrayList;
12 | import java.util.Arrays;
13 | import java.util.List;
14 | import java.util.logging.Level;
15 | import java.util.logging.Logger;
16 |
17 | public class FILMUtility {
18 |
19 | private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
20 |
21 | private static final Logger log = Logger.getLogger(FILMUtility.class.getName());
22 |
23 | public static void parse(String inputFile, FILMfile file) throws IOException {
24 | if(inputFile == null) {
25 | log.log(Level.WARNING, "Input File path is null, aborting parsing.");
26 | } else {
27 | File f = new File(inputFile);
28 |
29 | byte[] FILMBytes = Files.readAllBytes(f.toPath());
30 |
31 | ByteBuffer bb = ByteBuffer.wrap(FILMBytes, 4, 4);
32 | file.getHeader().setHeaderSize(bb.getInt());
33 |
34 | bb = ByteBuffer.wrap(FILMBytes, 28, 4);
35 | file.getHeader().setHeight(bb.getInt());
36 |
37 | bb = ByteBuffer.wrap(FILMBytes, 32, 4);
38 | file.getHeader().setWidth(bb.getInt());
39 |
40 | bb = ByteBuffer.wrap(FILMBytes, 36, 1);
41 | file.getHeader().setBpp(bb.get());
42 |
43 | bb = ByteBuffer.wrap(FILMBytes, 37, 1);
44 | file.getHeader().setAudioChannels(bb.get());
45 |
46 | bb = ByteBuffer.wrap(FILMBytes, 38, 1);
47 | file.getHeader().setAudioResolution(bb.get());
48 |
49 | bb = ByteBuffer.wrap(FILMBytes, 39, 1);
50 | file.getHeader().setCompression(bb.get());
51 |
52 | bb = ByteBuffer.wrap(FILMBytes, 40, 2);
53 | file.getHeader().setSampleRate(bb.getShort());
54 |
55 | bb = ByteBuffer.wrap(FILMBytes, 42, 2);
56 | file.getHeader().setUnknown(bb.getShort());
57 |
58 | bb = ByteBuffer.wrap(FILMBytes, 44, 1);
59 | file.getHeader().setChromaKeyEnable(bb.get());
60 |
61 | bb = ByteBuffer.wrap(FILMBytes, 45, 1);
62 | file.getHeader().setChromaKeyBlue(bb.get());
63 |
64 | bb = ByteBuffer.wrap(FILMBytes, 46, 1);
65 | file.getHeader().setChromaKeyBlue(bb.get());
66 |
67 | bb = ByteBuffer.wrap(FILMBytes, 47, 1);
68 | file.getHeader().setChromaKeyGreen(bb.get());
69 |
70 | bb = ByteBuffer.wrap(FILMBytes, 48, 1);
71 | file.getHeader().setChromaKeyRed(bb.get());
72 |
73 | bb = ByteBuffer.wrap(FILMBytes, 52, 4);
74 | file.getHeader().getStab().setLength(bb.getInt());
75 |
76 | bb = ByteBuffer.wrap(FILMBytes, 56, 4);
77 | file.getHeader().getStab().setFramerateFrequency(bb.getInt());
78 |
79 | bb = ByteBuffer.wrap(FILMBytes, 60, 4);
80 | int numEntries = bb.getInt();
81 | file.getHeader().getStab().setNumOfEntries(numEntries);
82 |
83 |
84 | int offsetStart = 64;
85 | List stabEntries = new ArrayList<>();
86 | for(int i = 0; i < numEntries; i++) {
87 |
88 | STABEntry entry = new STABEntry();
89 | bb = ByteBuffer.wrap(FILMBytes, offsetStart, 4);
90 | entry.setOffset(bb.getInt());
91 | offsetStart+=4;
92 |
93 | bb = ByteBuffer.wrap(FILMBytes, offsetStart, 4);
94 | entry.setLength(bb.getInt());
95 | offsetStart+=4;
96 |
97 | byte[] sample1 = Arrays.copyOfRange(FILMBytes, offsetStart, offsetStart + 4);
98 | offsetStart+=4;
99 | byte[] sample2 = Arrays.copyOfRange(FILMBytes, offsetStart, offsetStart + 4);
100 | offsetStart+=4;
101 |
102 | entry.setSampleInfo1(sample1);
103 | entry.setSampleInfo2(sample2);
104 | stabEntries.add(entry);
105 | }
106 |
107 | file.getHeader().getStab().setEntries(stabEntries);
108 |
109 | for(int i = 0; i < numEntries; i++) {
110 | STABEntry entry = stabEntries.get(i);
111 | byte[] chunk = Arrays.copyOfRange(FILMBytes, offsetStart, offsetStart + entry.getLength());
112 | offsetStart += entry.getLength();
113 | file.getChunks().add(chunk);
114 | }
115 | }
116 | }
117 |
118 | public static void extractAudio(FILMfile source, String outputFilePath, boolean waveOut) throws IOException {
119 | List sourceStabs = source.getHeader().getStab().getEntries();
120 |
121 | boolean isADX = false;
122 | if(source.getHeader().getCompression() == 2) {
123 | isADX = true;
124 | }
125 | ByteArrayOutputStream out = new ByteArrayOutputStream();
126 |
127 | for(int i = 0; i < sourceStabs.size(); i++) {
128 | if(isAudioChunk(sourceStabs.get(i))) {
129 |
130 | if(!isADX && source.getHeader().getAudioChannels() == 2) {
131 | int length = source.getChunks().get(i).length;
132 | int halfway = source.getChunks().get(i).length / 2;
133 | byte[] leftData = Arrays.copyOfRange(source.getChunks().get(i), 0, halfway);
134 | byte[] rightData = Arrays.copyOfRange(source.getChunks().get(i), halfway, length);
135 |
136 | int iter = 0;
137 | if(source.getHeader().getAudioResolution() == 16) {
138 | while(iter < leftData.length) {
139 | out.write(leftData[iter]);
140 | out.write(leftData[iter+1]);
141 |
142 | out.write(rightData[iter]);
143 | out.write(rightData[iter+1]);
144 |
145 | iter+=2;
146 | }
147 | } else {
148 | if(source.getHeader().getAudioResolution() == 8) {
149 | while(iter < leftData.length) {
150 | out.write(leftData[iter]);
151 | out.write(rightData[iter]);
152 | iter++;
153 | }
154 | }
155 | }
156 |
157 |
158 | } else {
159 | out.write(source.getChunks().get(i));
160 | }
161 |
162 | }
163 | }
164 |
165 | if(!isADX && source.getHeader().getAudioResolution() == 16 && waveOut) {
166 | ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
167 |
168 | byte[] fileBytes = out.toByteArray();
169 | ByteBuffer bb = ByteBuffer.allocate(4);
170 | bb.order(ByteOrder.LITTLE_ENDIAN);
171 | outBuffer.write(new String("RIFF").getBytes());
172 | bb.putInt(0, fileBytes.length + 44);
173 | outBuffer.write(bb.array());
174 | outBuffer.write(new String("WAVE").getBytes());
175 | outBuffer.write(new String("fmt ").getBytes());
176 | bb.putInt(0, 16);
177 | outBuffer.write(bb.array());
178 | bb = ByteBuffer.allocate(2);
179 | bb.order(ByteOrder.LITTLE_ENDIAN);
180 | bb.putShort(0, (short) 1);
181 | outBuffer.write(bb.array());
182 | bb.putShort(0, (short) source.getHeader().getAudioChannels());
183 | outBuffer.write(bb.array());
184 | bb = ByteBuffer.allocate(4);
185 | bb.order(ByteOrder.LITTLE_ENDIAN);
186 | bb.putInt(0, source.getHeader().getSampleRate());
187 | outBuffer.write(bb.array());
188 |
189 | int sampleRate = source.getHeader().getSampleRate();
190 | int resolution = source.getHeader().getAudioResolution();
191 | int channels = source.getHeader().getAudioChannels();
192 |
193 | int waveHeaderValue_1 = ((sampleRate * resolution * channels) / 8 );
194 | short waveHeaderValue_2 = (short) ((resolution * channels) / 8 );
195 |
196 | bb.putInt(0, waveHeaderValue_1);
197 | outBuffer.write(bb.array());
198 |
199 |
200 | bb = ByteBuffer.allocate(2);
201 | bb.order(ByteOrder.LITTLE_ENDIAN);
202 | bb.putShort(0, waveHeaderValue_2);
203 | outBuffer.write(bb.array());
204 | bb.putShort(0, (short) resolution);
205 | outBuffer.write(bb.array());
206 | outBuffer.write(new String("data").getBytes());
207 | bb = ByteBuffer.allocate(4);
208 | bb.order(ByteOrder.LITTLE_ENDIAN);
209 | bb.putInt(0, fileBytes.length);
210 | outBuffer.write(bb.array());
211 |
212 | outBuffer.write(swapByteOrder(fileBytes));
213 |
214 |
215 | out = outBuffer;
216 | }
217 |
218 | String[] pieces = outputFilePath.split("\\.");
219 |
220 | if(pieces.length > 1) {
221 | if(isADX) {
222 | pieces[pieces.length - 1] = "ADX";
223 | } else if(waveOut && source.header.getAudioResolution() == 16){
224 | pieces[pieces.length - 1] = "WAV";
225 | } else {
226 | pieces[pieces.length - 1] = "PCM";
227 | }
228 |
229 | String outputPath = pieces[0];
230 |
231 | for (int i = 1; i < pieces.length; i++) {
232 | outputPath = outputPath.concat(".").concat(pieces[i]);
233 | }
234 |
235 | outputFilePath = outputPath;
236 | }
237 |
238 | Path path = Paths.get(outputFilePath);
239 | try {
240 | Files.write(path, out.toByteArray());
241 | } catch (IOException e) {
242 | log.log(Level.SEVERE, "Caught IOException attempting to write bytes to file.", e);
243 | e.printStackTrace();
244 | }
245 | }
246 |
247 | public static FILMfile swapAudioFromADXFile(String adxFilePath, FILMfile dest) throws IOException {
248 |
249 | if(dest.getHeader().getCompression() == 2) {
250 | File f = new File(adxFilePath);
251 |
252 | byte[] ADXBytes = Files.readAllBytes(f.toPath());
253 |
254 | List destStabs = dest.getHeader().getStab().getEntries();
255 |
256 | List audioStabs = new ArrayList<>();
257 |
258 | int adxOffset = 0;
259 | List newChunks = new ArrayList<>();
260 | for(int i = 0; i < destStabs.size(); i++) {
261 | if(isAudioChunk(destStabs.get(i))) {
262 | audioStabs.add(i);
263 | newChunks.add(Arrays.copyOfRange(ADXBytes, adxOffset, adxOffset + destStabs.get(i).getLength()));
264 |
265 | adxOffset += destStabs.get(i).getLength();
266 |
267 | } else {
268 | newChunks.add(dest.getChunks().get(i));
269 | }
270 | }
271 |
272 | dest.setChunks(newChunks);
273 |
274 | return dest;
275 | } else {
276 | File f = new File(adxFilePath);
277 | byte[] ADXBytes = Files.readAllBytes(f.toPath());
278 |
279 | ByteBuffer adxBB = ByteBuffer.wrap(ADXBytes, 7, 1);
280 | byte adxChannels = adxBB.get();
281 |
282 | adxBB = ByteBuffer.wrap(ADXBytes, 8, 4);
283 |
284 | int adxSampleRate = adxBB.getInt();
285 |
286 | int oneHalfSecondOfAudio = adxSampleRate * adxChannels;
287 | int oneEighthOfAudio = oneHalfSecondOfAudio / 4;
288 |
289 | List destStabs = dest.getHeader().getStab().getEntries();
290 |
291 | List audioStabs = new ArrayList<>();
292 |
293 | List audioStabsToRemove = new ArrayList<>();
294 |
295 | int adxOffset = 0;
296 | List newChunks = new ArrayList<>();
297 | boolean isFirstChunk = true;
298 | boolean isOddChunk = true;
299 | boolean isLastChunk = false;
300 | int newOffset = 0;
301 |
302 | for(int i = 0; i < destStabs.size(); i++) {
303 | destStabs.get(i).setOffset(newOffset);
304 | if(isAudioChunk(destStabs.get(i))) {
305 | audioStabs.add(i);
306 |
307 | int newLength = 0;
308 | if(isFirstChunk) {
309 | isFirstChunk = false;
310 |
311 | newLength = ((oneHalfSecondOfAudio / 32) * 18) + 0x48;
312 |
313 | } else {
314 |
315 | if(isOddChunk) {
316 | isOddChunk = false;
317 |
318 | newLength = ((oneEighthOfAudio / 32) * 18) - 18;
319 | } else {
320 | isOddChunk = true;
321 |
322 | newLength = ((oneEighthOfAudio / 32) * 18) + 18;
323 | }
324 | }
325 |
326 | if(isLastChunk) {
327 | audioStabsToRemove.add(i);
328 | }
329 |
330 | if(adxOffset + newLength > ADXBytes.length && adxOffset < ADXBytes.length && !isLastChunk) {
331 | newLength = ADXBytes.length - adxOffset;
332 | newOffset += newLength;
333 | destStabs.get(i).setLength(newLength);
334 | newChunks.add(Arrays.copyOfRange(ADXBytes, adxOffset, adxOffset + newLength));
335 | isLastChunk = true;
336 | }
337 |
338 | if(adxOffset > ADXBytes.length) {
339 | isLastChunk = true;
340 | }
341 |
342 | if(!isLastChunk) {
343 | newOffset += newLength;
344 | newChunks.add(Arrays.copyOfRange(ADXBytes, adxOffset, adxOffset + newLength));
345 |
346 | destStabs.get(i).setLength(newLength);
347 | adxOffset += destStabs.get(i).getLength();
348 | }
349 |
350 |
351 |
352 | } else {
353 | newOffset += destStabs.get(i).getLength();
354 | newChunks.add(dest.getChunks().get(i));
355 | }
356 | }
357 |
358 | if(!audioStabsToRemove.isEmpty()) {
359 | List newStabs = new ArrayList<>();
360 | for(int i = 0; i < destStabs.size(); i++) {
361 | if(!audioStabsToRemove.contains(i)) {
362 | newStabs.add(destStabs.get(i));
363 | } else {
364 | System.out.println("Skipping " + i);
365 | }
366 | }
367 | dest.getHeader().getStab().setEntries(newStabs);
368 | dest.getHeader().getStab().setNumOfEntries(newStabs.size());
369 | dest.getHeader().getStab().setLength(newStabs.size() * 16);
370 | dest.getHeader().setHeaderSize((newStabs.size() * 16 ) + 0x40);
371 | }
372 |
373 | dest.setChunks(newChunks);
374 | dest.getHeader().setCompression((byte) 2);
375 | dest.getHeader().setAudioChannels(adxChannels);
376 | dest.getHeader().setAudioResolution((byte)16);
377 | dest.getHeader().setSampleRate((short) adxSampleRate);
378 |
379 | return dest;
380 | }
381 |
382 | }
383 |
384 | public static FILMfile swapAudioFromWAVFile(String wavFilePath, FILMfile dest) throws IOException {
385 | if(dest.getHeader().getCompression() == 0 && dest.header.getAudioResolution() == 16 ) {
386 | File f = new File(wavFilePath);
387 | byte[] wavBytes = Files.readAllBytes(f.toPath());
388 |
389 | byte[] PCMBytes = Arrays.copyOfRange(wavBytes, 44, wavBytes.length);
390 |
391 | return swapPCMData(dest, PCMBytes, false, false);
392 |
393 | }else {
394 | //Can't use 8 bit WAV file, Saturn doesn't support unsigned 8 bit for Cinepak, must but be signed.
395 | //WAV can't hold 8-bit signed PCM.
396 | return dest;
397 | }
398 | }
399 |
400 | public static FILMfile swapAudioFromPCMFile(String pcmFilePath, FILMfile dest, boolean satFormat, boolean bigEndian) throws IOException {
401 |
402 | if(dest.getHeader().getCompression() == 0) {
403 | File f = new File(pcmFilePath);
404 |
405 | byte[] PCMBytes = Files.readAllBytes(f.toPath());
406 |
407 | return swapPCMData(dest, PCMBytes, satFormat, bigEndian);
408 | } else {
409 | //file doesn't use PCM, abort.
410 | return dest;
411 | }
412 |
413 | }
414 |
415 |
416 | private static FILMfile swapPCMData(FILMfile dest, byte[] PCMBytes, boolean satFormat, boolean bigEndian) throws IOException {
417 | List destStabs = dest.getHeader().getStab().getEntries();
418 |
419 | List audioStabs = new ArrayList<>();
420 |
421 | int pcmOffset = 0;
422 | List newChunks = new ArrayList<>();
423 | for(int i = 0; i < destStabs.size(); i++) {
424 | if(isAudioChunk(destStabs.get(i))) {
425 | audioStabs.add(i);
426 |
427 | byte[] rawAudio = Arrays.copyOfRange(PCMBytes, pcmOffset, pcmOffset + destStabs.get(i).getLength());
428 |
429 | if(!satFormat) {
430 |
431 | if(dest.header.getAudioResolution() == 16 && !bigEndian) {
432 | rawAudio = swapByteOrder(rawAudio);
433 | }
434 |
435 | if(dest.header.getAudioChannels() == 2) {
436 |
437 | ByteArrayOutputStream merge = new ByteArrayOutputStream();
438 | ByteArrayOutputStream left = new ByteArrayOutputStream();
439 | ByteArrayOutputStream right = new ByteArrayOutputStream();
440 |
441 | int length = rawAudio.length;
442 | int half = length / 2;
443 |
444 | int iter = 0;
445 | if(dest.header.getAudioResolution() == 16) {
446 | while (iter < length) {
447 | left.write(rawAudio[iter]);
448 | left.write(rawAudio[iter + 1]);
449 |
450 | right.write(rawAudio[iter + 2]);
451 | right.write(rawAudio[iter + 3]);
452 | iter +=4;
453 | }
454 | } else {
455 | while (iter < length) {
456 | left.write(rawAudio[iter]);
457 | right.write(rawAudio[iter + 1]);
458 | iter+=2;
459 | }
460 |
461 | }
462 |
463 | merge.write(left.toByteArray());
464 | merge.write(right.toByteArray());
465 |
466 | rawAudio = merge.toByteArray();
467 |
468 | }
469 | }
470 |
471 | newChunks.add(rawAudio);
472 |
473 | pcmOffset += destStabs.get(i).getLength();
474 |
475 | } else {
476 | newChunks.add(dest.getChunks().get(i));
477 | }
478 | }
479 |
480 | dest.setChunks(newChunks);
481 |
482 | return dest;
483 | }
484 |
485 | public static FILMfile swapAudio(FILMfile source, FILMfile dest) throws IOException {
486 |
487 | List sourceStabs = source.getHeader().getStab().getEntries();
488 |
489 | List destStabs = dest.getHeader().getStab().getEntries();
490 |
491 | List audioStabs = new ArrayList<>();
492 | boolean isADX = false;
493 | if(source.getHeader().getCompression() == 2) {
494 | isADX = true;
495 | }
496 |
497 | List sourceAudioChunks = new ArrayList<>();
498 |
499 | List sourceVideoChunks = new ArrayList<>();
500 |
501 | for(int i = 0; i < sourceStabs.size(); i++) {
502 | if(isAudioChunk(sourceStabs.get(i))) {
503 | audioStabs.add(i);
504 | sourceAudioChunks.add(source.getChunks().get(i));
505 | } else {
506 | sourceVideoChunks.add(source.getChunks().get(i));
507 | }
508 | }
509 | System.out.println("AUDIO SOURCE: ");
510 | System.out.println("Found " + sourceAudioChunks.size() + " Audio Chunks");
511 | System.out.println("Found " + sourceVideoChunks.size() + " Video Chunks");
512 | int vidSize = 0;
513 | for(byte[] chunk : sourceVideoChunks) {
514 | vidSize += chunk.length;
515 | }
516 | System.out.println("Video Data Size " + vidSize);
517 |
518 | List destAudioChunks = new ArrayList<>();
519 |
520 | List destVideoChunks = new ArrayList<>();
521 | for(int i = 0; i < destStabs.size(); i++) {
522 | if(isAudioChunk(destStabs.get(i))) {
523 | destAudioChunks.add(dest.getChunks().get(i));
524 | } else {
525 | destVideoChunks.add(dest.getChunks().get(i));
526 | }
527 | }
528 | System.out.println("VIDEO SOURCE: ");
529 | System.out.println("Found " + destAudioChunks.size() + " Audio Chunks");
530 | System.out.println("Found " + destVideoChunks.size() + " Video Chunks");
531 | vidSize = 0;
532 | for(byte[] chunk : destVideoChunks) {
533 | vidSize += chunk.length;
534 | }
535 | System.out.println("Video Data Size " + vidSize);
536 |
537 | List newChunks = new ArrayList<>();
538 | List newStabs = new ArrayList<>();
539 |
540 | for(int i = 0; i < destStabs.size(); i++) {
541 | if(audioStabs.contains(i)) {
542 | newChunks.add(source.getChunks().get(i));
543 | newStabs.add(sourceStabs.get(i));
544 | } else if(!isAudioChunk(destStabs.get(i))) {
545 | newChunks.add(dest.getChunks().get(i));
546 | newStabs.add(destStabs.get(i));
547 | }
548 | }
549 |
550 | int offset = 0;
551 | for(int i = 0; i < newStabs.size(); i++) {
552 | newStabs.get(i).setOffset(offset);
553 | offset += newStabs.get(i).getLength();
554 | }
555 | int stabSize;
556 | if(isADX) {
557 | stabSize = newStabs.size() * 0x10;
558 |
559 | dest.getHeader().getStab().setNumOfEntries(newStabs.size());
560 | dest.getHeader().getStab().setLength(stabSize);
561 | dest.getHeader().setHeaderSize(stabSize + 0x40);
562 | dest.getHeader().setCompression(source.getHeader().getCompression());
563 | } else {
564 | stabSize = (newStabs.size() * 0x10) + 0x10;
565 | dest.getHeader().getStab().setNumOfEntries(newStabs.size());
566 | dest.getHeader().getStab().setLength(stabSize);
567 | dest.getHeader().setHeaderSize(stabSize + 0x30);
568 | }
569 |
570 | dest.getHeader().getStab().setEntries(newStabs);
571 | dest.setChunks(newChunks);
572 |
573 | System.out.println("OUTPUT:");
574 | System.out.println("Source Chunks: " + source.getChunks().size());
575 | System.out.println("New Chunks: " + newChunks.size());
576 |
577 | return dest;
578 |
579 | }
580 |
581 | public static void reconstruct(FILMfile film, String outputFilePath) throws IOException {
582 |
583 | ByteArrayOutputStream out = new ByteArrayOutputStream();
584 | FILMHeader header = film.getHeader();
585 |
586 | out.write(header.getFilmString().getBytes());
587 |
588 | ByteBuffer bb = ByteBuffer.allocate(4);
589 | bb.putInt(0, header.getHeaderSize());
590 | out.write(bb.array());
591 |
592 | out.write(header.getVersion().getBytes());
593 | out.write(new byte[4]);
594 | out.write(header.getFsdcString().getBytes());
595 |
596 | bb.putInt(0, header.getFsdcLength());
597 | out.write(bb.array());
598 | out.write(header.getFourCC().getBytes());
599 |
600 | bb.putInt(0, header.getHeight());
601 | out.write(bb.array());
602 | bb.putInt(0, header.getWidth());
603 | out.write(bb.array());
604 |
605 | out.write(header.getBpp());
606 |
607 | out.write(header.getAudioChannels());
608 |
609 | out.write(header.getAudioResolution());
610 |
611 | out.write(header.getCompression());
612 |
613 | bb = ByteBuffer.allocate(2);
614 | bb.putShort(0, header.getSampleRate());
615 | out.write(bb.array());
616 |
617 | bb.putShort(0, header.getUnknown());
618 | out.write(bb.array());
619 |
620 | bb = ByteBuffer.allocate(1);
621 |
622 | bb.put(0, header.getChromaKeyEnable());
623 | out.write(bb.array());
624 |
625 | bb.put(0, header.getChromaKeyBlue());
626 | out.write(bb.array());
627 |
628 | bb.put(0, header.getChromaKeyGreen());
629 | out.write(bb.array());
630 |
631 | bb.put(0, header.getChromaKeyRed());
632 | out.write(bb.array());
633 |
634 | STABChunk stab = header.getStab();
635 |
636 | out.write(stab.getStabString().getBytes());
637 |
638 | bb = ByteBuffer.allocate(4);
639 | bb.putInt(0, stab.getLength());
640 | out.write(bb.array());
641 |
642 | bb.putInt(0, stab.getFramerateFrequency());
643 | out.write(bb.array());
644 |
645 | bb.putInt(0, stab.getNumOfEntries());
646 | out.write(bb.array());
647 |
648 |
649 | System.out.println("Stabs size: " + stab.getEntries().size());
650 |
651 | for(int i = 0; i < stab.getEntries().size(); i++) {
652 | STABEntry entry = stab.getEntries().get(i);
653 |
654 | bb.putInt(0, entry.getOffset());
655 | out.write(bb.array());
656 |
657 | bb.putInt(0, entry.getLength());
658 | out.write(bb.array());
659 | out.write(entry.getSampleInfo1());
660 | out.write(entry.getSampleInfo2());
661 | }
662 |
663 | for(int i = 0; i < film.getChunks().size(); i++) {
664 | out.write(film.getChunks().get(i));
665 | }
666 | Path path = Paths.get(outputFilePath);
667 | try {
668 | Files.write(path, out.toByteArray());
669 | } catch (IOException e) {
670 | log.log(Level.SEVERE, "Caught IOException attempting to write bytes to file.", e);
671 | e.printStackTrace();
672 | }
673 | }
674 |
675 | private static boolean isAudioChunk(STABEntry entry) {
676 | if(bytesToHex(entry.getSampleInfo1()).equals("FFFFFFFF")) {
677 | return true;
678 | }
679 |
680 | return false;
681 | }
682 |
683 | private static String bytesToHex(byte[] bytes) {
684 | char[] hexChars = new char[bytes.length * 2];
685 | for ( int j = 0; j < bytes.length; j++ ) {
686 | int v = bytes[j] & 0xFF;
687 | hexChars[j * 2] = hexArray[v >>> 4];
688 | hexChars[j * 2 + 1] = hexArray[v & 0x0F];
689 | }
690 | return new String(hexChars);
691 | }
692 |
693 | private static byte[] swapByteOrder(byte[] value) {
694 | final int length = value.length;
695 | byte[] res = new byte[length];
696 | int i = 0;
697 | while(i < length) {
698 | if(i+1 >= length) {
699 | res[i] = value[i];
700 | }else {
701 | res[i] = value[i+1];
702 | res[i+1] = value[i];
703 | }
704 |
705 | i += 2;
706 | }
707 | return res;
708 | }
709 |
710 | private static byte[] convertSignedToUnsigned(byte[] value) {
711 | final int length = value.length;
712 | byte[] res = new byte[length];
713 | int i = 0;
714 | while(i < length) {
715 |
716 | int val = value[i] & 0x000000FF;
717 | res[i] = (byte) val;
718 | i++;
719 | }
720 |
721 | return res;
722 | }
723 |
724 | }
725 |
--------------------------------------------------------------------------------