From 24041519225d5c8cd8f06e0eb6be28d7a17ad8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Wed, 13 Dec 2023 20:05:10 +0100 Subject: [PATCH] Fixed #2143 FLA Export / Sound playback - taking MP3 initial latency into account --- CHANGELOG.md | 2 + .../action/model/CallMethodActionItem.java | 2 +- .../flash/exporters/SoundExporter.java | 2 +- .../decompiler/flash/tags/DefineSoundTag.java | 19 +++++ .../flash/tags/SoundStreamHead2Tag.java | 5 ++ .../flash/tags/SoundStreamHeadTag.java | 5 ++ .../decompiler/flash/tags/base/SoundTag.java | 2 + .../flash/tags/gfx/DefineExternalSound.java | 5 ++ .../tags/gfx/DefineExternalStreamSound.java | 5 ++ .../flash/timeline/SoundStreamFrameRange.java | 5 ++ .../flash/types/sound/SoundFormat.java | 79 +++++++++++-------- .../decompiler/flash/xfl/XFLConverter.java | 38 ++++++--- .../flash/gui/GenericTagTreePanel.java | 3 + .../decompiler/flash/gui/SoundTagPlayer.java | 2 +- 14 files changed, 130 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c1aa8e3f..a62247a39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ All notable changes to this project will be documented in this file. - [#2148] AS2 Uninitialized class fields detector - [#2148] AS1/2 callmethod by register value - [#2148] AS2 Do not return undefined for setters +- [#2143] FLA Export / Sound playback - taking MP3 initial latency into account ### Changed - [#2120] Exported assets no longer take names from assigned classes if there is more than 1 assigned class @@ -3359,6 +3360,7 @@ Major version of SWF to XML export changed to 2. [#2145]: https://www.free-decompiler.com/flash/issues/2145 [#2142]: https://www.free-decompiler.com/flash/issues/2142 [#2148]: https://www.free-decompiler.com/flash/issues/2148 +[#2143]: https://www.free-decompiler.com/flash/issues/2143 [#2120]: https://www.free-decompiler.com/flash/issues/2120 [#1130]: https://www.free-decompiler.com/flash/issues/1130 [#1220]: https://www.free-decompiler.com/flash/issues/1220 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/action/model/CallMethodActionItem.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/action/model/CallMethodActionItem.java index 2e64ad7e9..d405a4bb3 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/action/model/CallMethodActionItem.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/action/model/CallMethodActionItem.java @@ -127,7 +127,7 @@ public class CallMethodActionItem extends ActionItem { scriptObject.toString(writer, localData); } if ( - !(((DirectValueActionItem)methodName).value instanceof RegisterNumber) + !(((DirectValueActionItem) methodName).value instanceof RegisterNumber) && IdentifiersDeobfuscation.isValidName(false, methodName.toStringNoQuotes(localData)) ) { writer.append("."); diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/SoundExporter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/SoundExporter.java index dd6f2bd3d..c6a4182ae 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/SoundExporter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/exporters/SoundExporter.java @@ -194,7 +194,7 @@ public class SoundExporter { } } else { List soundData = st.getRawSoundData(); - fmt.createWav(null, soundData, fos); + fmt.createWav(null, soundData, fos, st.getInitialLatency()); } } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineSoundTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineSoundTag.java index 021adf905..bba8265b5 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineSoundTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/DefineSoundTag.java @@ -31,6 +31,8 @@ import com.jpexs.helpers.ByteArrayRange; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * @@ -137,6 +139,9 @@ public class DefineSoundTag extends CharacterTag implements SoundTag { @Override public SoundExportFormat getExportFormat() { if (soundFormat == SoundFormat.FORMAT_MP3) { + if (getInitialLatency() > 0) { + return SoundExportFormat.WAV; + } return SoundExportFormat.MP3; } if (soundFormat == SoundFormat.FORMAT_ADPCM) { @@ -241,5 +246,19 @@ public class DefineSoundTag extends CharacterTag implements SoundTag { @Override public String getFlaExportName() { return "sound" + getCharacterId(); + } + + @Override + public int getInitialLatency() { + if (soundFormat == SoundFormat.FORMAT_MP3) { + SWFInputStream sis; + try { + sis = new SWFInputStream(null, soundData.getRangeData(0, 2)); + return sis.readSI16("seekSamples"); + } catch (IOException ex) { + //ignore + } + } + return 0; } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/SoundStreamHead2Tag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/SoundStreamHead2Tag.java index 6fd88c3b1..c9af04352 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/SoundStreamHead2Tag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/SoundStreamHead2Tag.java @@ -290,4 +290,9 @@ public class SoundStreamHead2Tag extends SoundStreamHeadTypeTag { public String getFlaExportName() { return "sound" + getCharacterId(); } + + @Override + public int getInitialLatency() { + return 0; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/SoundStreamHeadTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/SoundStreamHeadTag.java index f958aa82e..0a3c4b347 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/SoundStreamHeadTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/SoundStreamHeadTag.java @@ -299,4 +299,9 @@ public class SoundStreamHeadTag extends SoundStreamHeadTypeTag { public String getFlaExportName() { return "sound" + getCharacterId(); } + + @Override + public int getInitialLatency() { + return 0; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/SoundTag.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/SoundTag.java index 622967f67..7acd0a3e8 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/SoundTag.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/base/SoundTag.java @@ -65,4 +65,6 @@ public interface SoundTag extends TreeItem { public String getName(); public String getFlaExportName(); + + public int getInitialLatency(); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineExternalSound.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineExternalSound.java index 161d22e32..c268e14cc 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineExternalSound.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineExternalSound.java @@ -247,5 +247,10 @@ public class DefineExternalSound extends CharacterTag implements SoundTag { @Override public String getFlaExportName() { return "sound" + getCharacterId(); + } + + @Override + public int getInitialLatency() { + return 0; } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineExternalStreamSound.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineExternalStreamSound.java index b32671b9b..5fe400328 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineExternalStreamSound.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/tags/gfx/DefineExternalStreamSound.java @@ -260,4 +260,9 @@ public class DefineExternalStreamSound extends Tag implements CharacterIdTag, So public String getFlaExportName() { return "sound" + getCharacterId(); } + + @Override + public int getInitialLatency() { + return 0; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/SoundStreamFrameRange.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/SoundStreamFrameRange.java index 5d91bb295..5285740d7 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/SoundStreamFrameRange.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/timeline/SoundStreamFrameRange.java @@ -164,4 +164,9 @@ public class SoundStreamFrameRange implements TreeItem, SoundTag { public String getFlaExportName() { return head.getFlaExportName() + "_" + (startFrame + 1) + "-" + (endFrame + 1); } + + @Override + public int getInitialLatency() { + return 0; + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/sound/SoundFormat.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/sound/SoundFormat.java index 9b677f544..1d8383672 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/sound/SoundFormat.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/sound/SoundFormat.java @@ -23,6 +23,7 @@ import com.jpexs.helpers.utf8.Utf8Helper; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.util.Arrays; import java.util.List; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; @@ -86,28 +87,11 @@ public class SoundFormat { this.samplingRate = samplingRate; this.stereo = stereo; ensureFormat(); - } - - public byte[] decode(SWFInputStream sis) { - try { - return getDecoder().decode(sis); - } catch (IOException ex) { - return null; - } - } - - public boolean decode(SWFInputStream sis, OutputStream os) { - try { - getDecoder().decode(sis, os); - return true; - } catch (IOException ex) { - return false; - } - } + } public boolean play(SWFInputStream sis) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - if (!decode(sis, baos)) { + if (!SoundFormat.this.decode(sis, baos)) { return false; } @@ -200,7 +184,24 @@ public class SoundFormat { } } - public boolean createWav(SOUNDINFO soundInfo, List dataRanges, OutputStream os) throws IOException { + public byte[] decode(SWFInputStream sis) { + try { + return getDecoder().decode(sis); + } catch (IOException ex) { + return null; + } + } + + public boolean decode(SWFInputStream sis, OutputStream os) { + try { + getDecoder().decode(sis, os); + return true; + } catch (IOException ex) { + return false; + } + } + + public byte[] decode(SOUNDINFO soundInfo, List dataRanges, int skipSamples) throws IOException { ensureFormat(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); SoundDecoder decoder = getDecoder(); @@ -209,34 +210,48 @@ public class SoundFormat { sis.seek(dataRange.getPos()); decoder.decode(sis, baos); } - - /* - System.err.println("sampling rate:" + samplingRate); - System.err.println("len:" + baos.toByteArray().length); - */ + byte[] decodedData = baos.toByteArray(); + if (skipSamples > 0) { + byte[] data = decodedData; + data = Arrays.copyOfRange( + data, + skipSamples * 2 * (stereo ? 2 : 1), + data.length + ); + return data; + } + + return decodedData; + } + + public boolean createWav(SOUNDINFO soundInfo, List dataRanges, OutputStream os, int skipSamples) throws IOException { + + byte[] decodedData = decode(soundInfo, dataRanges, skipSamples); boolean convertedStereo = stereo; ByteArrayOutputStream baosFiltered; if (soundInfo == null) { - baosFiltered = baos; + baosFiltered = new ByteArrayOutputStream(); + baosFiltered.write(decodedData); } else { int inPoint = (soundInfo.hasInPoint ? (int) Math.round(soundInfo.inPoint * samplingRate / 44100.0) : 0); int outPoint = (soundInfo.hasOutPoint ? (int) Math.round(soundInfo.outPoint * samplingRate / 44100.0) : Integer.MAX_VALUE); - byte[] data = baos.toByteArray(); baosFiltered = new ByteArrayOutputStream(); int inPointBytes = inPoint * 2 /*16bit*/ * (stereo ? 2 : 1); - int outPointBytes = soundInfo.hasOutPoint ? outPoint * 2 /*16bit*/ * (stereo ? 2 : 1) : data.length; + //Q: Use skipSamples value? + + int outPointBytes = soundInfo.hasOutPoint ? outPoint * 2 /*16bit*/ * (stereo ? 2 : 1) : decodedData.length; for (int i = inPointBytes; i < outPointBytes; i += (stereo ? 4 : 2)) { - if (i + 1 >= data.length) { + if (i + 1 >= decodedData.length) { break; } - int left = ((data[i] & 0xff) + ((data[i + 1] & 0xff) << 8)) << 16 >> 16; + int left = ((decodedData[i] & 0xff) + ((decodedData[i + 1] & 0xff) << 8)) << 16 >> 16; int right = left; if (stereo) { - if (i + 3 >= data.length) { + if (i + 3 >= decodedData.length) { break; } - right = ((data[i + 2] & 0xff) + ((data[i + 3] & 0xff) << 8)) << 16 >> 16; + right = ((decodedData[i + 2] & 0xff) + ((decodedData[i + 3] & 0xff) << 8)) << 16 >> 16; } if (soundInfo.hasEnvelope) { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java index a358a793b..c0b5e5602 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java @@ -162,6 +162,7 @@ import java.awt.Font; import java.awt.Point; import java.awt.geom.Point2D; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -1961,7 +1962,7 @@ public class XFLConverter { writer.writeEndElement(); } - private void convertSoundMedia(SWF swf, ReadOnlyTagList tags, SoundTag symbol, XFLXmlWriter writer, HashMap files) throws XMLStreamException { + private void convertSoundMedia(SWF swf, ReadOnlyTagList tags, SoundTag symbol, XFLXmlWriter writer, HashMap files, HashMap datfiles) throws XMLStreamException { int soundFormat = 0; int soundRate = 0; boolean soundType = false; @@ -2034,6 +2035,7 @@ public class XFLConverter { logger.log(Level.SEVERE, null, ex); } } + int seekSamples = 0; if (soundFormat == SoundFormat.FORMAT_MP3) { exportFormat = "mp3"; if (!soundType) { //mono @@ -2043,9 +2045,13 @@ public class XFLConverter { try { SWFInputStream sis = new SWFInputStream(swf, soundData); MP3SOUNDDATA s = new MP3SOUNDDATA(sis, false); + if (s.seekSamples > 0) { + seekSamples = s.seekSamples; + exportFormat = "wav"; + } if (!s.frames.isEmpty()) { MP3FRAME frame = s.frames.get(0); - int bitRate = frame.getBitRate(); + int bitRate = frame.getBitRate() / 1000; switch (bitRate) { case 8: @@ -2084,8 +2090,7 @@ public class XFLConverter { case 160: bits = 17; break; - - } + } } } catch (IOException | IndexOutOfBoundsException ex) { logger.log(Level.SEVERE, null, ex); @@ -2095,10 +2100,22 @@ public class XFLConverter { SoundFormat fmt = st.getSoundFormat(); byte[] data = SWFInputStream.BYTE_ARRAY_EMPTY; try { - data = new SoundExporter().exportSound(st, SoundExportMode.MP3_WAV); + data = new SoundExporter().exportSound(st, seekSamples > 0 ? SoundExportMode.WAV : SoundExportMode.MP3_WAV); } catch (IOException ex) { logger.log(Level.SEVERE, null, ex); } + + String datFileName = null; + if (seekSamples > 0) { + long ts = getTimestamp(swf); + datFileName = "M " + (datfiles.size() + 1) + " " + ts + ".dat"; + try { + byte[] decodedData = st.getSoundFormat().decode(null, st.getRawSoundData(), seekSamples); + datfiles.put(datFileName, decodedData); + } catch (IOException ex) { + logger.log(Level.SEVERE, null, ex); + } + } String symbolFile = symbol.getFlaExportName() + "." + exportFormat; files.put(symbolFile, data); @@ -2107,6 +2124,9 @@ public class XFLConverter { "sourceLastImported", Long.toString(getTimestamp(swf)), "externalFileSize", Integer.toString(data.length)}); writer.writeAttribute("href", symbolFile); + if (datFileName != null) { + writer.writeAttribute("soundDataHRef", datFileName); + } writer.writeAttribute("format", rateMap[soundRate] + "kHz" + " " + (soundSize ? "16bit" : "8bit") + " " + (soundType ? "Stereo" : "Mono")); writer.writeAttribute("exportFormat", format); writer.writeAttribute("exportBits", bits); @@ -2227,7 +2247,7 @@ public class XFLConverter { statusStack.popStatus(); } else if (symbol instanceof DefineSoundTag) { statusStack.pushStatus(symbol.toString()); - convertSoundMedia(swf, tags, (DefineSoundTag) symbol, writer, files); + convertSoundMedia(swf, tags, (DefineSoundTag) symbol, writer, files, datfiles); boolean linkageExportForAS = false; if (characterClasses.containsKey(symbol.getCharacterId())) { @@ -2336,7 +2356,7 @@ public class XFLConverter { SoundStreamHeadTypeTag head = (SoundStreamHeadTypeTag) t; for (SoundStreamFrameRange range : head.getRanges()) { statusStack.pushStatus(range.toString()); - convertSoundMedia(swf, tags, range, writer, files); + convertSoundMedia(swf, tags, range, writer, files, datfiles); writer.writeEndElement(); mediaCount++; statusStack.popStatus(); @@ -2349,7 +2369,7 @@ public class XFLConverter { SoundStreamHeadTypeTag head = (SoundStreamHeadTypeTag) st; for (SoundStreamFrameRange range : head.getRanges()) { statusStack.pushStatus(range.toString()); - convertSoundMedia(swf, sprite.getTags(), range, writer, files); + convertSoundMedia(swf, sprite.getTags(), range, writer, files, datfiles); writer.writeEndElement(); mediaCount++; statusStack.popStatus(); @@ -3195,7 +3215,7 @@ public class XFLConverter { SWF swf = startSound.getSwf(); DefineSoundTag s = swf.getSound(startSound.soundId); if (s == null) { - logger.log(Level.WARNING, "Sount tag (ID={0}) was not found", startSound.soundId); + logger.log(Level.WARNING, "Sound tag (ID={0}) was not found", startSound.soundId); continue; } diff --git a/src/com/jpexs/decompiler/flash/gui/GenericTagTreePanel.java b/src/com/jpexs/decompiler/flash/gui/GenericTagTreePanel.java index e7d22c327..ff29d13bd 100644 --- a/src/com/jpexs/decompiler/flash/gui/GenericTagTreePanel.java +++ b/src/com/jpexs/decompiler/flash/gui/GenericTagTreePanel.java @@ -1290,6 +1290,9 @@ public class GenericTagTreePanel extends GenericTagPanel { return false; } + if (tag == null) { + return true; + } SWF swf = tag.getSwf(); assignTag(tag, editedTag); tag.setModified(true); diff --git a/src/com/jpexs/decompiler/flash/gui/SoundTagPlayer.java b/src/com/jpexs/decompiler/flash/gui/SoundTagPlayer.java index 5e009cc49..d2be680dd 100644 --- a/src/com/jpexs/decompiler/flash/gui/SoundTagPlayer.java +++ b/src/com/jpexs/decompiler/flash/gui/SoundTagPlayer.java @@ -192,7 +192,7 @@ public class SoundTagPlayer implements MediaDisplay { if (wavData == null) { List soundData = tag.getRawSoundData(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - tag.getSoundFormat().createWav(soundInfo, soundData, baos); + tag.getSoundFormat().createWav(soundInfo, soundData, baos, tag.getInitialLatency()); wavData = baos.toByteArray(); swf.putToCache(soundInfo, tag, wavData); }