diff --git a/CHANGELOG.md b/CHANGELOG.md index 79017f110..22f9d88ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file. - Folder preview of frames with time increasing - Flash viewer - Do not play StartSoundTag all over again on single frame - Flash viewer - StartSoundTag loops +- Flash viewer - Sound envelope handling ### Changed - [#1661] Slow rendering warning is optional with default to not display diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java index c855197c1..1a8f3924e 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java @@ -144,8 +144,11 @@ import com.jpexs.decompiler.flash.types.ColorTransform; import com.jpexs.decompiler.flash.types.MATRIX; import com.jpexs.decompiler.flash.types.RECT; import com.jpexs.decompiler.flash.types.SHAPE; +import com.jpexs.decompiler.flash.types.SOUNDENVELOPE; +import com.jpexs.decompiler.flash.types.SOUNDINFO; import com.jpexs.decompiler.flash.types.annotations.Internal; import com.jpexs.decompiler.flash.types.annotations.SWFField; +import com.jpexs.decompiler.flash.types.sound.SoundInfoSoundCacheEntry; import com.jpexs.decompiler.flash.xfl.FLAVersion; import com.jpexs.decompiler.flash.xfl.XFLConverter; import com.jpexs.decompiler.flash.xfl.XFLExportSettings; @@ -341,7 +344,7 @@ public final class SWF implements SWFContainerItem, Timelined { private final Cache shapeExportDataCache = Cache.getInstance(true, true, "shapeExportData"); @Internal - private final Cache soundCache = Cache.getInstance(false, false, "sound"); + private final Cache soundCache = Cache.getInstance(false, false, "sound"); @Internal public final AS2Cache as2Cache = new AS2Cache(); @@ -2577,9 +2580,10 @@ public final class SWF implements SWFContainerItem, Timelined { return null; } - public byte[] getFromCache(SoundTag soundTag) { - if (soundCache.contains(soundTag)) { - return soundCache.get(soundTag); + public byte[] getFromCache(SOUNDINFO soundInfo, SoundTag soundTag) { + SoundInfoSoundCacheEntry key = new SoundInfoSoundCacheEntry(soundInfo, soundTag); + if (soundCache.contains(key)) { + return soundCache.get(key); } return null; } @@ -2590,8 +2594,9 @@ public final class SWF implements SWFContainerItem, Timelined { } } - public void putToCache(SoundTag soundTag, byte[] data) { - soundCache.put(soundTag, data); + public void putToCache(SOUNDINFO soundInfo, SoundTag soundTag, byte[] data) { + SoundInfoSoundCacheEntry key = new SoundInfoSoundCacheEntry(soundInfo, soundTag); + soundCache.put(key, data); } public void clearImageCache() { 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 502e8fe09..2dc983fe4 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 @@ -12,7 +12,8 @@ * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public - * License along with this library. */ + * License along with this library. + */ package com.jpexs.decompiler.flash.exporters; import com.jpexs.decompiler.flash.AbortRetryIgnoreHandler; @@ -156,7 +157,7 @@ public class SoundExporter { } } else { List soundData = st.getRawSoundData(); - fmt.createWav(soundData, fos); + fmt.createWav(null, soundData, fos); } } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/SOUNDENVELOPE.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/SOUNDENVELOPE.java index 9d8e22f30..27c11e87b 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/SOUNDENVELOPE.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/SOUNDENVELOPE.java @@ -12,7 +12,8 @@ * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public - * License along with this library. */ + * License along with this library. + */ package com.jpexs.decompiler.flash.types; import com.jpexs.decompiler.flash.types.annotations.SWFType; @@ -32,4 +33,38 @@ public class SOUNDENVELOPE implements Serializable { @SWFType(BasicType.UI16) public int rightLevel; + + @Override + public int hashCode() { + int hash = 7; + hash = 29 * hash + (int) (this.pos44 ^ (this.pos44 >>> 32)); + hash = 29 * hash + this.leftLevel; + hash = 29 * hash + this.rightLevel; + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SOUNDENVELOPE other = (SOUNDENVELOPE) obj; + if (this.pos44 != other.pos44) { + return false; + } + if (this.leftLevel != other.leftLevel) { + return false; + } + if (this.rightLevel != other.rightLevel) { + return false; + } + return true; + } + } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/SOUNDINFO.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/SOUNDINFO.java index 04e8a9c75..04ed9028b 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/SOUNDINFO.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/SOUNDINFO.java @@ -12,13 +12,15 @@ * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public - * License along with this library. */ + * License along with this library. + */ package com.jpexs.decompiler.flash.types; import com.jpexs.decompiler.flash.types.annotations.Conditional; import com.jpexs.decompiler.flash.types.annotations.Reserved; import com.jpexs.decompiler.flash.types.annotations.SWFType; import java.io.Serializable; +import java.util.Arrays; /** * @@ -56,4 +58,70 @@ public class SOUNDINFO implements Serializable { @Conditional("hasEnvelope") public SOUNDENVELOPE[] envelopeRecords = new SOUNDENVELOPE[0]; + + @Override + public int hashCode() { + int hash = 3; + hash = 17 * hash + this.reserved; + hash = 17 * hash + (this.syncStop ? 1 : 0); + hash = 17 * hash + (this.syncNoMultiple ? 1 : 0); + hash = 17 * hash + (this.hasEnvelope ? 1 : 0); + hash = 17 * hash + (this.hasLoops ? 1 : 0); + hash = 17 * hash + (this.hasOutPoint ? 1 : 0); + hash = 17 * hash + (this.hasInPoint ? 1 : 0); + hash = 17 * hash + (int) (this.inPoint ^ (this.inPoint >>> 32)); + hash = 17 * hash + (int) (this.outPoint ^ (this.outPoint >>> 32)); + hash = 17 * hash + this.loopCount; + hash = 17 * hash + Arrays.deepHashCode(this.envelopeRecords); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SOUNDINFO other = (SOUNDINFO) obj; + if (this.reserved != other.reserved) { + return false; + } + if (this.syncStop != other.syncStop) { + return false; + } + if (this.syncNoMultiple != other.syncNoMultiple) { + return false; + } + if (this.hasEnvelope != other.hasEnvelope) { + return false; + } + if (this.hasLoops != other.hasLoops) { + return false; + } + if (this.hasOutPoint != other.hasOutPoint) { + return false; + } + if (this.hasInPoint != other.hasInPoint) { + return false; + } + if (this.inPoint != other.inPoint) { + return false; + } + if (this.outPoint != other.outPoint) { + return false; + } + if (this.loopCount != other.loopCount) { + return false; + } + if (!Arrays.deepEquals(this.envelopeRecords, other.envelopeRecords)) { + return false; + } + return true; + } + } 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 e55ab2b9f..593fcf38b 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 @@ -12,10 +12,12 @@ * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public - * License along with this library. */ + * License along with this library. + */ package com.jpexs.decompiler.flash.types.sound; import com.jpexs.decompiler.flash.SWFInputStream; +import com.jpexs.decompiler.flash.types.SOUNDINFO; import com.jpexs.helpers.ByteArrayRange; import com.jpexs.helpers.utf8.Utf8Helper; import java.io.ByteArrayOutputStream; @@ -198,7 +200,7 @@ public class SoundFormat { } } - public boolean createWav(List dataRanges, OutputStream os) throws IOException { + public boolean createWav(SOUNDINFO soundInfo, List dataRanges, OutputStream os) throws IOException { ensureFormat(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); SoundDecoder decoder = getDecoder(); @@ -208,8 +210,56 @@ public class SoundFormat { decoder.decode(sis, baos); } + /* + System.err.println("sampling rate:" + samplingRate); + System.err.println("len:" + baos.toByteArray().length); + */ + boolean convertedStereo = stereo; + + ByteArrayOutputStream baosFiltered; + if (soundInfo == null) { + baosFiltered = baos; + } 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; + for (int i = inPointBytes; i < outPointBytes; i += (stereo ? 4 : 2)) { + int left = ((data[i] & 0xff) + ((data[i + 1] & 0xff) << 8)) << 16 >> 16; + int right = left; + if (stereo) { + right = ((data[i + 2] & 0xff) + ((data[i + 3] & 0xff) << 8)) << 16 >> 16; + } + + if (soundInfo.hasEnvelope) { + for (int e = 0; e < soundInfo.envelopeRecords.length - 1; e++) { + int envPosBytes = inPointBytes + (int) (soundInfo.envelopeRecords[e].pos44 * samplingRate / 44100.0 * 2 * (stereo ? 2 : 1)); + int envNextPosBytes = inPointBytes + (int) (soundInfo.envelopeRecords[e + 1].pos44 * samplingRate / 44100.0 * 2 * (stereo ? 2 : 1)); + if (i >= envPosBytes && i <= envNextPosBytes) { + double pos = (i - envPosBytes) / (double) (envNextPosBytes - envPosBytes); + + int leftLevel = (int) (soundInfo.envelopeRecords[e].leftLevel + (soundInfo.envelopeRecords[e + 1].leftLevel - soundInfo.envelopeRecords[e].leftLevel) * pos); + int rightLevel = (int) (soundInfo.envelopeRecords[e].rightLevel + (soundInfo.envelopeRecords[e + 1].rightLevel - soundInfo.envelopeRecords[e].rightLevel) * pos); + double leftMultiplier = leftLevel / 32768.0; + double rightMultiplier = rightLevel / 32768.0; + + left = (int) Math.round(left * leftMultiplier); + right = (int) Math.round(right * rightMultiplier); + break; + } + } + } + + writeLE(baosFiltered, left, 2); + writeLE(baosFiltered, right, 2); + } + convertedStereo = true; + } + try { - createWavFromPcmData(os, samplingRate, true, stereo, baos.toByteArray()); + createWavFromPcmData(os, samplingRate, true, convertedStereo, baosFiltered.toByteArray()); return true; } catch (IOException ex) { return false; diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/sound/SoundInfoSoundCacheEntry.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/sound/SoundInfoSoundCacheEntry.java new file mode 100644 index 000000000..07347c34c --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/types/sound/SoundInfoSoundCacheEntry.java @@ -0,0 +1,49 @@ +package com.jpexs.decompiler.flash.types.sound; + +import com.jpexs.decompiler.flash.tags.base.SoundTag; +import com.jpexs.decompiler.flash.types.SOUNDINFO; +import java.util.Objects; + +/** + * + * @author JPEXS + */ +public class SoundInfoSoundCacheEntry { + public SOUNDINFO soundInfo; + public SoundTag soundTag; + + public SoundInfoSoundCacheEntry(SOUNDINFO soundInfo, SoundTag soundTag) { + this.soundInfo = soundInfo; + this.soundTag = soundTag; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 47 * hash + Objects.hashCode(this.soundInfo); + hash = 47 * hash + Objects.hashCode(this.soundTag); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SoundInfoSoundCacheEntry other = (SoundInfoSoundCacheEntry) obj; + if (!Objects.equals(this.soundInfo, other.soundInfo)) { + return false; + } + if (!Objects.equals(this.soundTag, other.soundTag)) { + return false; + } + return true; + } + +} diff --git a/libsrc/ffdec_lib/testdata/graphics/POOL-Pool_Shot-709343898.wav b/libsrc/ffdec_lib/testdata/graphics/POOL-Pool_Shot-709343898.wav deleted file mode 100644 index cc2847e47..000000000 Binary files a/libsrc/ffdec_lib/testdata/graphics/POOL-Pool_Shot-709343898.wav and /dev/null differ diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics.swf b/libsrc/ffdec_lib/testdata/graphics/graphics.swf index 634a429dd..7ed228fe0 100644 Binary files a/libsrc/ffdec_lib/testdata/graphics/graphics.swf and b/libsrc/ffdec_lib/testdata/graphics/graphics.swf differ diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics/DOMDocument.xml b/libsrc/ffdec_lib/testdata/graphics/graphics/DOMDocument.xml index d177fe515..cff881d67 100644 --- a/libsrc/ffdec_lib/testdata/graphics/graphics/DOMDocument.xml +++ b/libsrc/ffdec_lib/testdata/graphics/graphics/DOMDocument.xml @@ -1,12 +1,13 @@ - + + @@ -31,7 +32,7 @@ - + @@ -45,7 +46,7 @@ - + @@ -114,7 +115,7 @@ - + @@ -161,7 +162,7 @@ - + @@ -773,10 +774,10 @@ !3980 1990|2980 1990!2980 1990|2980 990!2980 990|3980 990!3980 990|3980 1990"/> - + @@ -1375,18 +1376,18 @@ - + - - + + @@ -2108,12 +2109,22 @@ - + + + + + + + - - - + + + + + + + @@ -3417,7 +3428,41 @@ - 058 Sound envelope + 058 Sound in/outpoint + + + + + + + + + + + + + + + + + 059 Sound envelope + + + + + + + + + + + + + + + + + 060 Full song @@ -3433,6 +3478,15 @@ + + + + + + + + + @@ -3444,14 +3498,5 @@ - - - - - - - - - \ No newline at end of file diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/Yung Raf - Nothing To Lose.mp3 b/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/Yung Raf - Nothing To Lose.mp3 new file mode 100644 index 000000000..b4bbdcee2 Binary files /dev/null and b/libsrc/ffdec_lib/testdata/graphics/graphics/LIBRARY/Yung Raf - Nothing To Lose.mp3 differ diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics/META-INF/metadata.xml b/libsrc/ffdec_lib/testdata/graphics/graphics/META-INF/metadata.xml index a2842583e..4f8c8e983 100644 --- a/libsrc/ffdec_lib/testdata/graphics/graphics/META-INF/metadata.xml +++ b/libsrc/ffdec_lib/testdata/graphics/graphics/META-INF/metadata.xml @@ -5,8 +5,8 @@ xmlns:xmp="http://ns.adobe.com/xap/1.0/"> Adobe Flash Professional CS6 - build 481 2021-03-14T08:29:20+01:00 - 2021-03-23T18:33:42+01:00 - 2021-03-23T18:33:42+01:00 + 2021-03-23T21:35:13+01:00 + 2021-03-23T21:35:13+01:00 @@ -15,7 +15,7 @@ - xmp.iid:875CB1E9FD8BEB11AEBE9CDB97D5B601 + xmp.iid:8C5CB1E9FD8BEB11AEBE9CDB97D5B601 xmp.did:D6D3FE199784EB1187FEAE6972EC5115 xmp.did:D6D3FE199784EB1187FEAE6972EC5115 @@ -80,6 +80,12 @@ 2021-03-14T08:29:20+01:00 Adobe Flash Professional CS6 - build 481 + + created + xmp.iid:8C5CB1E9FD8BEB11AEBE9CDB97D5B601 + 2021-03-14T08:29:20+01:00 + Adobe Flash Professional CS6 - build 481 + diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics/bin/M 6 1616534947.dat b/libsrc/ffdec_lib/testdata/graphics/graphics/bin/M 6 1616534947.dat new file mode 100644 index 000000000..4573075e0 Binary files /dev/null and b/libsrc/ffdec_lib/testdata/graphics/graphics/bin/M 6 1616534947.dat differ diff --git a/libsrc/ffdec_lib/testdata/graphics/graphics/bin/SymDepend.cache b/libsrc/ffdec_lib/testdata/graphics/graphics/bin/SymDepend.cache index b46ec6d29..345f67c47 100644 Binary files a/libsrc/ffdec_lib/testdata/graphics/graphics/bin/SymDepend.cache and b/libsrc/ffdec_lib/testdata/graphics/graphics/bin/SymDepend.cache differ diff --git a/src/com/jpexs/decompiler/flash/gui/ImagePanel.java b/src/com/jpexs/decompiler/flash/gui/ImagePanel.java index ea106881d..6895086b6 100644 --- a/src/com/jpexs/decompiler/flash/gui/ImagePanel.java +++ b/src/com/jpexs/decompiler/flash/gui/ImagePanel.java @@ -2039,7 +2039,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { loopCount = Math.max(1, soundInfo.loopCount); } - sp = new SoundTagPlayer(st, loopCount, false); + sp = new SoundTagPlayer(soundInfo, st, loopCount, false); sp.addEventListener(new MediaDisplayListener() { @Override public void mediaDisplayStateChanged(MediaDisplay source) { diff --git a/src/com/jpexs/decompiler/flash/gui/MainPanel.java b/src/com/jpexs/decompiler/flash/gui/MainPanel.java index 864386a0e..bf02f1850 100644 --- a/src/com/jpexs/decompiler/flash/gui/MainPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/MainPanel.java @@ -3606,7 +3606,7 @@ public final class MainPanel extends JPanel implements TreeSelectionListener, Se previewPanel.showImagePanel(new SerializableImage(View.loadImage("sound32"))); previewPanel.setImageReplaceButtonVisible(((Tag) treeItem).isReadOnly() && (treeItem instanceof DefineSoundTag), false); try { - SoundTagPlayer soundThread = new SoundTagPlayer((SoundTag) treeItem, Configuration.loopMedia.get() ? Integer.MAX_VALUE : 1, true); + SoundTagPlayer soundThread = new SoundTagPlayer(null, (SoundTag) treeItem, Configuration.loopMedia.get() ? Integer.MAX_VALUE : 1, true); previewPanel.setMedia(soundThread); } catch (LineUnavailableException | IOException | UnsupportedAudioFileException ex) { logger.log(Level.SEVERE, null, ex); diff --git a/src/com/jpexs/decompiler/flash/gui/SoundTagPlayer.java b/src/com/jpexs/decompiler/flash/gui/SoundTagPlayer.java index 056bee3ae..838661329 100644 --- a/src/com/jpexs/decompiler/flash/gui/SoundTagPlayer.java +++ b/src/com/jpexs/decompiler/flash/gui/SoundTagPlayer.java @@ -22,6 +22,7 @@ import com.jpexs.decompiler.flash.gui.player.MediaDisplayListener; import com.jpexs.decompiler.flash.gui.player.Zoom; import com.jpexs.decompiler.flash.tags.Tag; import com.jpexs.decompiler.flash.tags.base.SoundTag; +import com.jpexs.decompiler.flash.types.SOUNDINFO; import com.jpexs.helpers.ByteArrayRange; import java.awt.Color; import java.awt.image.BufferedImage; @@ -88,7 +89,7 @@ public class SoundTagPlayer implements MediaDisplay { private static final int FRAME_DIVISOR = 8000; - public SoundTagPlayer(final SoundTag tag, int loops, boolean async) throws LineUnavailableException, IOException, UnsupportedAudioFileException { + public SoundTagPlayer(final SOUNDINFO soundInfo, final SoundTag tag, int loops, boolean async) throws LineUnavailableException, IOException, UnsupportedAudioFileException { this.tag = tag; this.loopCount = loops; clip = (Clip) AudioSystem.getLine(new Line.Info(Clip.class)); @@ -135,13 +136,13 @@ public class SoundTagPlayer implements MediaDisplay { if (!async) { paused = true; - openSound(tag); + openSound(soundInfo, tag); } else { new Thread() { @Override public void run() { try { - openSound(tag); + openSound(soundInfo, tag); } catch (IOException | LineUnavailableException | UnsupportedAudioFileException ex) { Logger.getLogger(SoundTagPlayer.class.getName()).log(Level.SEVERE, null, ex); } @@ -155,15 +156,15 @@ public class SoundTagPlayer implements MediaDisplay { } } - private void openSound(SoundTag tag) throws IOException, LineUnavailableException, UnsupportedAudioFileException { + private void openSound(SOUNDINFO soundInfo, SoundTag tag) throws IOException, LineUnavailableException, UnsupportedAudioFileException { SWF swf = ((Tag) tag).getSwf(); - byte[] wavData = swf.getFromCache(tag); + byte[] wavData = swf.getFromCache(soundInfo, tag); if (wavData == null) { List soundData = tag.getRawSoundData(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - tag.getSoundFormat().createWav(soundData, baos); + tag.getSoundFormat().createWav(soundInfo, soundData, baos); wavData = baos.toByteArray(); - swf.putToCache(tag, wavData); + swf.putToCache(soundInfo, tag, wavData); } synchronized (playLock) {