#2451 Importing sound ranges of different format than head by converting to uncompressed wav

This commit is contained in:
Jindra Petřík
2025-06-07 23:03:59 +02:00
parent ac9ceffae2
commit 2a8cddc7f9
7 changed files with 222 additions and 129 deletions

View File

@@ -27,7 +27,6 @@ import com.jpexs.decompiler.flash.tags.SoundStreamBlockTag;
import com.jpexs.decompiler.flash.tags.Tag;
import com.jpexs.decompiler.flash.tags.base.CharacterTag;
import com.jpexs.decompiler.flash.tags.base.SoundImportException;
import com.jpexs.decompiler.flash.tags.base.SoundParametersMismatchException;
import com.jpexs.decompiler.flash.tags.base.SoundStreamHeadTypeTag;
import com.jpexs.decompiler.flash.tags.base.SoundTag;
import com.jpexs.decompiler.flash.tags.base.UnsupportedSamplingRateException;
@@ -245,7 +244,7 @@ public class SoundImporter {
* @return True if sound stream was imported successfully
* @throws UnsupportedSamplingRateException On unsupported sampling rate
*/
public boolean importSoundStream(SoundStreamHeadTypeTag streamHead, InputStream is, int newSoundFormat) throws UnsupportedSamplingRateException, SoundParametersMismatchException {
public boolean importSoundStream(SoundStreamHeadTypeTag streamHead, InputStream is, int newSoundFormat) throws UnsupportedSamplingRateException {
return importSoundStreamAtFrame(streamHead, is, newSoundFormat, null);
}
@@ -260,30 +259,50 @@ public class SoundImporter {
* @return True if sound stream was imported successfully
* @throws UnsupportedSamplingRateException On unsupported sampling rate
*/
public boolean importSoundStreamAtFrame(SoundStreamHeadTypeTag streamHead, InputStream is, int newSoundFormat, Integer startFrame) throws UnsupportedSamplingRateException, SoundParametersMismatchException {
public boolean importSoundStreamAtFrame(SoundStreamHeadTypeTag streamHead, InputStream is, int newSoundFormat, Integer startFrame) throws UnsupportedSamplingRateException {
List<MP3FRAME> mp3Frames = null;
int newSoundRate = -1;
boolean newSoundSize = false;
boolean newSoundType = false;
long newSoundSampleCount = -1;
byte[] uncompressedSoundData = null;
int bytesPerSwfFrame = -1;
byte[] mp3data = null;
SWF swf = streamHead.getSwf();
int sampleLen = 0;
int soundRateHz = 0;
int bitRateOriginal = -1;
int newBitRate = -1;
if (streamHead.getSoundFormatId() == SoundFormat.FORMAT_MP3) {
List<SoundStreamFrameRange> ranges = streamHead.getRanges();
if (!ranges.isEmpty()) {
SWFInputStream sis;
try {
sis = new SWFInputStream(swf, ranges.get(0).blocks.get(0).streamSoundData.getRangeData());
MP3SOUNDDATA s = new MP3SOUNDDATA(sis, false);
if (!s.frames.isEmpty()) {
bitRateOriginal = s.frames.get(0).getBitRate();
}
} catch (IOException ex) {
//ignore
}
}
}
switch (newSoundFormat) {
case SoundFormat.FORMAT_UNCOMPRESSED_LITTLE_ENDIAN:
try (AudioInputStream audioIs = AudioSystem.getAudioInputStream(new BufferedInputStream(is))) {
AudioFormat fmt = audioIs.getFormat();
newSoundType = fmt.getChannels() == 2;
newSoundSize = fmt.getSampleSizeInBits() == 16;
bitRateOriginal = -1;
//newSoundSampleCount = audioIs.getFrameLength();
uncompressedSoundData = Helper.readStream(audioIs);
sampleLen = (newSoundType ? 2 : 1) * (newSoundSize ? 2 : 1);
soundRateHz = (int) Math.round(fmt.getSampleRate());
newSoundSampleCount = (int) Math.ceil(soundRateHz / swf.frameRate);
bytesPerSwfFrame = (int) Math.ceil(soundRateHz / swf.frameRate) * sampleLen;
//bytesPerSwfFrame = (int) Math.ceil(soundRateHz / swf.frameRate) * sampleLen;
switch (soundRateHz) {
case 5512:
newSoundRate = 0;
@@ -308,7 +327,7 @@ public class SoundImporter {
case SoundFormat.FORMAT_MP3:
BufferedInputStream bis = new BufferedInputStream(is);
loadID3v2(bis);
byte[] mp3data = Helper.readStream(bis);
mp3data = Helper.readStream(bis);
final int ID3_V1_LENTGH = 128;
final int ID3_V1_EXT_LENGTH = 227;
@@ -330,6 +349,7 @@ public class SoundImporter {
if (!snd.frames.isEmpty()) {
MP3FRAME fr = snd.frames.get(0);
soundRateHz = fr.getSamplingRate();
newBitRate = fr.getBitRate();
switch (soundRateHz) {
case 11025:
newSoundRate = 1;
@@ -368,20 +388,85 @@ public class SoundImporter {
if (newSoundFormat != streamHead.getSoundFormatId()
|| newSoundSize != streamHead.getSoundSize()
|| newSoundType != streamHead.getSoundType()
|| newSoundRate != streamHead.getSoundRate()) {
throw new SoundParametersMismatchException(
streamHead.getSoundType(),
streamHead.getSoundSize(),
streamHead.getSoundRate(),
streamHead.getSoundFormatId(),
newSoundType,
newSoundSize,
newSoundRate,
newSoundFormat
);
|| newSoundRate != streamHead.getSoundRate()
|| newBitRate != bitRateOriginal) {
List<ByteArrayRange> data = streamHead.getRawSoundData();
byte[] wholeStreamUncompressedData;
try {
wholeStreamUncompressedData = streamHead.getSoundFormat().decode(null, data, 0);
} catch (IOException ex) {
return false;
}
if (mp3data != null) {
final int[] rateMap = {5512, 11025, 22050, 44100};
SoundFormat mp3SoundFormat = new SoundFormat(SoundFormat.FORMAT_MP3, rateMap[newSoundRate], newSoundType);
try {
uncompressedSoundData = mp3SoundFormat.decode(null, Arrays.asList(new ByteArrayRange(mp3data)), 0);
} catch (IOException ex) {
return false;
}
mp3Frames = null;
}
newSoundFormat = SoundFormat.FORMAT_UNCOMPRESSED_LITTLE_ENDIAN;
//16bit<>8bit does not match, convert to 16bit
if (newSoundSize && !streamHead.getSoundSize()) {
wholeStreamUncompressedData = to16bit(wholeStreamUncompressedData);
} else if (streamHead.getSoundSize() && !newSoundSize) {
uncompressedSoundData = to16bit(uncompressedSoundData);;
newSoundSize = true;
}
//stereo<>mono does not match, convert to stereo
if (newSoundType && !streamHead.getSoundType()) {
wholeStreamUncompressedData = toStereo(wholeStreamUncompressedData, newSoundSize);
} else if (streamHead.getSoundType() && !newSoundType) {
uncompressedSoundData = toStereo(uncompressedSoundData, newSoundSize);
newSoundType = true;
}
//sound rate does not match, convert to the higher one
if (newSoundRate > streamHead.getSoundRate()) {
for (int i = streamHead.getSoundRate(); i < newSoundRate; i++) {
wholeStreamUncompressedData = toHigherRate(wholeStreamUncompressedData, newSoundSize, newSoundType);
}
} else if (streamHead.getSoundRate() > newSoundRate) {
for (int i = newSoundRate; i < streamHead.getSoundRate(); i++) {
uncompressedSoundData = toHigherRate(uncompressedSoundData, newSoundSize, newSoundType);
}
newSoundRate = streamHead.getSoundRate();
}
sampleLen = (newSoundType ? 2 : 1) * (newSoundSize ? 2 : 1);
final int[] rateMap = {5512, 11025, 22050, 44100};
addStream(streamHead, wholeStreamUncompressedData, swf, rateMap[newSoundRate], sampleLen, null, null, true);
}
}
addStream(streamHead, uncompressedSoundData, swf, soundRateHz, sampleLen, mp3Frames, startFrame, false);
streamHead.setSoundCompression(newSoundFormat);
streamHead.setSoundSampleCount((int) newSoundSampleCount);
streamHead.setSoundSize(newSoundSize);
streamHead.setSoundType(newSoundType);
streamHead.setSoundRate(newSoundRate);
streamHead.setModified(true);
streamHead.getTimelined().resetTimeline();
swf.resetTimeline(); //to reload blocks
return true;
}
private void addStream(
SoundStreamHeadTypeTag streamHead,
byte[] uncompressedSoundData,
SWF swf,
int soundRateHz,
int sampleLen,
List<MP3FRAME> mp3Frames,
Integer startFrame,
boolean matchRanges
) {
ByteArrayInputStream bais = uncompressedSoundData == null ? null : new ByteArrayInputStream(uncompressedSoundData);
List<SoundStreamFrameRange> ranges = streamHead.getRanges();
@@ -418,7 +503,6 @@ public class SoundImporter {
for (SoundStreamBlockTag block : existingBlocks) {
timelined.removeTag(block);
}
List<SoundStreamBlockTag> blocks = new ArrayList<>();
if (bais != null) { //Uncompressed
DataInputStream dais = new DataInputStream(bais);
@@ -512,7 +596,18 @@ public class SoundImporter {
}
if (t instanceof ShowFrameTag) {
frame++;
if (frame >= startFrame && !blocks.isEmpty()) {
boolean match = false;
if (matchRanges) {
for (SoundStreamFrameRange range : ranges) {
if (frame >= range.startFrame && frame <= range.endFrame) {
match = true;
break;
}
}
} else if (frame >= startFrame) {
match = true;
}
if (match && !blocks.isEmpty()) {
SoundStreamBlockTag block = blocks.remove(0);
block.setTimelined(timelined);
timelined.addTag(i, block);
@@ -534,16 +629,107 @@ public class SoundImporter {
framesBefore++;
}
timelined.setFrameCount(framesBefore);
streamHead.setSoundCompression(newSoundFormat);
streamHead.setSoundSampleCount((int) newSoundSampleCount);
streamHead.setSoundSize(newSoundSize);
streamHead.setSoundType(newSoundType);
streamHead.setSoundRate(newSoundRate);
streamHead.setModified(true);
timelined.resetTimeline();
swf.resetTimeline(); //to reload blocks
return true;
}
private byte[] toStereo(byte[] data, boolean soundSize) {
byte[] ret = new byte[data.length * 2];
for (int i = 0; i < data.length; i += (soundSize ? 2 : 1)) {
if (soundSize) {
ret[i * 2] = data[i];
ret[i * 2 + 1] = data[i + 1];
ret[i * 2 + 2] = data[i];
ret[i * 2 + 3] = data[i + 1];
} else {
ret[i * 2] = data[i];
ret[i * 2 + 1] = data[i];
}
}
return ret;
}
private byte[] to16bit(byte[] data) {
byte[] ret = new byte[data.length * 2];
for (int i = 0; i < data.length; i++) {
int val = data[i] & 0xFF;
val = val * 65535 / 255;
ret[i * 2] = (byte) (val & 0xFF);
ret[i * 2 + 1] = (byte) ((val >> 8) & 0xFF);
}
return ret;
}
/**
* Resamples sound data to higher sound rate (doubles sound rate) 5512,
* 11025, 22050, 44100
*
* @param data Input data
* @param soundSize True = 2 bytes little endian per channel, False = 1 byte
* per channel
* @param soundType True = Stereo = two channels, False = mono = single
* channel
* @return Resampled data
*/
private byte[] toHigherRate(byte[] data, boolean soundSize, boolean soundType) {
int sampleLen = (soundType ? 2 : 1) * (soundSize ? 2 : 1);
byte[] ret = new byte[data.length * 2 - sampleLen];
int prevLeft = 0;
int prevRight = 0;
int retPos = 0;
for (int i = 0; i < data.length; i += sampleLen) {
int left;
int right;
if (soundSize) {
left = (short) ((data[i] & 0xFF) + ((data[i + 1] & 0xFF) << 8));
if (soundType) {
right = (short) ((data[i + 2] & 0xFF) + ((data[i + 3] & 0xFF) << 8));
} else {
right = left;
}
} else {
left = data[i];
if (soundType) {
right = data[i + 1];
} else {
right = left;
}
}
if (i > 0) {
int midLeft = (prevLeft + left) / 2;
int midRight = (prevRight + right) / 2;
if (soundSize) {
ret[retPos] = (byte) (midLeft & 0xFF);
ret[retPos + 1] = (byte) ((midLeft >> 8) & 0xFF);
if (soundType) {
ret[retPos + 2] = (byte) (midRight & 0xFF);
ret[retPos + 3] = (byte) ((midRight >> 8) & 0xFF);
}
} else {
ret[retPos] = (byte) (midLeft & 0xFF);
if (soundType) {
ret[retPos + 1] = (byte) (midRight & 0xFF);
}
}
retPos += sampleLen;
}
if (soundSize) {
ret[retPos] = (byte) (left & 0xFF);
ret[retPos + 1] = (byte) ((left >> 8) & 0xFF);
if (soundType) {
ret[retPos + 2] = (byte) (right & 0xFF);
ret[retPos + 3] = (byte) ((right >> 8) & 0xFF);
}
} else {
ret[retPos] = (byte) (left & 0xFF);
if (soundType) {
ret[retPos + 1] = (byte) (right & 0xFF);
}
}
prevLeft = left;
prevRight = right;
retPos += sampleLen;
}
return ret;
}
/**
@@ -624,7 +810,8 @@ public class SoundImporter {
}
int pos = -1;
loopChars: for (SoundTag tag : soundTags) {
loopChars:
for (SoundTag tag : soundTags) {
pos++;
int characterId = tag.getCharacterId();
List<File> existingFilesForSoundTag = new ArrayList<>();
@@ -670,7 +857,7 @@ public class SoundImporter {
for (SoundStreamFrameRange r : ranges.get(pos)) {
for (File sourceFile : existingFilesForSoundTag) {
if (sourceFile.getName().startsWith("" + characterId + "_" + (r.startFrame + 1) + "-")) {
try {
if (printOut) {
System.out.println("Importing character " + characterId + ", start frame " + r.startFrame + " from file " + sourceFile.getName());

View File

@@ -61,5 +61,3 @@ configurationFile.meta.showComments = Show configuration comments - set to true
configurationFile.meta.modifiedOnly = Store modified items only in this file - set to false (and exit app again to resave) to show all.
configurationFile.configuration = Section - Actual configuration
configuration.removed = WARNING: This configuration was REMOVED. It is unused.
exception.soundFormat.expected = Required format for import to this sound stream: %expected%\nFormat of selected file:%actual%\nPlease convert the file to the required format and try again.

View File

@@ -62,5 +62,3 @@ configurationFile.meta.showComments = Zobrazit koment\u00e1\u0159e ke konfigurac
configurationFile.meta.modifiedOnly = Ukl\u00e1dat do tohoto souboru pouze zm\u011bn\u011bn\u00e9 hodnoty - nastavte na false (a ukon\u010dete aplikaci pro nov\u00e9 ulo\u017een\u00ed) pro zobrazen\u00ed v\u0161eho.
configurationFile.configuration = Sekce - Vlastn\u00ed konfigurace
configuration.removed = VAROV\u00c1N\u00cd: Tato konfigurace byla ODSTRAN\u011aNA. Nepou\u017e\u00edv\u00e1 se.
exception.soundFormat.expected = Vy\u017eadovan\u00fd form\u00e1t pro import do tohoto zvukov\u00e9ho streamu: %expected%\nForm\u00e1t vybran\u00e9ho souboru: %actual%\nPros\u00edm zkonvertujte soubor do vy\u017eadovan\u00e9ho form\u00e1tu a zkuste to znovu.

View File

@@ -1,79 +0,0 @@
/*
* Copyright (C) 2010-2025 JPEXS, All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
*/
package com.jpexs.decompiler.flash.tags.base;
import com.jpexs.decompiler.flash.AppResources;
/**
*
* @author JPEXS
*/
public class SoundParametersMismatchException extends SoundImportException {
/**
*
* @param expectedSoundType
* @param expectedSoundSize
* @param expectedSoundRate
* @param expectedSoundFormat
*/
public SoundParametersMismatchException(
boolean expectedSoundType,
boolean expectedSoundSize,
int expectedSoundRate,
int expectedSoundFormat,
boolean actualSoundType,
boolean actualSoundSize,
int actualSoundRate,
int actualSoundFormat) {
super(AppResources.translate("exception.soundFormat.expected").replace("%expected%",
(expectedSoundType ? "stereo" : "mono") + " "
+ (expectedSoundSize ? "16bit" : "8bit") + " "
+ new int[]{5512, 11025, 22050, 44100}[expectedSoundRate] + " Hz"
+ " " + new String[]{
"uncompressed native endian",
"adpcm",
"mp3",
"uncompressed little endian",
"nellymoser 16 kHz",
"nellymoser 8 kHz",
"nellymoser",
"",
"",
"",
"",
"speex"
}[expectedSoundFormat]).replace("%actual%",
(actualSoundType ? "stereo" : "mono") + " "
+ (actualSoundSize ? "16bit" : "8bit") + " "
+ new int[]{5512, 11025, 22050, 44100}[actualSoundRate] + " Hz"
+ " " + new String[]{
"uncompressed native endian",
"adpcm",
"mp3",
"uncompressed little endian",
"nellymoser 16 kHz",
"nellymoser 8 kHz",
"nellymoser",
"",
"",
"",
"",
"speex"
}[actualSoundFormat]));
}
}

View File

@@ -52,7 +52,6 @@ public class MP3SOUNDDATA {
long initLen = mis.getPosition();
MarkingPushbackInputStream mpis = bitstream.getSource();
while (true) {
//System.err.println("initLen = "+initLen);
long posBefore = initLen + mpis.getPosition();
MP3FRAME frame = MP3FRAME.readFrame(bitstream, decoder);
if (frame == null) {
@@ -60,7 +59,7 @@ public class MP3SOUNDDATA {
}
long posAfter = initLen + mpis.getPosition();
frame.setFullData(Arrays.copyOfRange(data, (int) posBefore, (int) posAfter));
frames.add(frame);
frames.add(frame);
}
}