From a0e481b1eb370a503d6c2d14ba3517faaa0babb8 Mon Sep 17 00:00:00 2001 From: Mike S Date: Sat, 13 Feb 2010 21:55:00 +0000 Subject: [PATCH] v0.91.0 rev1155 (13 Feb 2010) - Fixed yuv (Yuv4mpeg2) saving (pixel order was wrong) - Added saving as AVI in YUV format (specifically YV12 codec) for the highest quality output - Added -preciseav option - Fixed player from ending movies too quickly - Improved handling of multiple parallel video + audio streams - Properly syncs audio and video - Now handles Chrono Cross ending movie - Index files slightly updated, so likely incompatable with 0.90.0 versions - Fixed audio -vol option - Fixed video -jpg option - Fixed video -frames option Internals: - Began adding api for letting index file specify disc file - Added ant build script - Moves lgpl code into separate source folder - Added ArgParser source to the repo (for simplicity) - Softened movie detection logic - Several optimizations, including - Added object pool to player design - Cleaning and simplification where possible - Audio and video saving pipeline almost completely rewritten - Simplified the player design - Modularized AVI writer design - Fixed various bugs (frame rate calculation in some cases, NPEs, etc.) Known Problems: - Frame rate detection is still pretty bad - Audio + Video player on Linux might be choppy - Gives way too many errors when encountering CD audio track - Not all command-line options are working, and many haven't been well tested - 'psx' video decoder quality is not verified --- CHANGES.txt | 32 + CREDITS.txt | 3 +- LICENSE.txt | 45 +- PlayStation1_STR_format.txt | 16 +- TODO.txt | 176 +- argparser.zip | Bin 132403 -> 0 bytes build.xml | 95 + jPSXdec-manual.odt | Bin 23327 -> 17185 bytes .../psx/video/mdec/idct/simple_idct.java | 0 src/argparser/ArgParseException.java | 33 + src/argparser/ArgParser.java | 2266 +++++++++++++++++ src/argparser/BooleanHolder.java | 33 + src/argparser/COPYRIGHT | 12 + src/argparser/CharHolder.java | 35 + src/argparser/DoubleHolder.java | 34 + src/argparser/FloatHolder.java | 35 + src/argparser/IntHolder.java | 34 + src/argparser/LongHolder.java | 34 + src/argparser/ObjectHolder.java | 33 + src/argparser/StringHolder.java | 34 + src/argparser/StringScanException.java | 37 + src/argparser/StringScanner.java | 633 +++++ src/argparser/package.html | 7 + src/jpsxdec/LogToFile.properties | 2 +- src/jpsxdec/Main.java | 37 +- src/jpsxdec/MainCommandLineParser.java | 5 +- src/jpsxdec/MediaPlayer.java | 396 +-- ....java => SourceDataLineAudioReceiver.java} | 33 +- src/jpsxdec/cdreaders/CDFileSectorReader.java | 5 +- src/jpsxdec/cdreaders/CDSector.java | 6 +- src/jpsxdec/formats/JavaImageFormat.java | 2 +- src/jpsxdec/formats/RgbIntImage.java | 22 +- src/jpsxdec/formats/Yuv4mpeg2.java | 99 +- src/jpsxdec/formats/Yuv4mpeg2Writer.java | 44 +- ...ncmdlinehelp.txt => main_cmdline_help.dat} | 2 +- src/jpsxdec/player/AudioChunkQueue.java | 39 + src/jpsxdec/player/AudioPositionTest.java | 66 + src/jpsxdec/player/AudioProcessor.java | 32 +- src/jpsxdec/player/DemuxReader.java | 8 +- src/jpsxdec/player/IAudioDecoder.java | 62 - src/jpsxdec/player/IAudioVideoReader.java | 9 +- src/jpsxdec/player/IDecodableAudioChunk.java | 4 + src/jpsxdec/player/IDecodableFrame.java | 11 +- src/jpsxdec/player/IVideoDecoder.java | 49 - .../player/MultiStateBlockingQueue.java | 197 +- src/jpsxdec/player/ObjectPool.java | 44 + src/jpsxdec/player/PlayController.java | 30 +- src/jpsxdec/player/VideoFrame.java | 53 - src/jpsxdec/player/VideoPlayer.java | 90 +- src/jpsxdec/player/VideoProcessor.java | 63 +- src/jpsxdec/player/VideoTimer.java | 2 +- .../plugins/ConsoleProgressListener.java | 8 + src/jpsxdec/plugins/DiscIndex.java | 161 +- .../plugins/DiscItemSerialization.java | 3 +- src/jpsxdec/plugins/DiscItemStreaming.java | 9 +- src/jpsxdec/plugins/IdentifiedSector.java | 22 +- src/jpsxdec/plugins/JPSXPlugin.java | 7 +- src/jpsxdec/plugins/ProgressListener.java | 9 + .../SectorISO9660DirectoryRecords.java | 2 +- .../iso9660/SectorISO9660PathTable.java | 2 +- .../SectorISO9660VolumePrimaryDescriptor.java | 2 +- .../psx/alice/SectorAliceFrameChunk.java | 18 +- .../psx/alice/SectorAliceFrameChunkNull.java | 62 +- .../plugins/psx/lain/JPSXPluginLain.java | 3 + src/jpsxdec/plugins/psx/lain/Lain_LAPKS.java | 2 +- .../plugins/psx/lain/SectorLainVideo.java | 105 + .../psx/square/DiscItemSquareAudioStream.java | 66 +- .../psx/square/SectorChronoXAudio.java | 14 +- .../psx/square/SectorChronoXVideo.java | 29 - .../psx/square/SectorChronoXVideoNull.java | 8 +- .../plugins/psx/square/SectorFF7Video.java | 193 +- src/jpsxdec/plugins/psx/square/SectorFF8.java | 10 +- src/jpsxdec/plugins/psx/square/SectorFF9.java | 10 +- .../plugins/psx/str/AudioVideoSync.java | 71 + .../psx/str/DemuxMovieWriterBuilder.java | 1145 --------- .../plugins/psx/str/DiscItemSTRVideo.java | 89 +- ...ramePushDemuxer.java => FrameDemuxer.java} | 162 +- .../plugins/psx/str/IDemuxReceiver.java | 14 + src/jpsxdec/plugins/psx/str/IVideoSector.java | 4 +- .../plugins/psx/str/JPSXPluginVideo.java | 12 +- .../plugins/psx/str/STRVideoSaver.java | 116 +- ...ovieWriter.java => SectorMovieWriter.java} | 15 +- .../psx/str/SectorMovieWriterBuilder.java | 623 +++++ .../plugins/psx/str/SectorMovieWriters.java | 748 ++++++ src/jpsxdec/plugins/psx/str/SectorSTR.java | 49 +- src/jpsxdec/plugins/psx/str/VideoSync.java | 72 + src/jpsxdec/plugins/psx/video/DemuxImage.java | 52 +- .../psx/video/decode/ArrayBitReader.java | 6 +- .../video/decode/DemuxFrameUncompressor.java | 2 +- .../decode/DemuxFrameUncompressor_FF7.java | 66 +- .../decode/DemuxFrameUncompressor_Lain.java | 18 +- .../decode/DemuxFrameUncompressor_STRv2.java | 24 +- .../decode/DemuxFrameUncompressor_STRv3.java | 20 +- .../psx/video/encode/DemuxQualityEditor.java | 11 +- .../plugins/psx/video/encode/MdecEncoder.java | 4 +- .../psx/video/encode/STRFrameReplacer.java | 3 +- .../psx/video/encode/STRRecompressor.java | 9 +- .../psx/video/mdec/MdecDecoder_double.java | 49 +- .../psx/video/mdec/MdecDecoder_int.java | 23 +- .../plugins/xa/AudioStreamsCombiner.java | 230 ++ src/jpsxdec/plugins/xa/AudioSync.java | 54 + ...ioStream.java => DiscItemAudioStream.java} | 39 +- .../plugins/xa/DiscItemXAAudioStream.java | 48 +- src/jpsxdec/plugins/xa/IAudioReceiver.java | 15 + ...rDecoder.java => IAudioSectorDecoder.java} | 16 +- ...udioWriter.java => SectorAudioWriter.java} | 14 +- ...der.java => SectorAudioWriterBuilder.java} | 81 +- src/jpsxdec/plugins/xa/SectorXA.java | 15 +- src/jpsxdec/plugins/xa/XAAudioItemSaver.java | 11 +- src/jpsxdec/util/Fraction.java | 16 + src/jpsxdec/util/IO.java | 19 + src/jpsxdec/util/aviwriter/AviWriter.java | 780 ++---- src/jpsxdec/util/aviwriter/AviWriterDIB.java | 166 ++ src/jpsxdec/util/aviwriter/AviWriterMJPG.java | 232 ++ src/jpsxdec/util/aviwriter/AviWriterYV12.java | 77 + 115 files changed, 7765 insertions(+), 3239 deletions(-) delete mode 100644 argparser.zip create mode 100644 build.xml rename {src => src-lgpl}/jpsxdec/plugins/psx/video/mdec/idct/simple_idct.java (100%) create mode 100644 src/argparser/ArgParseException.java create mode 100644 src/argparser/ArgParser.java create mode 100644 src/argparser/BooleanHolder.java create mode 100644 src/argparser/COPYRIGHT create mode 100644 src/argparser/CharHolder.java create mode 100644 src/argparser/DoubleHolder.java create mode 100644 src/argparser/FloatHolder.java create mode 100644 src/argparser/IntHolder.java create mode 100644 src/argparser/LongHolder.java create mode 100644 src/argparser/ObjectHolder.java create mode 100644 src/argparser/StringHolder.java create mode 100644 src/argparser/StringScanException.java create mode 100644 src/argparser/StringScanner.java create mode 100644 src/argparser/package.html rename src/jpsxdec/{util/SourceDataLineAudioOutputStream.java => SourceDataLineAudioReceiver.java} (67%) rename src/jpsxdec/{maincmdlinehelp.txt => main_cmdline_help.dat} (93%) create mode 100644 src/jpsxdec/player/AudioChunkQueue.java create mode 100644 src/jpsxdec/player/AudioPositionTest.java delete mode 100644 src/jpsxdec/player/IAudioDecoder.java delete mode 100644 src/jpsxdec/player/IVideoDecoder.java create mode 100644 src/jpsxdec/player/ObjectPool.java delete mode 100644 src/jpsxdec/player/VideoFrame.java create mode 100644 src/jpsxdec/plugins/psx/lain/SectorLainVideo.java create mode 100644 src/jpsxdec/plugins/psx/str/AudioVideoSync.java delete mode 100644 src/jpsxdec/plugins/psx/str/DemuxMovieWriterBuilder.java rename src/jpsxdec/plugins/psx/str/{StrFramePushDemuxer.java => FrameDemuxer.java} (61%) create mode 100644 src/jpsxdec/plugins/psx/str/IDemuxReceiver.java rename src/jpsxdec/plugins/psx/str/{DemuxMovieWriter.java => SectorMovieWriter.java} (87%) create mode 100644 src/jpsxdec/plugins/psx/str/SectorMovieWriterBuilder.java create mode 100644 src/jpsxdec/plugins/psx/str/SectorMovieWriters.java create mode 100644 src/jpsxdec/plugins/psx/str/VideoSync.java create mode 100644 src/jpsxdec/plugins/xa/AudioStreamsCombiner.java create mode 100644 src/jpsxdec/plugins/xa/AudioSync.java rename src/jpsxdec/plugins/xa/{IDiscItemAudioStream.java => DiscItemAudioStream.java} (65%) create mode 100644 src/jpsxdec/plugins/xa/IAudioReceiver.java rename src/jpsxdec/plugins/xa/{IDiscItemAudioSectorDecoder.java => IAudioSectorDecoder.java} (86%) rename src/jpsxdec/plugins/xa/{PCM16bitAudioWriter.java => SectorAudioWriter.java} (89%) rename src/jpsxdec/plugins/xa/{PCM16bitAudioWriterBuilder.java => SectorAudioWriterBuilder.java} (76%) create mode 100644 src/jpsxdec/util/aviwriter/AviWriterDIB.java create mode 100644 src/jpsxdec/util/aviwriter/AviWriterMJPG.java create mode 100644 src/jpsxdec/util/aviwriter/AviWriterYV12.java diff --git a/CHANGES.txt b/CHANGES.txt index 9e01b24..792c588 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,35 @@ +v0.91.0 rev1155 (13 Feb 2010) + - Fixed yuv (Yuv4mpeg2) saving (pixel order was wrong) + - Added saving as AVI in YUV format (specifically YV12 codec) + for the highest quality output + - Added -preciseav option + - Fixed player from ending movies too quickly + - Improved handling of multiple parallel video + audio streams + - Properly syncs audio and video + - Now handles Chrono Cross ending movie + - Index files slightly updated, so likely incompatable with 0.90.0 versions + - Fixed audio -vol option + - Fixed video -jpg option + - Fixed video -frames option + Internals: + - Began adding api for letting index file specify disc file + - Added ant build script + - Moves lgpl code into separate source folder + - Added ArgParser source to the repo (for simplicity) + - Softened movie detection logic + - Several optimizations, including + - Added object pool to player design + - Cleaning and simplification where possible + - Audio and video saving pipeline almost completely rewritten + - Simplified the player design + - Modularized AVI writer design + - Fixed various bugs (frame rate calculation in some cases, NPEs, etc.) + Known Problems: + - Frame rate detection is still pretty bad + - Audio + Video player on Linux might be choppy + - Gives way too many errors when encountering CD audio track + - Not all command-line options are working, and many haven't been well tested + - 'psx' video decoder quality is not verified v0.90.0 (18 Jan 2010) - LICENSE CHANGED TO NON-COMMERCIAL USE ONLY - Added a very basic real-time player diff --git a/CREDITS.txt b/CREDITS.txt index f999e5c..95d6133 100644 --- a/CREDITS.txt +++ b/CREDITS.txt @@ -21,8 +21,7 @@ Their STR decoding source code PSXMDECDecoder.cpp was invaluable Were Afraid to Ask." Compiled / edited by Joshua Walker. Perhaps the most valuable reference for any kind of PSX hacking, -especially the PSX assembly instruction set (note that it has CrCb -reversal error mentioned in ch 2.2). +especially the PSX assembly instruction set. smf, developer for MAME, for figuring out that everyone was getting the order of CrCb wrong (http://smf.mameworld.info/?m=200603). diff --git a/LICENSE.txt b/LICENSE.txt index 5def31f..bc36aa8 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -47,32 +47,49 @@ under a MIT license. A few individual components are licensed under compatible licenses. -jpsxdec/plugins/psx/video/mdec/idct/simple_idct.java -copyright (c) Michael Niedermayer -Originally from ffmpeg (ported by Alexander Strange) and is licensed under -the GNU Lesser General Public License Version 2.1. -The text of this license is contained in the file lgpl-2.1.txt -## Note that this file must be included in a separate .jar with ## -## binary distributions of jPSXdec so as not to violate the LGPL. ## - jpsxdec/plugins/psx/video/mdec/idct/StephensIDCT.java -Originally came from code written by Stephen Manley. -He generously offers his code free, without restriction. +Originally from code written by Stephen Manley. http://www.nyx.net/~smanley/ +He generously offers his code free, without restriction. jpsxdec/plugins/psx/video/mdec/idct/J2meMpegIDCT.java Copyright (c) 2009 Sequence Point Software S.L. +Originally part of Sequence Point Software's J2ME_MPEG. +http://code.seqpoint.com/j2me_mpeg/ Licensed under the Eclipse Public License v1.0 The text of this license is contained in the file EPLv1.html -jpsxdec/plugins/psx/video/mdec/idct/J2meMpegIDCT.java +jpsxdec/plugins/psx/video/mdec/idct/EclipseJpegIDCT.java Copyright (c) 2000, 2005 IBM Corporation and others. +Originally part of the Eclipse Standard Widget Toolkit (SWT) +http://www.eclipse.org/swt/ Licensed under the Eclipse Public License v1.0 The text of this license is contained in the file EPLv1.html -argparser.zip is Copyright John E. Lloyd, 2004. -See argparser.zip COPYRIGHT file for details. +argparser/*.* +From the argparser library. http://people.cs.ubc.ca/~lloyd/java/argparser.html +* Copyright John E. Lloyd, 2004. All rights reserved. Permission to use, +* copy, modify and redistribute is granted, provided that this copyright +* notice is retained and the author is given credit whenever appropriate. +* +* This software is distributed "as is", without any warranty, including +* any implied warranty of merchantability or fitness for a particular +* use. The author assumes no responsibility for, and shall not be liable +* for, any special, indirect, or consequential damages, or any damages +* whatsoever, arising out of or in connection with the use of this +* software. jpsxdec/util/Fraction.java -Originally written by Doug Lea and released into the public domain. \ No newline at end of file +Originally written by Doug Lea and released into the public domain. + +################################################################### + +## The following files must be included in a separate .jar with ## +## binary distributions of jPSXdec so as not to violate the LGPL. ## + +jpsxdec/plugins/psx/video/mdec/idct/simple_idct.java +Copyright (c) Michael Niedermayer +Originally from ffmpeg (ported by Alexander Strange) +Licensed under the GNU Lesser General Public License Version 2.1. +The text of this license is contained in the file lgpl-2.1.txt diff --git a/PlayStation1_STR_format.txt b/PlayStation1_STR_format.txt index 4030f0f..0f3350e 100644 --- a/PlayStation1_STR_format.txt +++ b/PlayStation1_STR_format.txt @@ -327,7 +327,7 @@ Offest Size Endian -------------------------------------------------------- Size of data (in bytes) following this header? 22 . . . 2 . . little . Always 0x3800 24 . . . 2 . . little . Frame's quantization scale - 28 . . . 2 . . little . Version of the video frame + 26 . . . 2 . . little . Version of the video frame (see next section for details) 28 . . . 2 . . little . Always 0x00000000 32 --------------------------------------------------------------------------- @@ -993,7 +993,7 @@ IDCT_table = [ 0.354 -0.354 -0.354 0.354 0.354 -0.354 -0.354 0.354 ] IDCT_table * Deqantizized_Matrix * IDCT_table^transposed -Like the de-quantization table, the IDCT matrix is programmable, but (nearly?) all games use the same table (game initializes it when starting): +Like the quantization table, the IDCT matrix is programmable, but (nearly?) all games use the same table (game initializes it when starting): [ 23170 23170 23170 23170 23170 23170 23170 23170 ] [ 32138 27245 18204 6392 -6393 -18205 -27246 -32139 ] @@ -1127,7 +1127,7 @@ Offest Size Endian -------------------------------------------------------- 20 . . . 2 . . little . Unknown 22 . . . 2 . . little . Unknown 24 . . . 2 . . little . Unknown - 28 . . . 2 . . little . Unknown + 26 . . . 2 . . little . Unknown 28 . . . 2 . . little . Always 0x00000000 32 --------------------------------------------------------------------------- @@ -1567,12 +1567,12 @@ Offest Size Endian -------------------------------------------------------- (including header)? 16 . . . 2 . . little . Width of frame in pixels 18 . . . 2 . . little . Height of frame in pixels - 20 . . . 1 . . n/a . . quantization scale for luminance blocks - 21 . . . 1 . . n/a . . quantization scale for chrominance blocks - 22 . . . 2 . . little . All but the last movie: always 0x3800 - The last movie: frame number (again) + 20 . . . 1 . . n/a . . quantization scale for luminance blocks (one movie has 0) + 21 . . . 1 . . n/a . . quantization scale for chrominance blocks (one movie has 0) + 22 . . . 2 . . little . Almost always 0x3800. One movie has 0x0000, + and the last movie has the frame number (again) 24 . . . 2 . . little . Number of run length codes in the frame - 28 . . . 2 . . little . Version of the video frame: always 0 + 26 . . . 2 . . little . Version of the video frame: always 0 28 . . . 4 . . little . Always 0x00000000 32 --------------------------------------------------------------------------- diff --git a/TODO.txt b/TODO.txt index e5a05f2..7edaaff 100644 --- a/TODO.txt +++ b/TODO.txt @@ -8,78 +8,128 @@ / = partially done /?/ = done? -* Remove DemuxMovieWriterBuilder.open() and PCM16bitAudioWriter.open() - because it's not serving any value. Rename .create*() to .createAndOpen*(). - Once that is done, I may be able to get rid of PCM16bitAudioWriter and just - use AudioOutputStream. -* Add PAL and NTSC ratio selection to yuv4mpeg2 writing. -* Add option to save copied files out with cdxa header -* Display physical arrangement of disc items in some graphical form -* Figure out why fps detects square-type movies having sectors/frame of 2 -? Introduce option to ignore errors: specifically for CD sector reading and frame uncompression -* Change how FF7 frame camera data is treated: - keep it as part of the first sector and not part of the frame -* Print unused/unrecognized command-line parameters -* Use decoder comparison to create RGB conversion matricies - so images decoded with other programs can be re-encoded with consistent coloring -/?/ Java sometimes makes .wav files that WMP can't play? -* Change index and source file handling so only the index file is used most of the time +* Find new host + Requirements: + * free + ? simple issue tracking (anon bug reports a plus) + * project name in url + * clean & simple homepage not focused on the code but on summary and downloads + * native SVN support (not bridges) + * 5 or more developers allowed on the project + * syntax highlighted repo browsing + * allows non OSI licences + http://www.svnhostingcomparison.com/ + ? assembla.com (# of users?, reasonably clean, wiki for a home page, add/remove tabs, bugs called 'tickets') e.g. http://www.assembla.com/spaces/burro + ? javaforge.com (cluttered) e.g. http://javaforge.com/project/dock + ? sharesource.org (# of users?, very simple, bug reporting without register, no code highlighting) + ? activestate.com/firefly/plans (where are the downloads?, # of users?, url is firefly.activestate.com/username/projectname) e.g. http://firefly.activestate.com/ahamino/mindreader http://firefly.activestate.com/dafi/morekomodo + ? projectlocker.com (5 users, lots of q's to signup, unable to find a project page on site, has ads) + ? myversioncontrol.com (3 users, unable to find a project page on site) + x bountysource.com (untrusted connection for all projeccts) + x Freepository (1 user, svn ok, license ok, no issue tracking) + x java.net (cluttered) e.g. https://jinput.dev.java.net/ + x codespaces.com (2 users, unable to find a project page on site) + x xp-dev (project name not in url, no repo browsing) + x codeplex (best interface, but svn please) + x googlecode (incompatable license) + x github (svn please) + x bitbucket (svn please) + x launchpad (svn please) +/* Restore feedback messages in new decoding pipeline +! Figure out why yuv coloring isn't the same as rgb +! Display physical arrangement of disc items in some graphical form +! Add PAL and NTSC ratio selection to yuv4mpeg2 writing. +! Change index and source file handling so only the index file is used most of the time (index file points to source file, but can be manually specified if source file moved) -* Add generating of silence for audio to keep in sync with video (both player and saver) -* Make a simplier audio format normalization for parallel audio streams in videos - - Just normalize mono/stereo and sample rate -* Add/finish api for selecting which parallel audio stream to play -! Add more/improve frame rate detection logic -! Find non-commercial use license - ? MAME non-commercial license and move to Kenai and Mercurial - - find new SSL SMTP lib - // find new fast idct +! Add -precisefps +*! Add/finish api for selecting which parallel audio stream to play + // Identify when parallel audio streams are mutually exclusive + // Add handling for cases where there is a break between sequential parallel audio streams + // Move the selection of parallel audio into DemuxMovieWriterBuilder + //Then just have DemuxMovieWriter.feedSector(IdentifiedSector) that will take care of decoding + // Make a simplier audio format normalization for parallel audio streams in videos + // Just normalize mono/stereo and sample rate +// change StrPushDemuxer to handle the rest of the demuxing logic found in StrSaver + // each sector added could produce up to 2 frames (maybe more in the future) + x make it more like IDiscItemAudioDecoder? +// Inform user if corrupted data is found while reading sectors + - Don't show an error on every sector (try to group them) +* Add XA format & channel consistency check +* When printing what video is saving, also print the parallel audio item(s) +* Change back to Java 5 if possible +* Introduce option to ignore errors: specifically for CD sector reading and frame uncompression +* Print unused/unrecognized command-line parameters +* Clean up listener management in DemuxMovieWriter* +* Add more/improve frame rate detection logic + - Figure out why fps detects square-type movies having sectors/frame of 2 +* Write video encoder + - Smart chrominance sub-sampling calculation + - Pre-anti-aliasing and Pre-blockiness reduction + - Temporal spreading of quantization error + - Use decoder comparison to create RGB conversion matricies + so images decoded with other programs can be re-encoded with consistent coloring * Remove DemuxImageUncompressor from plugin interface (somehow) -* Automatic error reporting +* When displaying progress, setup a timer to only update the display at most every second +// make build release ant task +// move lgpl code into separate root source folder +// put argparser source directly into repo +// Fix the super.toString() for children of UnknownData +// Add generating of silence for audio to keep in sync with video for -preciseavsync +// Remove DemuxMovieWriterBuilder.open() and PCM16bitAudioWriter.open() +// because it's not serving any value. Rename .create*() to .createAndOpen*(). +// Add option to save copied files out with cdxa header +// Change how FF7 frame camera data is treated: +// keep it as part of the first sector and not part of the frame +/?/ Java sometimes makes .wav files that WMP can't play? +// find new SSL SMTP lib +// Two types of listeners: Verbose feedback, and Progress // Yuv4mpeg2 writing // Implement PSX YUV -> Standard YUV // Change Yuv4mpeg2 and MdecDecoder_double.readYuv() to use 4:2:0 subsampling. +// Display CD format details to the user + * Reimplement a GUI - Figure out how netbeans uses a folder chooser so I can use it too - show media list as a tree list showing all the files on the disc and what media belongs to what file. if not a disc image, just show the file name and the media it contains - Use my various conceptual models (e.g. OneOfManyModel) for the types of source data + - Automatic error reporting * AVI Writing - - Subclass AVI writer to make a BufferedAviWriter to generate cleaner AVI files - - AVI PNG codec - ! AVI YUV codec (specifically fourcc YV12) + ? add optional logic to buffer a/v data to generate cleaner interleaved files + ? AVI PNG codec + // AVI YUV codec (specifically fourcc YV12) * Real-time media playback/preview // Abstract out the media player // Change thread syncing to use PipedIOStream approach - Make sure I'm not reusing the same buffer for all my audio streaming - - Cleanup - - Sync player: end of first frame with start of audio - - Add new state to queue: BUMP_WHEN_FULL that will empty the oldest item - when full and a new item is added. This state will be enabled by the audio - queue when it is empty to prevent blocking. - - Add choosing of interpolation option to api - = Add choosing of aspect ratio + // Cleanup + // Sync player: end of first frame with start of audio + - Move that logic into something more independent so writer can use it too + - Implement my own low latency audio position tracking + - Skip audio in the rare case it is ahead of sector reading + // Add new state to queue: BUMP_WHEN_FULL that will empty the oldest item + // when full and a new item is added. This state will be enabled by the audio + // queue when it is empty to prevent blocking. + - Change VideoPlayer into PlayerCanvas that also holds the progress bar and control buttons + - Add choosing of video interpolation option to api + - Add choosing of aspect ratio - Add command-line options to pick zooming, interpolation, and aspect ratio + - Fix it on Linux (Ubuntu) somehow + ? Register listeners with the SourceDataLine and let those events pause the + other components indirectly - Reduce amount of object creation/destruction so GC delays aren't ever a problem Current process: byte[] for sector data CDSector to wrap that + CDXAHeader + CodingInfo + SubMode IdentifiedSector to wrap that - StrFramePushDemuxer to hold several IdentifiedSector - Also contains IdentifiedSector[] - byte[] created by StrFramePushDemuxer - DemuxImage created to hold byte[] - ArrayBitReader created to hold byte[] again - MdecDecoder_ creates one MdecCode per frame - BufferedImage created from rgb -* Allow writing stuff to stdout -* Fix the super.toString() for children of UnknownData -* When displaying progress, setup a timer to only update the display at most every second -// Two types of listeners: Verbose feedback, and Progress -* inform user if corrupted data is found while reading sectors - - Don't show an error on every sector (try to group them) + All the NotThisTypeExceptions thrown during identification + //byte[] created by FrameDemuxer + //DemuxImage created to hold byte[] + //byte[] created by uncompressor + //BufferedImage created from rgb + AudioFormats created along the way? Indexing * Visually populate a list as the disc/file is being indexed * Make indexing faster (don't use constructors to identify sector types) @@ -88,20 +138,21 @@ Indexing - Searching for static demux and MDEC data - Finding and uncompressing lhz data, often used in PSX games - Re-add TIM search - * When reading an existing index file, just print how many items were identified - * and also print when lines have errors - /?/ Serialize should keep key-value pairs in the order they are added + // When reading an existing index file, just print how many items were identified + // and also print when lines have errors + // Serialize should keep key-value pairs in the order they are added + ! show error when deserialzing line has error + * Print in a comment what sector types were found during indexing (for debugging) * Find a way to serialze the suggested base name so they're all unique and may be used to refer to item to decode * STR indexing: add serialized value: Precise fps:Y/N. When there's no rate variance, then it can be used for -precisefps * ISO indexer keeps a bit array of all sectors in the CD and flags them as mode 1/2. When creating the DiscItemISO9660Files, it checks if the file contains mode 2 sectors -! Finish re-adding TIM handling +x Allow writing stuff to stdout + x Only yuv4mpeg2 and plain pcm? x Raw direct CD reading from the disc drive (somehow) x CD-i video formats -x More game handling x CD track audio handling ? Change package structure to .jspxdec.* -* Display CD format details to the user ? Add function "IdentifySector()" in PSXMediaStreaming to more quickly figure out what type of sectors are read (since it won't have to go through the whole list of like 10 types). @@ -111,21 +162,22 @@ x CD track audio handling * write code documentation * develop a test set // Figure out why ISO9660 isn't work on CD-i -> because it doesn't use ISO9660 (http://www.smart-projects.net/help.php?help=230) -* Figure out why CD-i stereo XA leaves right audio silent +// Figure out why CD-i stereo XA leaves one channel audio silent -> it's really how the data is on the disc GAME SPECIFIC: +* More game handling * Soul Reaver 009:TIM is extra wide and saved as jpg looks weird (transparency?) * FF Chronicles has a tim file being saved as blank * Legend of Mana has 000:TIM with 64 palettes! One being saved all transparent? -* Super Puzzle Figher 2: First movie, last frame is split because audio sector changes format -* Japanese Tekken 3 has subtitles for the english movie? but where is the data for that? -* Chrono Chross final movie - need to add handling for it +/?/ Super Puzzle Figher 2: First movie, last frame is split because audio sector changes format +x Japanese Tekken 3 has subtitles for the english movie? but where is the data for that? +// Chrono Chross final movie - need to add handling for it * Castlevania seems to have movies with lots of extra frames at the end? * Lain Disc 2 048:STR - End of movie has 2 audio sectors next to each other // Figure out how the heck to REALLY handle variable frame rates (Alice in cyberland) * Pop at end of 429:XA (FF Chron?) disc 1. Test with PsxMC -* Lain movie of camera opening her door while she's takling to friend on computer - audio is way off for some reason +* Lain disc1 F024.STR audio is way off for some reason +// Spyro indexing isn't doing XA right -> due to bad sectors New code style: @@ -136,5 +188,5 @@ New code style: primitive type objects will also still be 'io', 'blno', 'dblo' etc nested non-static class fields can start with double underscore '__' core types prefixes: long=lng int=i short=si byte=b - double=dbl float=flt boolean=bln + double=dbl float=flt boolean=bln enum=e array prefix: a (e.g. int[] = ai) diff --git a/argparser.zip b/argparser.zip deleted file mode 100644 index c2198db3343605386a3bee509f7802d7e52c31db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132403 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
4+nS|pVPkLouesjve@_3gX3Y=(w>2w7 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�Ci&lP03-NdiRR9U0OOWFNkY=aUi~_phNjHFc zuKYLi+8NA4HO;~I<+%Z-7r-QgQ4^gInhCRa@Neo_nsNfvX^bymVSt)(iW*J?O@&!R z_M3`!j;=A`R950=QgKP3sW6YW{idF!>w1=06+z>!v4_MV*{K70aIJ> zVfq7tB8{f+sVGnlu(f&^ok#$7EN~r%m?L&`z*%f(dKIu8bq?HMSvTAZ z2#Z00LmfxlBRE&k*>H7;se`lGe?W)Q+(KMJ0D%^OCl}~oY_b=54jOE40g-fdu=@}6 z4^ju4hDab#FrYlFRj$PTg|2E3)EmD|ex6mhNa5)D$v~hj-jiT;DXTtx%tO1%) z;e|Sc3~0ibQ_bl5@=WGoho)DSadw7+^PJI)hj7Vw?pQqlEC=*ISX`(f{nt2m#oZf_xN2{E<#59;r6BlubigFr}rAP@zNfce^U z1^k2i5-`g_tYhV zHQ#~8LTCFED&rZ{Lko;P##lb2$^q2lbbmz6+=Zr|q~;SQ^BK$vfQjLDd3+UsoVM!3 z9yAjwH=jT+odK-^pcmfUM*9Ik9Zy-6@&FnNlkiTk9A{u_0qg^d;tR8Yv#|pq2)1XL zg5aR)2-rMw!sR=Id#JAQWrGNyS$Ep%YFv-&=GWmKBoi&HRBr~sKKf` zjd&V+T>YMK>CWLwGNy-91D4GPF4(wJg?Bdhtju>7Jp!l!yydwOAOQKW{=@r!SO7i( zafg!tnl61^RYzU^nw;jZJeqSx>X-5n=dP1wint5>HxN-kgfO%bLTK9mrwaamOCLzg zqhZiV|7gf_IO70te!nn(N3FvlPc9K02$&V%JgFytX?8T<06R801T}OE5XH$sF z(J<{iB$@JVpb+rY4=>nOFZ>JA%H9%~>UZU^urmK;4OsfoAn?!{-d8Px`2e>{0t(XO zlH|x5A{2ir?BE7*cC&)Gv%5M&j-Pl0TI@XZH(3MauR#8KXmk={kI>Duf1<7I%^)7^ zU|ZW06Q9?9D!WE;Xh8evo@&26V5|Y#5zHWt&Ja_uE3n+*a4zcKaKVN*MY4bcC_^~C{|N`Q{pGU|7j|=Jpa}do{ljK2sxUy;1J-9Sz&OR0z+dS04*!wwR_9$f0;o#} z6oSXS?@0S_U~mI#nqLiCdi!2Qn?AZwCk)DF0j5Y3+rqILT}2>4YkTe{lW{xz7x9}Qw7V`1cUxYwcy8n~``?v$N49ouWWgN$llj8KBf?yXlLJL|1@*QljI(v!Z zsX%p-j9}NYK*QC5%m)iU&KE~3TTVew!Yk}b7HDYhDd->TTK+YsBy XFjyoYiGe^ez|VDJ5UBU-VdVcGl2As6 diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..2236ac8 --- /dev/null +++ b/build.xml @@ -0,0 +1,95 @@ + + + + + Build jPSXdec for releasing. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jPSXdec-manual.odt b/jPSXdec-manual.odt index 5ba541ae24a6f5b8ab9b1ec7c45e2321dc397625..8e486cd65d278dfc3cbd22d36e7b85e4c1339c5c 100644 GIT binary patch delta 12044 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 V?swn>2Ibdv^!(SO5DNXi{XeP&sGtA< delta 18203 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 when + * command line arguments contain an error. + * + * @author John E. Lloyd, Fall 2004 + * @see ArgParser + */ +public class ArgParseException extends IOException +{ + /** + * Creates a new ArgParseException with the given message. + * + * @param msg Exception message + */ + public ArgParseException (String msg) + { super (msg); + } + + /** + * Creates a new ArgParseException from the given + * argument and message. + * + * @param arg Offending argument + * @param msg Error message + */ + public ArgParseException (String arg, String msg) + { super (arg + ": " + msg); + } +} diff --git a/src/argparser/ArgParser.java b/src/argparser/ArgParser.java new file mode 100644 index 0000000..3d84447 --- /dev/null +++ b/src/argparser/ArgParser.java @@ -0,0 +1,2266 @@ +/** + * Copyright John E. Lloyd, 2004. All rights reserved. Permission to use, + * copy, modify and redistribute is granted, provided that this copyright + * notice is retained and the author is given credit whenever appropriate. + * + * This software is distributed "as is", without any warranty, including + * any implied warranty of merchantability or fitness for a particular + * use. The author assumes no responsibility for, and shall not be liable + * for, any special, indirect, or consequential damages, or any damages + * whatsoever, arising out of or in connection with the use of this + * software. + */ +package argparser; + +import java.io.PrintStream; +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.File; +import java.io.FileReader; +import java.io.Reader; +import java.util.Vector; + +import java.lang.reflect.Array; + +/** + * ArgParser is used to parse the command line arguments for a java + * application program. It provides a compact way to specify options and match + * them against command line arguments, with support for + * range checking, + * multiple option names (aliases), + * single word options, + * multiple values associated with an option, + * multiple option invocation, + * generating help information, + * custom argument parsing, and + * reading arguments from a file. The + * last feature is particularly useful and makes it + * easy to create ad-hoc configuration files for an application. + * + *

Basic Example

+ * + *

Here is a simple example in which an application has three + * command line options: + * -theta (followed by a floating point value), + * -file (followed by a string value), and + * -debug, which causes a boolean value to be set. + * + *

+ *
+ * static public void main (String[] args)
+ *  {
+ *    // create holder objects for storing results ...
+ * 
+ *    DoubleHolder theta = new DoubleHolder();
+ *    StringHolder fileName = new StringHolder();
+ *    BooleanHolder debug = new BooleanHolder();
+ * 
+ *    // create the parser and specify the allowed options ...
+ * 
+ *    ArgParser parser = new ArgParser("java argparser.SimpleExample");
+ *    parser.addOption ("-theta %f #theta value (in degrees)", theta); 
+ *    parser.addOption ("-file %s #name of the operating file", fileName);
+ *    parser.addOption ("-debug %v #enables display of debugging info", debug);
+ *
+ *    // match the arguments ...
+ * 
+ *    parser.matchAllArgs (args);
+ *
+ *    // and print out the values
+ *
+ *    System.out.println ("theta=" + theta.value);
+ *    System.out.println ("fileName=" + fileName.value);
+ *    System.out.println ("debug=" + debug.value);
+ *  }
+ * 
+ *

A command line specifying all three options might look like this: + *

+ * java argparser.SimpleExample -theta 7.8 -debug -file /ai/lloyd/bar 
+ * 
+ * + *

The application creates an instance of ArgParser and then adds + * descriptions of the allowed options using {@link #addOption addOption}. The + * method {@link #matchAllArgs(String[]) matchAllArgs} is then used to match + * these options against the command line arguments. Values associated with + * each option are returned in the value field of special + * ``holder'' classes (e.g., {@link argparser.DoubleHolder DoubleHolder}, + * {@link argparser.StringHolder StringHolder}, etc.). + * + *

The first argument to {@link #addOption addOption} is a string that + * specifies (1) the option's name, (2) a conversion code for its associated + * value (e.g., %f for floating point, %s for a + * string, %v for a boolean flag), and (3) an optional description + * (following the # character) which is used for generating help + * messages. The second argument is the holder object through which the value + * is returned. This may be either a type-specific object (such as {@link + * argparser.DoubleHolder DoubleHolder} or {@link argparser.StringHolder + * StringHolder}), an array of the appropriate type, or + * an instance of + * java.util.Vector. + * + *

By default, arguments that don't match the specified options, are out of range, or are otherwise formatted incorrectly, + * will cause matchAllArgs to print a message and exit the + * program. Alternatively, an application can use {@link + * #matchAllArgs(String[],int,int) matchAllArgs(args,idx,exitFlags)} to obtain + * an array of unmatched arguments which can then be + * processed separately + * + *

Range Specification

+ * + * The values associated with options can also be given range specifications. A + * range specification appears in curly braces immediately following the + * conversion code. In the code fragment below, we show how to specify an + * option -name that expects to be provided with one of three + * string values (john, mary, or jane), + * an option -index that expects to be supplied with a integer + * value in the range 1 to 256, an option -size that expects to be + * supplied with integer values of either 1, 2, 4, 8, or 16, and an option + * -foo that expects to be supplied with floating point values in + * the ranges -99 < foo <= -50, or 50 <= foo < 99. + * + *
+ *    StringHolder name = new StringHolder();
+ *    IntHolder index = new IntHolder();
+ *    IntHolder size = new IntHolder();
+ *    DoubleHolder foo = new DoubleHolder();
+ * 
+ *    parser.addOption ("-name %s {john,mary,jane}", name);
+ *    parser.addOption ("-index %d {[1,256]}", index);
+ *    parser.addOption ("-size %d {1,2,4,8,16}", size);
+ *    parser.addOption ("-foo %f {(-99,-50],[50,99)}", foo);
+ * 
+ * + * If an argument value does not lie within a specified range, an error is + * generated. + * + *

Multiple Option Names

+ * + * An option may be given several names, or aliases, in the form of + * a comma seperated list: + * + *
+ *    parser.addOption ("-v,--verbose %v #print lots of info");
+ *    parser.addOption ("-of,-outfile,-outputFile %s #output file");
+ * 
+ * + *

Single Word Options

+ * + * Normally, options are assumed to be "multi-word", meaning + * that any associated value must follow the option as a + * separate argument string. For + * example, + *
+ *    parser.addOption ("-file %s #file name");
+ * 
+ * will cause the parser to look for two strings in the argument list + * of the form + *
+ *    -file someFileName
+ * 
+ * However, if there is no white space separting the option's name from + * it's conversion code, then values associated with that + * option will be assumed to be part of the same argument + * string as the option itself. For example, + *
+ *    parser.addOption ("-file=%s #file name");
+ * 
+ * will cause the parser to look for a single string in the argument + * list of the form + *
+ *    -file=someFileName
+ * 
+ * Such an option is called a "single word" option. + * + *

+ * In cases where an option has multiple names, then this single + * word behavior is invoked if there is no white space between + * the last indicated name and the conversion code. However, previous + * names in the list will still be given multi-word behavior + * if there is white space between the name and the + * following comma. For example, + *

+ *    parser.addOption ("-nb=,-number ,-n%d #number of blocks");
+ * 
+ * will cause the parser to look for one, two, and one word constructions + * of the forms + *
+ *    -nb=N
+ *    -number N
+ *    -nN
+ * 
+ * + *

Multiple Option Values

+ * + * If may be useful for an option to be followed by several values. + * For instance, we might have an option -velocity + * which should be followed by three numbers denoting + * the x, y, and z components of a velocity vector. + * We can require multiple values for an option + * by placing a multiplier specification, + * of the form XN, where N is an integer, + * after the conversion code (or range specification, if present). + * For example, + * + *
+ *    double[] pos = new double[3];
+ *
+ *    addOption ("-position %fX3 #position of the object", pos);
+ * 
+ * will cause the parser to look for + *
+ *    -position xx yy zz
+ * 
+ * + * in the argument list, where xx, yy, and + * zz are numbers. The values are stored in the array + * pos. + * + * Options requiring multiple values must use arrays to + * return their values, and cannot be used in single word format. + * + *

Multiple Option Invocation

+ * + * Normally, if an option appears twice in the command list, the + * value associated with the second instance simply overwrites the + * value associated with the first instance. + * + * However, the application can instead arrange for the storage of all + * values associated with multiple option invocation, by supplying a instance + * of java.util.Vector to serve as the value holder. Then every + * time the option appears in the argument list, the parser will create a value + * holder of appropriate type, set it to the current value, and store the + * holder in the vector. For example, the construction + * + *
+ *    Vector vec = new Vector(10);
+ *
+ *    parser.addOption ("-foo %f", vec);
+ *    parser.matchAllArgs(args);
+ * 
+ * when supplied with an argument list that contains + *
+ *    -foo 1.2 -foo 1000 -foo -78
+ * 
+ * + * will create three instances of {@link argparser.DoubleHolder DoubleHolder}, + * initialized to 1.2, 1000, and -78, + * and store them in vec. + * + *

Generating help information

+ * + * ArgParser automatically generates help information for the options, and this + * information may be printed in response to a help option, or may be + * queried by the application using {@link #getHelpMessage getHelpMessage}. + * The information for each option consists of the option's name(s), it's + * required value(s), and an application-supplied description. Value + * information is generated automaticlly from the conversion code, range, and + * multiplier specifications (although this can be overriden, as + * described below). + * The application-supplied description is whatever + * appears in the specification string after the optional # + * character. The string returned by {@link #getHelpMessage getHelpMessage} for + * the first example above would be + * + *
+ * Usage: java argparser.SimpleExample
+ * Options include:
+ * 
+ * -help,-?                displays help information
+ * -theta <float>          theta value (in degrees)
+ * -file <string>          name of the operating file
+ * -debug                  enables display of debugging info
+ * 
+ * + * The options -help and -? are including in the + * parser by default as help options, and they automatically cause the help + * message to be printed. To exclude these + * options, one should use the constructor {@link #ArgParser(String,boolean) + * ArgParser(synopsis,false)}. + * Help options can also be specified by the application using {@link + * #addOption addOption} and the conversion code %h. Help options + * can be disabled using {@link #setHelpOptionsEnabled + * setHelpOptionsEnabled(false)}. + * + *

+ * A description of the required values for an option can be + * specified explicitly + * by placing a second # character in the specification + * string. Everything between the first and second # + * characters then becomes the value description, and everything + * after the second # character becomes the option + * description. + * For example, if the -theta option + * above was specified with + *

+ *    parser.addOption ("-theta %f #NUMBER#theta value (in degrees)",theta);
+ * 
+ * instead of + *
+ *    parser.addOption ("-theta %f #theta value (in degrees)", theta);
+ * 
+ * then the corresponding entry in the help message would look + * like + *
+ * -theta NUMBER          theta value (in degrees)
+ * 
+ * + *

Custom Argument Parsing

+ * + * An application may find it necessary to handle arguments that + * don't fit into the framework of this class. There are a couple + * of ways to do this. + * + *

+ * First, the method {@link #matchAllArgs(String[],int,int) + * matchAllArgs(args,idx,exitFlags)} returns an array of + * all unmatched arguments, which can then be handled + * specially: + *

+ *    String[] unmatched =
+ *       parser.matchAllArgs (args, 0, parser.EXIT_ON_ERROR);
+ *    for (int i = 0; i < unmatched.length; i++)
+ *     { ... handle unmatched arguments ...
+ *     }
+ * 
+ * + * For instance, this would be useful for an applicatoon that accepts an + * arbitrary number of input file names. The options can be parsed using + * matchAllArgs, and the remaining unmatched arguments + * give the file names. + * + *

If we need more control over the parsing, we can parse arguments one at + * a time using {@link #matchArg matchArg}: + * + *

+ *    int idx = 0;
+ *    while (idx < args.length)
+ *     { try
+ *        { idx = parser.matchArg (args, idx);
+ *          if (parser.getUnmatchedArgument() != null)
+ *           {
+ *             ... handle this unmatched argument ourselves ...
+ *           }
+ *        }
+ *       catch (ArgParserException e) 
+ *        { // malformed or erroneous argument
+ *          parser.printErrorAndExit (e.getMessage());
+ *        }
+ *     }
+ * 
+ * + * {@link #matchArg matchArg(args,idx)} matches one option at location + * idx in the argument list, and then returns the location value + * that should be used for the next match. If an argument does + * not match any option, + * {@link #getUnmatchedArgument getUnmatchedArgument} will return a copy of the + * unmatched argument. + * + *

Reading Arguments From a File

+ * + * The method {@link #prependArgs prependArgs} can be used to automatically + * read in a set of arguments from a file and prepend them onto an existing + * argument list. Argument words correspond to white-space-delimited strings, + * and the file may contain the comment character # (which + * comments out everything to the end of the current line). A typical usage + * looks like this: + * + *
+ *    ... create parser and add options ...
+ * 
+ *    args = parser.prependArgs (new File(".configFile"), args);
+ *
+ *    parser.matchAllArgs (args);
+ * 
+ * + * This makes it easy to generate simple configuration files for an + * application. + * + * @author John E. Lloyd, Fall 2004 + */ +public class ArgParser +{ + Vector matchList; +// int tabSpacing = 8; + String synopsisString; + boolean helpOptionsEnabled = true; + Record defaultHelpOption = null; + Record firstHelpOption = null; + PrintStream printStream = System.out; + int helpIndent = 24; + String errMsg = null; + String unmatchedArg = null; + + static String validConversionCodes = "iodxcbfsvh"; + + /** + * Indicates that the program should exit with an appropriate message + * in the event of an erroneous or malformed argument.*/ + public static int EXIT_ON_ERROR = 1; + + /** + * Indicates that the program should exit with an appropriate message + * in the event of an unmatched argument.*/ + public static int EXIT_ON_UNMATCHED = 2; + + /** + * Returns a string containing the valid conversion codes. These + * are the characters which may follow the % character in + * the specification string of {@link #addOption addOption}. + * + * @return Valid conversion codes + * @see #addOption + */ + public static String getValidConversionCodes() + { + return validConversionCodes; + } + + static class NameDesc + { + String name; + // oneWord implies that any value associated with + // option is concatenated onto the argument string itself + boolean oneWord; + NameDesc next = null; + } + + static class RangePnt + { + double dval = 0; + long lval = 0; + String sval = null; + boolean bval = true; + boolean closed = true; + + RangePnt (String s, boolean closed) + { sval = s; + this.closed = closed; + } + + RangePnt (double d, boolean closed) + { dval = d; + this.closed = closed; + } + + RangePnt (long l, boolean closed) + { lval = l; + this.closed = closed; + } + + RangePnt (boolean b, boolean closed) + { bval = b; + this.closed = closed; + } + + RangePnt (StringScanner scanner, int type) + throws IllegalArgumentException + { + String typeName = null; + try + { switch (type) + { + case Record.CHAR: + { typeName = "character"; + lval = scanner.scanChar(); + break; + } + case Record.INT: + case Record.LONG: + { typeName = "integer"; + lval = scanner.scanInt(); + break; + } + case Record.FLOAT: + case Record.DOUBLE: + { typeName = "float"; + dval = scanner.scanDouble(); + break; + } + case Record.STRING: + { typeName = "string"; + sval = scanner.scanString(); + break; + } + case Record.BOOLEAN: + { typeName = "boolean"; + bval = scanner.scanBoolean(); + break; + } + } + } + catch (StringScanException e) + { throw new IllegalArgumentException ( + "Malformed " + typeName + " '" + + scanner.substring(scanner.getIndex(), + e.getFailIndex()+1) + + "' in range spec"); + } +// this.closed = closed; + } + + void setClosed (boolean closed) + { this.closed = closed; + } + + boolean getClosed() + { return closed; + } + + int compareTo (double d) + { if (dval < d) + { return -1; + } + else if (d == dval) + { return 0; + } + else + { return 1; + } + } + + int compareTo (long l) + { if (lval < l) + { return -1; + } + else if (l == lval) + { return 0; + } + else + { return 1; + } + } + + int compareTo (String s) + { return sval.compareTo (s); + } + + int compareTo (boolean b) + { if (b == bval) + { return 0; + } + else + { return 1; + } + } + + public String toString() + { return "{ dval=" + dval + ", lval=" + lval + + ", sval=" + sval + ", bval=" + bval + + ", closed=" + closed + "}"; + } + } + + class RangeAtom + { + RangePnt low = null; + RangePnt high = null; + RangeAtom next = null; + + RangeAtom (RangePnt p0, RangePnt p1, int type) + throws IllegalArgumentException + { + int cmp = 0; + switch (type) + { + case Record.CHAR: + case Record.INT: + case Record.LONG: + { cmp = p0.compareTo (p1.lval); + break; + } + case Record.FLOAT: + case Record.DOUBLE: + { cmp = p0.compareTo (p1.dval); + break; + } + case Record.STRING: + { cmp = p0.compareTo (p1.sval); + break; + } + } + if (cmp > 0) + { // then switch high and low + low = p1; + high = p0; + } + else + { low = p0; + high = p1; + } + } + + RangeAtom (RangePnt p0) + throws IllegalArgumentException + { + low = p0; + } + + boolean match (double d) + { int lc = low.compareTo(d); + if (high != null) + { int hc = high.compareTo(d); + return (lc*hc < 0 || + (low.closed && lc==0) || + (high.closed && hc==0)); + } + else + { return lc == 0; + } + } + + boolean match (long l) + { int lc = low.compareTo(l); + if (high != null) + { int hc = high.compareTo(l); + return (lc*hc < 0 || + (low.closed && lc==0) || + (high.closed && hc==0)); + } + else + { return lc == 0; + } + } + + boolean match (String s) + { int lc = low.compareTo(s); + if (high != null) + { int hc = high.compareTo(s); + return (lc*hc < 0 || + (low.closed && lc==0) || + (high.closed && hc==0)); + } + else + { return lc == 0; + } + } + + boolean match (boolean b) + { return low.compareTo(b) == 0; + } + + public String toString() + { return "low=" + (low==null ? "null" : low.toString()) + + ", high=" + (high==null ? "null" : high.toString()); + } + } + + class Record + { + NameDesc nameList; + static final int NOTYPE = 0; + static final int BOOLEAN = 1; + static final int CHAR = 2; + static final int INT = 3; + static final int LONG = 4; + static final int FLOAT = 5; + static final int DOUBLE = 6; + static final int STRING = 7; + int type; + int numValues; + boolean vectorResult = false; + + String helpMsg = null; + String valueDesc = null; + String rangeDesc = null; + Object resHolder = null; + RangeAtom rangeList = null; + RangeAtom rangeTail = null; + char convertCode; + boolean vval = true; // default value for now + + NameDesc firstNameDesc() + { + return nameList; + } + + RangeAtom firstRangeAtom() + { + return rangeList; + } + + int numRangeAtoms() + { int cnt = 0; + for (RangeAtom ra=rangeList; ra!=null; ra=ra.next) + { cnt++; + } + return cnt; + } + + void addRangeAtom (RangeAtom ra) + { if (rangeList == null) + { rangeList = ra; + } + else + { rangeTail.next = ra; + } + rangeTail = ra; + } + + boolean withinRange (double d) + { + if (rangeList == null) + { return true; + } + for (RangeAtom ra=rangeList; ra!=null; ra=ra.next) + { if (ra.match (d)) + { return true; + } + } + return false; + } + + boolean withinRange (long l) + { + if (rangeList == null) + { return true; + } + for (RangeAtom ra=rangeList; ra!=null; ra=ra.next) + { if (ra.match (l)) + { return true; + } + } + return false; + } + + boolean withinRange (String s) + { + if (rangeList == null) + { return true; + } + for (RangeAtom ra=rangeList; ra!=null; ra=ra.next) + { if (ra.match (s)) + { return true; + } + } + return false; + } + + boolean withinRange (boolean b) + { + if (rangeList == null) + { return true; + } + for (RangeAtom ra=rangeList; ra!=null; ra=ra.next) + { if (ra.match (b)) + { return true; + } + } + return false; + } + + String valTypeName() + { + switch (convertCode) + { + case 'i': + { return ("integer"); + } + case 'o': + { return ("octal integer"); + } + case 'd': + { return ("decimal integer"); + } + case 'x': + { return ("hex integer"); + } + case 'c': + { return ("char"); + } + case 'b': + { return ("boolean"); + } + case 'f': + { return ("float"); + } + case 's': + { return ("string"); + } + } + return ("unknown"); + } + + void scanValue (Object result, String name, String s, int resultIdx) + throws ArgParseException + { + double dval = 0; + String sval = null; + long lval = 0; + boolean bval = false; + + if (s.length()==0) + { throw new ArgParseException + (name, "requires a contiguous value"); + } + StringScanner scanner = new StringScanner(s); + try + { + switch (convertCode) + { + case 'i': + { lval = scanner.scanInt(); + break; + } + case 'o': + { lval = scanner.scanInt (8, false); + break; + } + case 'd': + { lval = scanner.scanInt (10, false); + break; + } + case 'x': + { lval = scanner.scanInt (16, false); + break; + } + case 'c': + { lval = scanner.scanChar(); + break; + } + case 'b': + { bval = scanner.scanBoolean(); + break; + } + case 'f': + { dval = scanner.scanDouble(); + break; + } + case 's': + { sval = scanner.getString(); + break; + } + } + } + catch (StringScanException e) + { throw new ArgParseException ( + name, "malformed " + valTypeName() + " '" + s + "'"); + } + scanner.skipWhiteSpace(); + if (!scanner.atEnd()) + { throw new ArgParseException ( + name, "malformed " + valTypeName() + " '" + s + "'"); + } + boolean outOfRange = false; + switch (type) + { + case CHAR: + case INT: + case LONG: + { outOfRange = !withinRange (lval); + break; + } + case FLOAT: + case DOUBLE: + { outOfRange = !withinRange (dval); + break; + } + case STRING: + { outOfRange = !withinRange (sval); + break; + } + case BOOLEAN: + { outOfRange = !withinRange (bval); + break; + } + } + if (outOfRange) + { String errmsg = "value " + s + " not in range "; + throw new ArgParseException ( + name, "value '" + s + "' not in range " + rangeDesc); + } + if (result.getClass().isArray()) + { + switch (type) + { + case BOOLEAN: + { ((boolean[])result)[resultIdx] = bval; + break; + } + case CHAR: + { ((char[])result)[resultIdx] = (char)lval; + break; + } + case INT: + { ((int[])result)[resultIdx] = (int)lval; + break; + } + case LONG: + { ((long[])result)[resultIdx] = lval; + break; + } + case FLOAT: + { ((float[])result)[resultIdx] = (float)dval; + break; + } + case DOUBLE: + { ((double[])result)[resultIdx] = dval; + break; + } + case STRING: + { ((String[])result)[resultIdx] = sval; + break; + } + } + } + else + { + switch (type) + { + case BOOLEAN: + { ((BooleanHolder)result).value = bval; + break; + } + case CHAR: + { ((CharHolder)result).value = (char)lval; + break; + } + case INT: + { ((IntHolder)result).value = (int)lval; + break; + } + case LONG: + { ((LongHolder)result).value = lval; + break; + } + case FLOAT: + { ((FloatHolder)result).value = (float)dval; + break; + } + case DOUBLE: + { ((DoubleHolder)result).value = dval; + break; + } + case STRING: + { ((StringHolder)result).value = sval; + break; + } + } + } + } + } + + private String firstHelpOptionName() + { + if (firstHelpOption != null) + { return firstHelpOption.nameList.name; + } + else + { return null; + } + } + + /** + * Creates an ArgParser with a synopsis + * string, and the default help options -help and + * -?. + * + * @param synopsisString string that briefly describes program usage, + * for use by {@link #getHelpMessage getHelpMessage}. + * @see ArgParser#getSynopsisString + * @see ArgParser#getHelpMessage + */ + public ArgParser(String synopsisString) + { + this (synopsisString, true); + } + + /** + * Creates an ArgParser with a synopsis + * string. The help options -help and + * -? are added if defaultHelp + * is true. + * + * @param synopsisString string that briefly describes program usage, + * for use by {@link #getHelpMessage getHelpMessage}. + * @param defaultHelp if true, adds the default help options + * @see ArgParser#getSynopsisString + * @see ArgParser#getHelpMessage + */ + public ArgParser(String synopsisString, boolean defaultHelp) + { + matchList = new Vector(128); + this.synopsisString = synopsisString; + if (defaultHelp) + { addOption ("-help,-? %h #displays help information", null); + defaultHelpOption = firstHelpOption = (Record)matchList.get(0); + } + } + + /** + * Returns the synopsis string used by the parser. + * The synopsis string is a short description of how to invoke + * the program, and usually looks something like + *

+ * + * "java somepackage.SomeClass [options] files ..." + * + * + *

It is used in help and error messages. + * + * @return synopsis string + * @see ArgParser#setSynopsisString + * @see ArgParser#getHelpMessage + */ + public String getSynopsisString () + { + return synopsisString; + } + + /** + * Sets the synopsis string used by the parser. + * + * @param s new synopsis string + * @see ArgParser#getSynopsisString + * @see ArgParser#getHelpMessage + */ + public void setSynopsisString (String s) + { + synopsisString = s; + } + + /** + * Indicates whether or not help options are enabled. + * + * @return true if help options are enabled + * @see ArgParser#setHelpOptionsEnabled + * @see ArgParser#addOption + */ + public boolean getHelpOptionsEnabled () + { + return helpOptionsEnabled; + } + + /** + * Enables or disables help options. Help options are those + * associated with a conversion code of %h. If + * help options are enabled, and a help option is matched, + * then the string produced by + * {@link #getHelpMessage getHelpMessage} + * is printed to the default print stream and the program + * exits with code 0. Otherwise, arguments which match help + * options are ignored. + * + * @param enable enables help options if true. + * @see ArgParser#getHelpOptionsEnabled + * @see ArgParser#addOption + * @see ArgParser#setDefaultPrintStream */ + public void setHelpOptionsEnabled(boolean enable) + { helpOptionsEnabled = enable; + } + + /** + * Returns the default print stream used for outputting help + * and error information. + * + * @return default print stream + * @see ArgParser#setDefaultPrintStream + */ + public PrintStream getDefaultPrintStream() + { return printStream; + } + + /** + * Sets the default print stream used for outputting help + * and error information. + * + * @param stream new default print stream + * @see ArgParser#getDefaultPrintStream + */ + public void setDefaultPrintStream (PrintStream stream) + { + printStream = stream; + } + + /** + * Gets the indentation used by {@link #getHelpMessage + * getHelpMessage}. + * + * @return number of indentation columns + * @see ArgParser#setHelpIndentation + * @see ArgParser#getHelpMessage + */ + public int getHelpIndentation() + { + return helpIndent; + } + + /** + * Sets the indentation used by {@link #getHelpMessage + * getHelpMessage}. This is the number of columns that an option's help + * information is indented. If the option's name and value information + * can fit within this number of columns, then all information about + * the option is placed on one line. Otherwise, the indented help + * information is placed on a separate line. + * + * @param indent number of indentation columns + * @see ArgParser#getHelpIndentation + * @see ArgParser#getHelpMessage + */ + public void setHelpIndentation (int indent) + { helpIndent = indent; + } + +// public void setTabSpacing (int n) +// { tabSpacing = n; +// } + +// public int getTabSpacing () +// { return tabSpacing; +// } + + private void scanRangeSpec (Record rec, String s) + throws IllegalArgumentException + { + StringScanner scanner = new StringScanner (s); + int i0, i = 1; + char c, c0, c1; + + scanner.setStringDelimiters (")],}"); + c = scanner.getc(); // swallow the first '{' + scanner.skipWhiteSpace(); + while ((c=scanner.peekc()) != '}') + { RangePnt p0, p1; + + if (c == '[' || c == '(') + { + if (rec.convertCode == 'v' || rec.convertCode == 'b') + { throw new IllegalArgumentException + ("Sub ranges not supported for %b or %v"); + } + c0 = scanner.getc(); // record & swallow character + scanner.skipWhiteSpace(); + p0 = new RangePnt (scanner, rec.type); + scanner.skipWhiteSpace(); + if (scanner.getc() != ',') + { throw new IllegalArgumentException + ("Missing ',' in subrange specification"); + } + p1 = new RangePnt (scanner, rec.type); + scanner.skipWhiteSpace(); + if ((c1=scanner.getc()) != ']' && c1 != ')') + { throw new IllegalArgumentException + ("Unterminated subrange"); + } + if (c0 == '(') + { p0.setClosed (false); + } + if (c1 == ')') + { p1.setClosed (false); + } + rec.addRangeAtom (new RangeAtom (p0, p1, rec.type)); + } + else + { scanner.skipWhiteSpace(); + p0 = new RangePnt (scanner, rec.type); + rec.addRangeAtom (new RangeAtom (p0)); + } + scanner.skipWhiteSpace(); + if ((c=scanner.peekc()) == ',') + { scanner.getc(); + scanner.skipWhiteSpace(); + } + else if (c != '}') + { + throw new IllegalArgumentException + ("Range spec: ',' or '}' expected"); + } + } + if (rec.numRangeAtoms()==1) + { rec.rangeDesc = s.substring (1, s.length()-1); + } + else + { rec.rangeDesc = s; + } + } + + private int defaultResultType (char convertCode) + { + switch (convertCode) + { + case 'i': + case 'o': + case 'd': + case 'x': + { return Record.LONG; + } + case 'c': + { return Record.CHAR; + } + case 'v': + case 'b': + { return Record.BOOLEAN; + } + case 'f': + { return Record.DOUBLE; + } + case 's': + { return Record.STRING; + } + } + return Record.NOTYPE; + } + + /** + * Adds a new option description to the parser. The method takes two + * arguments: a specification string, and a result holder in which to + * store the associated value. + * + *

The specification string has the general form + * + *

optionNames + * %conversionCode + * [{rangeSpec}] + * [Xmultiplier] + * [#valueDescription] + * [#optionDescription] + * + *

+ * where + *

  • optionNames is a + * comma-separated list of names for the option + * (such as -f, --file). + * + *

  • conversionCode is a single letter, + * following a % character, specifying + * information about what value the option requires: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    %fa floating point number
    %ian integer, in either decimal, + * hex (if preceeded by 0x), or + * octal (if preceeded by 0)
    %da decimal integer
    %oan octal integer
    %ha hex integer (without the + * preceeding 0x)
    %ca single character, including + * escape sequences (such as \n or \007), + * and optionally enclosed in single quotes + *
    %ba boolean value (true + * or false)
    %sa string. This will + * be the argument string itself (or its remainder, in + * the case of a single word option)
    %vno explicit value is expected, + * but a boolean value of true (by default) + * will be stored into the associated result holder if this + * option is matched. If one wishes to have a value of + * false stored instead, then the %v + * should be followed by a "range spec" containing + * false, as in %v{false}. + *
    + * + *

  • rangeSpec is an optional range specification, + * placed inside curly braces, consisting of a + * comma-separated list of range items each specifying + * permissible values for the option. A range item may be an + * individual value, or it may itself be a subrange, + * consisting of two individual values, separated by a comma, + * and enclosed in square or round brackets. Square and round + * brackets denote closed and open endpoints of a subrange, indicating + * that the associated endpoint value is included or excluded + * from the subrange. + * The values specified in the range spec need to be + * consistent with the type of value expected by the option. + * + *

    Examples: + * + *

    A range spec of {2,4,8,16} for an integer + * value will allow the integers 2, 4, 8, or 16. + * + *

    A range spec of {[-1.0,1.0]} for a floating + * point value will allow any floating point number in the + * range -1.0 to 1.0. + * + *

    A range spec of {(-88,100],1000} for an integer + * value will allow values > -88 and <= 100, as well as 1000. + * + *

    A range spec of {"foo", "bar", ["aaa","zzz")} for a + * string value will allow strings equal to "foo" or + * "bar", plus any string lexically greater than or equal + * to "aaa" but less then "zzz". + * + *

  • multiplier is an optional integer, + * following a X character, + * indicating the number of values which the option expects. + * If the multiplier is not specified, it is assumed to be + * 1. If the multiplier value is greater than 1, then the + * result holder should be either an array (of appropriate + * type) with a length greater than or equal to the multiplier + * value, or a java.util.Vector + * as discussed below. + * + *

  • valueDescription is an optional + * description of the option's value requirements, + * and consists of all + * characters between two # characters. + * The final # character initiates the + * option description, which may be empty. + * The value description is used in + * generating help messages. + * + *

  • optionDescription is an optional + * description of the option itself, consisting of all + * characters between a # character + * and the end of the specification string. + * The option description is used in + * generating help messages. + *
+ * + *

The result holder must be an object capable of holding + * a value compatible with the conversion code, + * or it must be a java.util.Vector. + * When the option is matched, its associated value is + * placed in the result holder. If the same option is + * matched repeatedly, the result holder value will be overwritten, + * unless the result holder is a java.util.Vector, + * in which + * case new holder objects for each match will be allocated + * and added to the vector. Thus if + * multiple instances of an option are desired by the + * program, the result holder should be a + * java.util.Vector. + * + *

If the result holder is not a Vector, then + * it must correspond as follows to the conversion code: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
%i, %d, %x, + * %o{@link argparser.IntHolder IntHolder}, + * {@link argparser.LongHolder LongHolder}, int[], or + * long[]
%f{@link argparser.FloatHolder FloatHolder}, + * {@link argparser.DoubleHolder DoubleHolder}, + * float[], or + * double[]
%b, %v{@link argparser.BooleanHolder BooleanHolder} or + * boolean[]
%s{@link argparser.StringHolder StringHolder} or + * String[]
%c{@link argparser.CharHolder CharHolder} or + * char[]
+ * + *

In addition, if the multiplier is greater than 1, + * then only the array type indicated above may be used, + * and the array must be at least as long as the multiplier. + * + *

If the result holder is a + * Vector, then the system will create an appropriate + * result holder object and add it to the vector. Multiple occurances + * of the option will cause multiple results to be added to the vector. + * + *

The object allocated by the system to store the result + * will correspond to the conversion code as follows: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
%i, %d, %x, + * %o{@link argparser.LongHolder LongHolder}, or + * long[] if the multiplier value exceeds 1
%f{@link argparser.DoubleHolder DoubleHolder}, or + * double[] if the multiplier value exceeds 1
%b, %v{@link argparser.BooleanHolder BooleanHolder}, or + * boolean[] + * if the multiplier value exceeds 1
%s{@link argparser.StringHolder StringHolder}, or + * String[] + * if the multiplier value exceeds 1
%c{@link argparser.CharHolder CharHolder}, or char[] + * if the multiplier value exceeds 1
+ * + * @param spec the specification string + * @param resHolder object in which to store the associated + * value + * @throws IllegalArgumentException if there is an error in + * the specification or if the result holder is of an invalid + * type. */ + public void addOption (String spec, Object resHolder) + throws IllegalArgumentException + { + // null terminated string is easier to parse + StringScanner scanner = new StringScanner(spec); + Record rec = null; + NameDesc nameTail = null; + NameDesc ndesc; + int i0, i1; + char c; + + do + { ndesc = new NameDesc(); + boolean nameEndsInWhiteSpace = false; + + scanner.skipWhiteSpace(); + i0 = scanner.getIndex(); + while (!Character.isWhitespace(c=scanner.getc()) && + c != ',' && c != '%' && c != '\000') + ; + i1 = scanner.getIndex(); + if (c!='\000') + { i1--; + } + if (i0==i1) + { // then c is one of ',' '%' or '\000' + throw new IllegalArgumentException + ("Null option name given"); + } + if (Character.isWhitespace(c)) + { nameEndsInWhiteSpace = true; + scanner.skipWhiteSpace(); + c = scanner.getc(); + } + if (c=='\000') + { throw new IllegalArgumentException + ("No conversion character given"); + } + if (c != ',' && c != '%') + { throw new IllegalArgumentException + ("Names not separated by ','"); + } + ndesc.name = scanner.substring (i0, i1); + if (rec == null) + { rec = new Record(); + rec.nameList = ndesc; + } + else + { nameTail.next = ndesc; + } + nameTail = ndesc; + ndesc.oneWord = !nameEndsInWhiteSpace; + } + while (c != '%'); + + if (nameTail == null) + { throw new IllegalArgumentException + ("Null option name given"); + } + if (!nameTail.oneWord) + { for (ndesc=rec.nameList; ndesc!=null; ndesc=ndesc.next) + { ndesc.oneWord = false; + } + } + c = scanner.getc(); + if (c=='\000') + { throw new IllegalArgumentException + ("No conversion character given"); + } + if (validConversionCodes.indexOf(c) == -1) + { throw new IllegalArgumentException + ("Conversion code '" + c + "' not one of '" + + validConversionCodes + "'"); + } + rec.convertCode = c; + + if (resHolder instanceof Vector) + { rec.vectorResult = true; + rec.type = defaultResultType (rec.convertCode); + } + else + { + switch (rec.convertCode) + { + case 'i': + case 'o': + case 'd': + case 'x': + { if (resHolder instanceof LongHolder || + resHolder instanceof long[]) + { rec.type = Record.LONG; + } + else if (resHolder instanceof IntHolder || + resHolder instanceof int[]) + { rec.type = Record.INT; + } + else + { throw new IllegalArgumentException ( + "Invalid result holder for %" + c); + } + break; + } + case 'c': + { if (!(resHolder instanceof CharHolder) && + !(resHolder instanceof char[])) + { throw new IllegalArgumentException ( + "Invalid result holder for %c"); + } + rec.type = Record.CHAR; + break; + } + case 'v': + case 'b': + { if (!(resHolder instanceof BooleanHolder) && + !(resHolder instanceof boolean[])) + { throw new IllegalArgumentException ( + "Invalid result holder for %" + c); + } + rec.type = Record.BOOLEAN; + break; + } + case 'f': + { if (resHolder instanceof DoubleHolder || + resHolder instanceof double[]) + { rec.type = Record.DOUBLE; + } + else if (resHolder instanceof FloatHolder || + resHolder instanceof float[]) + { rec.type = Record.FLOAT; + } + else + { throw new IllegalArgumentException ( + "Invalid result holder for %f"); + } + break; + } + case 's': + { if (!(resHolder instanceof StringHolder) && + !(resHolder instanceof String[])) + { throw new IllegalArgumentException ( + "Invalid result holder for %s"); + } + rec.type = Record.STRING; + break; + } + case 'h': + { // resHolder is ignored for this type + break; + } + } + } + if (rec.convertCode == 'h') + { rec.resHolder = null; + } + else + { rec.resHolder = resHolder; + } + + scanner.skipWhiteSpace(); + // get the range specification, if any + if (scanner.peekc() == '{') + { + if (rec.convertCode == 'h') + { throw new IllegalArgumentException + ("Ranges not supported for %h"); + } +// int bcnt = 0; + i0 = scanner.getIndex(); // beginning of range spec + do + { c = scanner.getc(); + if (c=='\000') + { throw new IllegalArgumentException + ("Unterminated range specification"); + } +// else if (c=='[' || c=='(') +// { bcnt++; +// } +// else if (c==']' || c==')') +// { bcnt--; +// } +// if ((rec.convertCode=='v'||rec.convertCode=='b') && bcnt>1) +// { throw new IllegalArgumentException +// ("Sub ranges not supported for %b or %v"); +// } + } + while (c != '}'); +// if (c != ']') +// { throw new IllegalArgumentException +// ("Range specification must end with ']'"); +// } + i1 = scanner.getIndex(); // end of range spec + scanRangeSpec (rec, scanner.substring (i0, i1)); + if (rec.convertCode == 'v' && rec.rangeList!=null) + { rec.vval = rec.rangeList.low.bval; + } + } + // check for value multiplicity information, if any + if (scanner.peekc() == 'X') + { + if (rec.convertCode == 'h') + { throw new IllegalArgumentException + ("Multipliers not supported for %h"); + } + scanner.getc(); + try + { rec.numValues = (int)scanner.scanInt(); + } + catch (StringScanException e) + { throw new IllegalArgumentException + ("Malformed value multiplier"); + } + if (rec.numValues <= 0) + { throw new IllegalArgumentException + ("Value multiplier number must be > 0"); + } + } + else + { rec.numValues = 1; + } + if (rec.numValues > 1) + { for (ndesc=rec.nameList; ndesc!=null; ndesc=ndesc.next) + { if (ndesc.oneWord) + { throw new IllegalArgumentException ( +"Multiplier value incompatible with one word option " + ndesc.name); + } + } + } + if (resHolder != null && resHolder.getClass().isArray()) + { if (Array.getLength(resHolder) < rec.numValues) + { throw new IllegalArgumentException ( +"Result holder array must have a length >= " + rec.numValues); + } + } + else + { if (rec.numValues > 1 && !(resHolder instanceof Vector)) + { throw new IllegalArgumentException ( +"Multiplier requires result holder to be an array of length >= " ++ rec.numValues); + } + } + + // skip white space following conversion information + scanner.skipWhiteSpace(); + + // get the help message, if any + + if (!scanner.atEnd()) + { if (scanner.getc() != '#') + { throw new IllegalArgumentException + ("Illegal character(s), expecting '#'"); + } + String helpInfo = scanner.substring (scanner.getIndex()); + // look for second '#'. If there is one, then info + // between the first and second '#' is the value descriptor. + int k = helpInfo.indexOf ("#"); + if (k != -1) + { rec.valueDesc = helpInfo.substring (0, k); + rec.helpMsg = helpInfo.substring (k+1); + } + else + { rec.helpMsg = helpInfo; + } + } + else + { rec.helpMsg = ""; + } + // add option information to match list + if (rec.convertCode == 'h' && firstHelpOption == defaultHelpOption) + { matchList.remove (defaultHelpOption); + firstHelpOption = rec; + } + matchList.add (rec); + } + + Record lastMatchRecord () + { return (Record)matchList.lastElement(); + } + + private Record getRecord (String arg, ObjectHolder ndescHolder) + { + NameDesc ndesc; + for (int i=0; i". The character # acts as + * a comment character, causing input to the end of the current line to + * be ignored. + * + * @param reader Reader from which to read the strings + * @param args Initial set of argument values. Can be + * specified as null. + * @throws IOException if an error occured while reading. + */ + public static String[] prependArgs (Reader reader, String[] args) + throws IOException + { + if (args == null) + { args = new String[0]; + } + LineNumberReader lineReader = new LineNumberReader (reader); + Vector vec = new Vector(100, 100); + String line; + int i, k; + + while ((line = lineReader.readLine()) != null) + { int commentIdx = line.indexOf ("#"); + if (commentIdx != -1) + { line = line.substring (0, commentIdx); + } + try + { stringToArgs (vec, line, /*allowQuotedStings=*/true); + } + catch (StringScanException e) + { throw new IOException ( + "malformed string, line "+lineReader.getLineNumber()); + } + } + String[] result = new String[vec.size()+args.length]; + for (i=0; i". The character # acts as a + * comment character, causing input to the end of the current line to + * be ignored. + * + * @param file File to be read + * @param args Initial set of argument values. Can be + * specified as null. + * @throws IOException if an error occured while reading the file. + */ + public static String[] prependArgs (File file, String[] args) + throws IOException + { + if (args == null) + { args = new String[0]; + } + if (!file.canRead()) + { return args; + } + try + { return prependArgs (new FileReader (file), args); + } + catch (IOException e) + { throw new IOException ( +"File " + file.getName() + ": " + e.getMessage()); + } + } + + /** + * Sets the parser's error message. + * + * @param s Error message + */ + protected void setError (String msg) + { + errMsg = msg; + } + + /** + * Prints an error message, along with a pointer to help options, + * if available, and causes the program to exit with code 1. + */ + public void printErrorAndExit (String msg) + { + if (helpOptionsEnabled && firstHelpOptionName() != null) + { msg += "\nUse "+firstHelpOptionName()+" for help information"; + } + if (printStream != null) + { printStream.println (msg); + } + System.exit(1); + } + + /** + * Matches arguments within an argument list. + * + *

In the event of an erroneous or unmatched argument, the method + * prints a message and exits the program with code 1. + * + *

If help options are enabled and one of the arguments matches a + * help option, then the result of {@link #getHelpMessage + * getHelpMessage} is printed to the default print stream and the + * program exits with code 0. If help options are not enabled, they + * are ignored. + * + * @param args argument list + * @see ArgParser#getDefaultPrintStream + */ + public void matchAllArgs (String[] args) + { + matchAllArgs (args, 0, EXIT_ON_UNMATCHED | EXIT_ON_ERROR); + } + + /** + * Matches arguments within an argument list and returns + * those which were not matched. The matching starts at a location + * in args specified by idx, and + * unmatched arguments are returned in a String array. + * + *

In the event of an erroneous argument, the method either prints a + * message and exits the program (if {@link #EXIT_ON_ERROR} is + * set in exitFlags) + * or terminates the matching and creates a error message that + * can be retrieved by {@link #getErrorMessage}. + * + *

In the event of an umatched argument, the method will print a + * message and exit if {@link #EXIT_ON_UNMATCHED} is set + * in errorFlags. + * Otherwise, the unmatched argument will be appended to the returned + * array of unmatched values, and the matching will continue at the + * next location. + * + *

If help options are enabled and one of the arguments matches a + * help option, then the result of {@link #getHelpMessage + * getHelpMessage} is printed to the the default print stream and the + * program exits with code 0. If help options are not enabled, then + * they will not be matched. + * + * @param args argument list + * @param idx starting location in list + * @param exitFlags conditions causing the program to exit. Should be + * an or-ed combintion of {@link #EXIT_ON_ERROR} or {@link + * #EXIT_ON_UNMATCHED}. + * @return array of arguments that were not matched, or + * null if all arguments were successfully matched + * @see ArgParser#getErrorMessage + * @see ArgParser#getDefaultPrintStream + */ + public String[] matchAllArgs (String[] args, int idx, int exitFlags) + { + Vector unmatched = new Vector(10); + + while (idx < args.length) + { try + { idx = matchArg (args, idx); + if (unmatchedArg != null) + { if ((exitFlags & EXIT_ON_UNMATCHED) != 0) + { printErrorAndExit ( + "Unrecognized argument: " + unmatchedArg); + } + else + { unmatched.add (unmatchedArg); + } + } + } + catch (ArgParseException e) + { if ((exitFlags & EXIT_ON_ERROR) != 0) + { printErrorAndExit (e.getMessage()); + } + break; + } + } + if (unmatched.size() == 0) + { return null; + } + else + { return (String[])unmatched.toArray(new String[0]); + } + } + + /** + * Matches one option starting at a specified location in an argument + * list. The method returns the location in the list where the next + * match should begin. + * + *

In the event of an erroneous argument, the method throws + * an {@link argparser.ArgParseException ArgParseException} + * with an appropriate error message. This error + * message can also be retrieved using + * {@link #getErrorMessage getErrorMessage}. + * + *

In the event of an umatched argument, the method will return idx + * + 1, and {@link #getUnmatchedArgument getUnmatchedArgument} will + * return a copy of the unmatched argument. If an argument is matched, + * {@link #getUnmatchedArgument getUnmatchedArgument} will return + * null. + * + *

If help options are enabled and the argument matches a help + * option, then the result of {@link #getHelpMessage getHelpMessage} is printed to + * the the default print stream and the program exits with code 0. If + * help options are not enabled, then they are ignored. + * + * @param args argument list + * @param idx location in list where match should start + * @return location in list where next match should start + * @throws ArgParseException if there was an error performing + * the match (such as improper or insufficient values). + * @see ArgParser#setDefaultPrintStream + * @see ArgParser#getHelpOptionsEnabled + * @see ArgParser#getErrorMessage + * @see ArgParser#getUnmatchedArgument + */ + public int matchArg (String[] args, int idx) + throws ArgParseException + { + unmatchedArg = null; + setError (null); + try + { ObjectHolder ndescHolder = new ObjectHolder(); + Record rec = getRecord (args[idx], ndescHolder); + if (rec == null || (rec.convertCode=='h' && !helpOptionsEnabled)) + { // didn't match + unmatchedArg = new String(args[idx]); + return idx+1; + } + NameDesc ndesc = (NameDesc)ndescHolder.value; + Object result; + if (rec.resHolder instanceof Vector) + { result = createResultHolder (rec); + } + else + { result = rec.resHolder; + } + if (rec.convertCode == 'h') + { if (helpOptionsEnabled) + { printStream.println (getHelpMessage()); + System.exit (0); + } + else + { return idx+1; + } + } + else if (rec.convertCode != 'v') + { if (ndesc.oneWord) + { rec.scanValue ( + result, ndesc.name, + args[idx].substring (ndesc.name.length()), 0); + } + else + { if (idx+rec.numValues >= args.length) + { throw new ArgParseException ( + ndesc.name, "requires " + rec.numValues + " value" + + (rec.numValues > 1 ? "s" : "")); + } + for (int k=0; k 0) +// { ps.print (spaceString(initialIndent)); +// } +// for (int i=0; i"; +// if (rec.numValues > 1) +// { s += "X" + rec.numValues; +// } +// } +// } +// s = s + "]"; +// /* +// (col+=s.length()) > (maxcols-1) => we will spill over edge. +// we use (maxcols-1) because if we go right to the edge +// (maxcols), we get wrap new line inserted "for us". +// i != 0 means we print the first entry, no matter +// how long it is. Subsequent entries are printed +// full length anyway. */ + +// if ((col+=s.length()) > (maxcols-1) && i != 0) +// { col = initialIndent+s.length(); +// ps.print ("\n" + spaceString(initialIndent)); +// } +// ps.print (s); +// } +// if (matchList.size() > 0) +// { ps.print ('\n'); +// ps.flush(); +// } +// } + + /** + * Returns a string describing the allowed options + * in detail. + * + * @return help information string. + */ + public String getHelpMessage () + { + Record rec; + NameDesc ndesc; + boolean hasOneWordAlias = false; + String s; + + s = "Usage: " + synopsisString + "\n"; + s += "Options include:\n\n"; + for (int i=0; i"; + } + else + { optionInfo += "<" + rec.valTypeName() + ">"; + } + } + } + if (rec.numValues > 1) + { optionInfo += "X" + rec.numValues; + } + s += optionInfo; + if (rec.helpMsg.length() > 0) + { int pad = helpIndent - optionInfo.length(); + if (pad < 2) + { s += '\n'; + pad = helpIndent; + } + s += spaceString(pad) + rec.helpMsg; + } + s += '\n'; + } + return s; + } + + /** + * Returns the parser's error message. This is automatically + * set whenever an error is encountered in matchArg + * or matchAllArgs, and is automatically set to + * null at the beginning of these methods. + * + * @return error message + */ + public String getErrorMessage() + { + return errMsg; + } + + /** + * Returns the value of an unmatched argument discovered {@link + * #matchArg matchArg} or {@link #matchAllArgs(String[],int,int) + * matchAllArgs}. If there was no unmatched argument, + * null is returned. + * + * @return unmatched argument + */ + public String getUnmatchedArgument() + { + return unmatchedArg; + } +} + + diff --git a/src/argparser/BooleanHolder.java b/src/argparser/BooleanHolder.java new file mode 100644 index 0000000..a1e7bd6 --- /dev/null +++ b/src/argparser/BooleanHolder.java @@ -0,0 +1,33 @@ +package argparser; + +/** + * Wrapper class which ``holds'' a boolean value, + * enabling methods to return boolean values through + * arguments. + */ +public class BooleanHolder implements java.io.Serializable +{ + /** + * Value of the boolean, set and examined + * by the application as needed. + */ + public boolean value; + + /** + * Constructs a new BooleanHolder with an initial + * value of false. + */ + public BooleanHolder () + { value = false; + } + + /** + * Constructs a new BooleanHolder with a + * specific initial value. + * + * @param b Initial boolean value. + */ + public BooleanHolder (boolean b) + { value = b; + } +} diff --git a/src/argparser/COPYRIGHT b/src/argparser/COPYRIGHT new file mode 100644 index 0000000..b478c5e --- /dev/null +++ b/src/argparser/COPYRIGHT @@ -0,0 +1,12 @@ +/** + * Copyright John E. Lloyd, 2004. All rights reserved. Permission to use, + * copy, modify and redistribute is granted, provided that this copyright + * notice is retained and the author is given credit whenever appropriate. + * + * This software is distributed "as is", without any warranty, including + * any implied warranty of merchantability or fitness for a particular + * use. The author assumes no responsibility for, and shall not be liable + * for, any special, indirect, or consequential damages, or any damages + * whatsoever, arising out of or in connection with the use of this + * software. + */ diff --git a/src/argparser/CharHolder.java b/src/argparser/CharHolder.java new file mode 100644 index 0000000..418f666 --- /dev/null +++ b/src/argparser/CharHolder.java @@ -0,0 +1,35 @@ +package argparser; + +/** + * Wrapper class which ``holds'' a character value, + * enabling methods to return character values through + * arguments. + */ +public class CharHolder implements java.io.Serializable +{ + /** + * Value of the character, set and examined + * by the application as needed. + */ + public char value; + + /** + * Constructs a new CharHolder with an initial + * value of 0. + */ + public CharHolder () + { value = 0; + } + + /** + * Constructs a new CharHolder with a + * specific initial value. + * + * @param c Initial character value. + */ + public CharHolder (char c) + { value = c; + } +} + + diff --git a/src/argparser/DoubleHolder.java b/src/argparser/DoubleHolder.java new file mode 100644 index 0000000..d6d52e7 --- /dev/null +++ b/src/argparser/DoubleHolder.java @@ -0,0 +1,34 @@ +package argparser; + +/** + * Wrapper class which ``holds'' a double value, + * enabling methods to return double values through + * arguments. + */ +public class DoubleHolder implements java.io.Serializable +{ + /** + * Value of the double, set and examined + * by the application as needed. + */ + public double value; + + /** + * Constructs a new DoubleHolder with an initial + * value of 0. + */ + public DoubleHolder () + { value = 0; + } + + /** + * Constructs a new DoubleHolder with a + * specific initial value. + * + * @param d Initial double value. + */ + public DoubleHolder (double d) + { value = d; + } +} + diff --git a/src/argparser/FloatHolder.java b/src/argparser/FloatHolder.java new file mode 100644 index 0000000..12fe40b --- /dev/null +++ b/src/argparser/FloatHolder.java @@ -0,0 +1,35 @@ +package argparser; + +/** + * Wrapper class which ``holds'' a float value, + * enabling methods to return float values through + * arguments. + */ +public class FloatHolder implements java.io.Serializable +{ + /** + * Value of the float, set and examined + * by the application as needed. + */ + public float value; + + /** + * Constructs a new FloatHolder with an initial + * value of 0. + */ + public FloatHolder () + { value = 0; + } + + /** + * Constructs a new FloatHolder with a + * specific initial value. + * + * @param f Initial float value. + */ + public FloatHolder (float f) + { value = f; + } +} + + diff --git a/src/argparser/IntHolder.java b/src/argparser/IntHolder.java new file mode 100644 index 0000000..9f40466 --- /dev/null +++ b/src/argparser/IntHolder.java @@ -0,0 +1,34 @@ +package argparser; + +/** + * Wrapper class which ``holds'' an integer value, + * enabling methods to return integer values through + * arguments. + */ +public class IntHolder implements java.io.Serializable +{ + /** + * Value of the integer, set and examined + * by the application as needed. + */ + public int value; + + /** + * Constructs a new IntHolder with an initial + * value of 0. + */ + public IntHolder () + { value = 0; + } + + /** + * Constructs a new IntHolder with a + * specific initial value. + * + * @param i Initial integer value. + */ + public IntHolder (int i) + { value = i; + } +} + diff --git a/src/argparser/LongHolder.java b/src/argparser/LongHolder.java new file mode 100644 index 0000000..6684add --- /dev/null +++ b/src/argparser/LongHolder.java @@ -0,0 +1,34 @@ +package argparser; + +/** + * Wrapper class which ``holds'' a long value, + * enabling methods to return long values through + * arguments. + */ +public class LongHolder implements java.io.Serializable +{ + /** + * Value of the long, set and examined + * by the application as needed. + */ + public long value; + + /** + * Constructs a new LongHolder with an initial + * value of 0. + */ + public LongHolder () + { value = 0; + } + + /** + * Constructs a new LongHolder with a + * specific initial value. + * + * @param l Initial long value. + */ + public LongHolder (long l) + { value = l; + } +} + diff --git a/src/argparser/ObjectHolder.java b/src/argparser/ObjectHolder.java new file mode 100644 index 0000000..8aa4189 --- /dev/null +++ b/src/argparser/ObjectHolder.java @@ -0,0 +1,33 @@ +package argparser; + +/** + * Wrapper class which ``holds'' an Object reference, + * enabling methods to return Object references through + * arguments. + */ +public class ObjectHolder implements java.io.Serializable +{ + /** + * Value of the Object reference, set and examined + * by the application as needed. + */ + public Object value; + + /** + * Constructs a new ObjectHolder with an initial + * value of null. + */ + public ObjectHolder () + { value = null; + } + + /** + * Constructs a new ObjectHolder with a + * specific initial value. + * + * @param o Initial Object reference. + */ + public ObjectHolder (Object o) + { value = o; + } +} diff --git a/src/argparser/StringHolder.java b/src/argparser/StringHolder.java new file mode 100644 index 0000000..09c3cab --- /dev/null +++ b/src/argparser/StringHolder.java @@ -0,0 +1,34 @@ +package argparser; + +/** + * Wrapper class which ``holds'' a String reference, + * enabling methods to return String references through + * arguments. + */ +public class StringHolder implements java.io.Serializable +{ + /** + * Value of the String reference, set and examined + * by the application as needed. + */ + public String value; + + /** + * Constructs a new StringHolder with an + * initial value of null. + */ + public StringHolder () + { value = null; + } + + /** + * Constructs a new StringHolder with a + * specific initial value. + * + * @param s Initial String reference. + */ + public StringHolder (String s) + { value = s; + } +} + diff --git a/src/argparser/StringScanException.java b/src/argparser/StringScanException.java new file mode 100644 index 0000000..744dd7f --- /dev/null +++ b/src/argparser/StringScanException.java @@ -0,0 +1,37 @@ +package argparser; + +import java.io.IOException; + +/** + * Exception class used by StringScanner when + * command line arguments do not parse correctly. + * + * @author John E. Lloyd, Winter 2001 + * @see StringScanner + */ +class StringScanException extends IOException +{ + int failIdx; + + /** + * Creates a new StringScanException with the given message. + * + * @param msg Error message + * @see StringScanner + */ + + public StringScanException (String msg) + { super (msg); + } + + public StringScanException (int idx, String msg) + { + super (msg); + failIdx = idx; + } + + public int getFailIndex() + { + return failIdx; + } +} diff --git a/src/argparser/StringScanner.java b/src/argparser/StringScanner.java new file mode 100644 index 0000000..2032ed2 --- /dev/null +++ b/src/argparser/StringScanner.java @@ -0,0 +1,633 @@ +/** + * Copyright John E. Lloyd, 2004. All rights reserved. Permission to use, + * copy, modify and redistribute is granted, provided that this copyright + * notice is retained and the author is given credit whenever appropriate. + * + * This software is distributed "as is", without any warranty, including + * any implied warranty of merchantability or fitness for a particular + * use. The author assumes no responsibility for, and shall not be liable + * for, any special, indirect, or consequential damages, or any damages + * whatsoever, arising out of or in connection with the use of this + * software. + */ +package argparser; + +class StringScanner +{ + private char[] buf; + private int idx; + private int len; + private String stringDelimiters = ""; + + public StringScanner (String s) + { + buf = new char[s.length()+1]; + s.getChars (0, s.length(), buf, 0); + len = s.length(); + buf[len] = 0; + idx = 0; + } + + public int getIndex() + { return idx; + } + + public void setIndex(int i) + { if (i < 0) + { idx = 0; + } + else if (i > len) + { idx = len; + } + else + { idx = i; + } + } + + public void setStringDelimiters (String s) + { stringDelimiters = s; + } + + public String getStringDelimiters() + { return stringDelimiters; + } + + public char scanChar () + throws StringScanException + { + int idxSave = idx; + skipWhiteSpace(); + try + { if (buf[idx] == '\'') + { return scanQuotedChar(); + } + else + { return scanUnquotedChar(); + } + } + catch (StringScanException e) + { idx = idxSave; + throw e; + } + } + + public char scanQuotedChar () + throws StringScanException + { + StringScanException exception = null; + char retval = 0; + int idxSave = idx; + + skipWhiteSpace(); + if (idx == len) + { exception = new StringScanException (idx, "end of input"); + } + else if (buf[idx++] == '\'') + { try + { retval = scanUnquotedChar(); + } + catch (StringScanException e) + { exception = e; + } + if (exception==null) + { if (idx==len) + { exception = new StringScanException + (idx, "end of input"); + } + else if (buf[idx++] != '\'') + { exception = new StringScanException + (idx-1, "unclosed quoted character"); + } + } + } + else + { exception = new StringScanException + (idx-1, "uninitialized quoted character"); + } + if (exception!=null) + { idx = idxSave; + throw exception; + } + return retval; + } + + public char scanUnquotedChar () + throws StringScanException + { + StringScanException exception = null; + char c, retval = 0; + int idxSave = idx; + + if (idx == len) + { exception = new StringScanException (idx, "end of input"); + } + else if ((c = buf[idx++]) == '\\') + { if (idx == len) + { exception = new StringScanException (idx, "end of input"); + } + else + { + c = buf[idx++]; + if (c == '"') + { retval = '"'; + } + else if (c == '\'') + { retval = '\''; + } + else if (c == '\\') + { retval = '\\'; + } + else if (c == 'n') + { retval = '\n'; + } + else if (c == 't') + { retval = '\t'; + } + else if (c == 'b') + { retval = '\b'; + } + else if (c == 'r') + { retval = '\r'; + } + else if (c == 'f') + { retval = '\f'; + } + else if ('0' <= c && c < '8') + { int v = c - '0'; + for (int j=0; j<2; j++) + { if (idx==len) + { break; + } + c = buf[idx]; + if ('0' <= c && c < '8' && (v*8 + (c-'0')) <= 255) + { v = v*8 + (c-'0'); + idx++; + } + else + { break; + } + } + retval = (char)v; + } + else + { exception = new StringScanException + (idx-1, "illegal escape character '" + c + "'"); + } + } + } + else + { retval = c; + } + if (exception!=null) + { idx = idxSave; + throw exception; + } + return retval; + } + + public String scanQuotedString () + throws StringScanException + { + StringScanException exception = null; + StringBuffer sbuf = new StringBuffer(len); + char c; + int idxSave = idx; + + skipWhiteSpace(); + if (idx == len) + { exception = new StringScanException (idx, "end of input"); + } + else if ((c=buf[idx++]) == '"') + { while (idx=len) + { exception = new StringScanException (len, "end of input"); + } + else if (exception == null && c == '\n') + { exception = new StringScanException + (idx, "unclosed quoted string"); + } + else + { idx++; + } + } + else + { exception = new StringScanException (idx-1, +"quoted string must start with \""); + } + if (exception != null) + { idx = idxSave; + throw exception; + } + return sbuf.toString(); + } + + public String scanNonWhiteSpaceString() + throws StringScanException + { + StringBuffer sbuf = new StringBuffer(len); + int idxSave = idx; + char c; + + skipWhiteSpace(); + if (idx == len) + { StringScanException e = new StringScanException ( + idx, "end of input"); + idx = idxSave; + throw e; + } + else + { c = buf[idx++]; + while (idx= len) + { exception = new StringScanException (len, "end of input"); + } + else if ((charval=Character.digit(buf[idx++],radix)) == -1) + { exception = new StringScanException + (idx-1, "malformed " + baseDesc(radix) + " integer"); + } + else + { val = charval; + while ((charval=Character.digit(buf[idx],radix)) != -1) + { val = val*radix + charval; + idx++; + } + if (Character.isLetter(c=buf[idx]) || + Character.isDigit(c) || c == '_') + { exception = new StringScanException + (idx, "malformed " + baseDesc(radix) + " integer"); + } + } + if (exception != null) + { idx = idxSave; + throw exception; + } + return negate ? -val : val; + } + + public double scanDouble () + throws StringScanException + { + StringScanException exception = null; + int idxSave = idx; + char c; + // parse [-][0-9]*[.][0-9]*[eE][-][0-9]* + boolean hasDigits = false; + boolean signed; + double value = 0; + + skipWhiteSpace(); + if (idx == len) + { exception = new StringScanException ("end of input"); + } + else + { + if ((c=buf[idx]) == '-' || c == '+') + { signed = true; + idx++; + } + if (matchDigits()) + { hasDigits = true; + } + if (buf[idx] == '.') + { idx++; + } + if (!hasDigits && (buf[idx] < '0' || buf[idx] > '9')) + { if (idx==len) + { exception = new StringScanException (idx, "end of input"); + } + else + { exception = new StringScanException ( + idx, "malformed floating number: no digits"); + } + } + else + { matchDigits(); + + if ((c=buf[idx]) == 'e' || c == 'E') + { idx++; + if ((c=buf[idx]) == '-' || c == '+') + { signed = true; + idx++; + } + if (buf[idx] < '0' || buf[idx] > '9') + { if (idx==len) + { exception = new StringScanException( + idx, "end of input"); + } + else + { exception = new StringScanException (idx, +"malformed floating number: no digits in exponent"); + } + } + else + { matchDigits(); + } + } + } + } + if (exception == null) + { +// if (Character.isLetterOrDigit(c=buf[idx]) || c == '_') +// { exception = new StringScanException (idx, +//"malformed floating number"); +// } +// else + { + try + { value = Double.parseDouble(new String(buf, idxSave, + idx-idxSave)); + } + catch (NumberFormatException e) + { exception = new StringScanException ( + idx, "malformed floating number"); + } + } + } + if (exception != null) + { idx = idxSave; + throw exception; + } + return value; + } + + public boolean scanBoolean () + throws StringScanException + { + StringScanException exception = null; + int idxSave = idx; + String testStr = "false"; + boolean testval = false; + char c; + + skipWhiteSpace(); + if (buf[idx] == 't') + { testStr = "true"; + testval = true; + } + else + { testval = false; + } + int i = 0; + for (i=0; i= len || s.charAt(i) != buf[k++]) + { return false; + } + } + idx = k; + return true; + } + + public boolean matchDigits () + { + int k = idx; + char c; + + while ((c=buf[k]) >= '0' && c <= '9') + { k++; + } + if (k > idx) + { idx = k; + return true; + } + else + { return false; + } + } + + public void skipWhiteSpace() + { + while (Character.isWhitespace(buf[idx])) + { idx++; + } + } + + private int skipWhiteSpace(int k) + { + while (Character.isWhitespace(buf[k])) + { k++; + } + return k; + } + + public boolean atEnd() + { + return idx == len; + } + + public boolean atBeginning() + { + return idx == 0; + } + + public void ungetc() + { + if (idx > 0) + { idx--; + } + } + + public char getc() + { + char c = buf[idx]; + if (idx < len) + { idx++; + } + return c; + } + + public char peekc() + { + return buf[idx]; + } + + public String substring (int i0, int i1) + { + if (i0 < 0) + { i0 = 0; + } + else if (i0 >= len) + { i0= len-1; + } + if (i1 < 0) + { i1 = 0; + } + else if (i1 > len) + { i1= len; + } + if (i1 <= i0) + { return ""; + } + return new String (buf, i0, i1-i0); + } + + public String substring (int i0) + { + if (i0 < 0) + { i0 = 0; + } + if (i0 >= len) + { return ""; + } + else + { return new String (buf, i0, len-i0); + } + } +} diff --git a/src/argparser/package.html b/src/argparser/package.html new file mode 100644 index 0000000..555e2c0 --- /dev/null +++ b/src/argparser/package.html @@ -0,0 +1,7 @@ + +argparser is a package for parsing the command line arguments +of a Java program; detailed documentation is supplied +in the class documentation for ArgParser. + + + diff --git a/src/jpsxdec/LogToFile.properties b/src/jpsxdec/LogToFile.properties index aeb66d1..eb44c3f 100644 --- a/src/jpsxdec/LogToFile.properties +++ b/src/jpsxdec/LogToFile.properties @@ -1,6 +1,6 @@ handlers=java.util.logging.FileHandler -java.util.logging.FileHandler.pattern=out%g-%u.log +java.util.logging.FileHandler.pattern=debug%g.log java.util.logging.FileHandler.formatter=jpsxdec.util.BriefFormatter java.util.logging.FileHandler.count=3 java.util.logging.FileHandler.level=ALL diff --git a/src/jpsxdec/Main.java b/src/jpsxdec/Main.java index 8d74b22..a8096ca 100644 --- a/src/jpsxdec/Main.java +++ b/src/jpsxdec/Main.java @@ -59,7 +59,7 @@ import jpsxdec.plugins.JPSXPlugin; import jpsxdec.plugins.psx.str.DiscItemSTRVideo; import jpsxdec.plugins.psx.str.IVideoSector; -import jpsxdec.plugins.xa.IDiscItemAudioStream; +import jpsxdec.plugins.xa.DiscItemAudioStream; import jpsxdec.util.FeedbackStream; import jpsxdec.util.IO; import jpsxdec.util.NotThisTypeException; @@ -71,7 +71,7 @@ public class Main { private static FeedbackStream Outputter; - public final static String Version = "0.90.0 (alpha)"; + public final static String Version = "0.91.0 (alpha)"; public final static String VerString = "jPSXdec: PSX media decoder, v" + Version; private static MainCommandLineParser _mainSettings; @@ -203,18 +203,19 @@ private static CDSectorReader openCD(boolean blnCheckHasHeaders) { private static DiscIndex doTheIndex(CDSectorReader cdReader, boolean blnIndexOnly) { // index the STR file - DiscIndex discIndex = new DiscIndex(cdReader); + DiscIndex discIndex; boolean blnSaveIndexFile = false; String sIndexFile = _mainSettings.getIndexFile(); if (sIndexFile != null) { if (blnIndexOnly || !new File(_mainSettings.getIndexFile()).exists()) { Outputter.printlnNorm("Building index"); - discIndex.indexDisc(new ConsoleProgressListener(Outputter)); + discIndex = new DiscIndex(cdReader, new ConsoleProgressListener(Outputter)); blnSaveIndexFile = true; } else { Outputter.printlnNorm("Reading index file " + sIndexFile); try { - discIndex.deserializeIndex(sIndexFile); + discIndex = new DiscIndex(sIndexFile, cdReader, Outputter); + Outputter.printlnNorm(discIndex.size() + " items loaded."); } catch (NotThisTypeException ex) { log.log(Level.SEVERE, "Reading index error", ex); Outputter.printlnErr("Invalid index file"); @@ -227,7 +228,8 @@ private static DiscIndex doTheIndex(CDSectorReader cdReader, boolean blnIndexOnl } } else { Outputter.printlnNorm("Building index"); - discIndex.indexDisc(new ConsoleProgressListener(Outputter)); + discIndex = new DiscIndex(cdReader, new ConsoleProgressListener(Outputter)); + Outputter.printlnNorm(discIndex.size() + " items found."); } // save index file if necessary @@ -548,29 +550,20 @@ private static int player() { DiscItemSTRVideo video = (DiscItemSTRVideo) item; if (video.hasAudio()) { - IDiscItemAudioStream audio = video.getParallelAudioStreams()[0]; + DiscItemAudioStream audio = video.getParallelAudioStream(0); int iStartSector = Math.min(video.getStartSector(), audio.getStartSector()); - int iEndSector = Math.min(video.getEndSector(), audio.getEndSector()); - controller = new PlayController( - new MediaPlayer.StrReader(video, iStartSector, iEndSector), - new MediaPlayer.XAAudioDecoder(audio), - new MediaPlayer.StrVideoDecoder(video)); + int iEndSector = Math.max(video.getEndSector(), audio.getEndSector()); + controller = new PlayController(new MediaPlayer(video, audio.makeDecoder(true, 1.0), iStartSector, iEndSector)); } else { - controller = new PlayController( - new MediaPlayer.StrReader(video), - null, - new MediaPlayer.StrVideoDecoder(video)); + controller = new PlayController(new MediaPlayer(video)); } - } else if (item instanceof IDiscItemAudioStream) { + } else if (item instanceof DiscItemAudioStream) { Outputter.printlnNorm("Creating player for"); Outputter.printlnNorm(item.toString()); - IDiscItemAudioStream audio = (IDiscItemAudioStream) item; + DiscItemAudioStream audio = (DiscItemAudioStream) item; - controller = new PlayController( - new MediaPlayer.StrReader(audio), - new MediaPlayer.XAAudioDecoder(audio), - null); + controller = new PlayController(new MediaPlayer(audio)); } else { Outputter.printlnErr(item.toString()); Outputter.printlnErr("is not audio or video. Cannot create player."); diff --git a/src/jpsxdec/MainCommandLineParser.java b/src/jpsxdec/MainCommandLineParser.java index e57a752..d78a568 100644 --- a/src/jpsxdec/MainCommandLineParser.java +++ b/src/jpsxdec/MainCommandLineParser.java @@ -59,7 +59,10 @@ public class MainCommandLineParser { private static String[] MAIN_HELP = loadMainHelp(); private static String[] loadMainHelp() { - InputStream is = MainCommandLineParser.class.getResourceAsStream("maincmdlinehelp.txt"); + InputStream is = MainCommandLineParser.class.getResourceAsStream("main_cmdline_help.dat"); + if (is == null) + throw new RuntimeException("Unable to find help resource " + + MainCommandLineParser.class.getResource("main_cmdline_help.dat")); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); ArrayList lines = new ArrayList(); diff --git a/src/jpsxdec/MediaPlayer.java b/src/jpsxdec/MediaPlayer.java index 113972a..35cd191 100644 --- a/src/jpsxdec/MediaPlayer.java +++ b/src/jpsxdec/MediaPlayer.java @@ -38,289 +38,311 @@ package jpsxdec; import java.awt.image.BufferedImage; +import java.awt.image.WritableRaster; import java.io.FileNotFoundException; import java.io.IOException; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.SourceDataLine; import javax.sound.sampled.UnsupportedAudioFileException; import jpsxdec.cdreaders.CDSector; +import jpsxdec.cdreaders.CDSectorReader; import jpsxdec.formats.RgbIntImage; import jpsxdec.player.AudioProcessor; -import jpsxdec.player.IAudioDecoder; import jpsxdec.player.IAudioVideoReader; import jpsxdec.player.IDecodableAudioChunk; import jpsxdec.player.IDecodableFrame; -import jpsxdec.player.IVideoDecoder; +import jpsxdec.player.ObjectPool; import jpsxdec.player.VideoProcessor; -import jpsxdec.plugins.DiscItem; import jpsxdec.plugins.IdentifiedSector; import jpsxdec.plugins.JPSXPlugin; import jpsxdec.plugins.psx.str.DiscItemSTRVideo; import jpsxdec.plugins.psx.str.IVideoSector; -import jpsxdec.plugins.psx.str.StrFramePushDemuxer; -import jpsxdec.plugins.psx.video.DemuxImage; +import jpsxdec.plugins.psx.str.FrameDemuxer; +import jpsxdec.plugins.psx.str.IDemuxReceiver; import jpsxdec.plugins.psx.video.decode.DemuxFrameUncompressor; import jpsxdec.plugins.psx.video.decode.UncompressionException; import jpsxdec.plugins.psx.video.mdec.MdecDecoder_int; import jpsxdec.plugins.psx.video.mdec.idct.simple_idct; -import jpsxdec.plugins.xa.IDiscItemAudioSectorDecoder; -import jpsxdec.plugins.xa.IDiscItemAudioStream; +import jpsxdec.plugins.xa.IAudioSectorDecoder; +import jpsxdec.plugins.xa.DiscItemAudioStream; import jpsxdec.util.NotThisTypeException; -import jpsxdec.util.SourceDataLineAudioOutputStream; /** Holds all the class implementations that the jpsxdec.player framework * needs to playback PlayStation audio and/or video. */ -public class MediaPlayer { - - public static class StrReader implements IAudioVideoReader { +public class MediaPlayer implements IAudioVideoReader { - private final DiscItem _mediaItem; + private static final boolean DEBUG = false; - private final int _iStartSector; - private final int _iEndSector; - private int _iFrame; - private StrFramePushDemuxer _demuxer; - private int _iSector; + private final int _iMovieStartSector; + private final int _iMovieEndSector; + private int _iSector; + private final CDSectorReader _cdReader; - public StrReader(IDiscItemAudioStream aud) - throws FileNotFoundException, UnsupportedAudioFileException, - IOException - { - _mediaItem = (DiscItem) aud; - _iSector = _iStartSector = aud.getStartSector(); - _iEndSector = aud.getEndSector(); + //---------------------------------------------------------- - _iFrame = -1; + private final MdecDecoder_int _decoder; + private final DiscItemSTRVideo _vid; + private final int _iSectorsPerSecond = 150; + private final int[] _aiFrameIndexes; + private DemuxFrameUncompressor _uncompressor; + private FrameDemuxer _demuxer; - _demuxer = new StrFramePushDemuxer(_iFrame); - } + public MediaPlayer(DiscItemSTRVideo vid) + throws UnsupportedAudioFileException, IOException + { + this(vid, vid.getStartSector(), vid.getEndSector()); + } - public StrReader(DiscItemSTRVideo vid) - throws UnsupportedAudioFileException, IOException - { - this(vid, vid.getStartSector(), vid.getEndSector()); - } - - public StrReader(DiscItemSTRVideo vid, int iSectorStart, int iSectorEnd) - throws UnsupportedAudioFileException, IOException - { - _mediaItem = vid; - _iSector = _iStartSector = iSectorStart; - _iEndSector = iSectorEnd; - _iFrame = vid.getStartFrame(); + public MediaPlayer(DiscItemSTRVideo vid, int iSectorStart, int iSectorEnd) + throws UnsupportedAudioFileException, IOException + { + _cdReader = vid.getSourceCD(); + _iSector = _iMovieStartSector = iSectorStart; + _iMovieEndSector = iSectorEnd; - _demuxer = new StrFramePushDemuxer(_iFrame); - } + _vid = vid; + _decoder = new MdecDecoder_int(new simple_idct(), + vid.getWidth(), + vid.getHeight()); - /** Adds a video sector to a frame demuxer. It turns out to be more - * complicated than you'd think. */ - private static StrFramePushDemuxer addToDemux(VideoProcessor vidProc, - StrFramePushDemuxer demuxer, - IVideoSector vidSector, - int iSectorsFromStart) - throws IOException - { - if (demuxer == null) { - // create the demuxer for the sector's frame - demuxer = new StrFramePushDemuxer(vidSector.getFrameNumber()); - } - if (demuxer.getFrameNumber() == vidSector.getFrameNumber()) { - // add the sector if it is the same frame number - demuxer.addChunk(vidSector); - } else { - // if sector has a different frame number, close off the demuxer - DemuxImage demuxFrame = demuxer.getDemuxFrame(); - // create a new one with this new sector - demuxer = new StrFramePushDemuxer(); - demuxer.addChunk(vidSector); - // and send the finished frame thru the pipe - // (wanted to wait in case of an error) - vidProc.addFrame(new StrFrame(demuxFrame, iSectorsFromStart)); - } - if (demuxer.isFull()) { - // send the image thru the pipe if it is complete - DemuxImage demuxFrame = demuxer.getDemuxFrame(); - demuxer = null; - vidProc.addFrame(new StrFrame(demuxFrame, iSectorsFromStart)); - } - return demuxer; - } + _aiFrameIndexes = new int[_vid.getEndFrame() - _vid.getStartFrame() + 1]; + } - public boolean readNext(VideoProcessor vidProc, AudioProcessor audProc) { + //----------------------------------------------------------------------- - try { + private final static boolean AUDIO_BIGENDIAN = true; + private IAudioSectorDecoder _audioDecoder; + private SourceDataLineAudioReceiver _audioOut; - if (!(_iSector <= _iEndSector)) - return false; - - CDSector cdSector = _mediaItem.getSourceCD().getSector(_iSector); - IdentifiedSector identifiedSector = JPSXPlugin.identifyPluginSector(cdSector); - if (vidProc != null && identifiedSector instanceof IVideoSector) { - if (_mediaItem.getStartSector() <= _iSector && - _iSector <= _mediaItem.getEndSector()) - { - IVideoSector vidSector = (IVideoSector) identifiedSector; - _demuxer = addToDemux(vidProc, _demuxer, vidSector, _iSector - _iStartSector); - _iFrame = vidSector.getFrameNumber(); - } - } else if (audProc != null && identifiedSector != null) { - audProc.addDecodableAudioChunk(new XAAudioChunk(identifiedSector)); - } - _iSector++; - return true; + public MediaPlayer(DiscItemAudioStream aud) + throws FileNotFoundException, UnsupportedAudioFileException, + IOException + { + _cdReader = aud.getSourceCD(); + _iSector = _iMovieStartSector = aud.getStartSector(); + _iMovieEndSector = aud.getEndSector(); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - public void seekToTime(long lngTime) { - throw new RuntimeException(); - } + _audioDecoder = aud.makeDecoder(AUDIO_BIGENDIAN, 1.0); - public void reset() { - throw new RuntimeException(); - } + // ignore video + _decoder = null; + _vid = null; + _aiFrameIndexes = null; + } + + //---------------------------------------------------------- - public void seekToFrame(int iFrame) { - throw new RuntimeException(); - } + MediaPlayer(DiscItemSTRVideo vid, IAudioSectorDecoder audio, int iSectorStart, int iSectorEnd) throws UnsupportedAudioFileException, IOException { + // do the video init + this(vid, iSectorStart, iSectorEnd); + // manually init the audio + _audioDecoder = audio; } - public static class XAAudioChunk implements IDecodableAudioChunk { - private IdentifiedSector _sector; + public int readNext(VideoProcessor vidProc, AudioProcessor audProc) { - public XAAudioChunk(IdentifiedSector sector) { - _sector = sector; - } + try { + + if (!(_iSector < _iMovieEndSector)) { + return -1; + } + CDSector cdSector = _cdReader.getSector(_iSector); + IdentifiedSector identifiedSector = JPSXPlugin.identifyPluginSector(cdSector); + if (vidProc != null && identifiedSector instanceof IVideoSector) { + if (_demuxer == null) { + _demuxer = new FrameDemuxer( + new DemuxReceiver(vidProc), + _iMovieStartSector, _iMovieEndSector); + } + _demuxer.feedSector((IVideoSector) identifiedSector); + } else if (audProc != null && identifiedSector != null) { + audProc.addDecodableAudioChunk(new XAAudioChunk(identifiedSector)); + } + _iSector++; + return (_iSector - _iMovieStartSector) * 100 / (_iMovieEndSector - _iMovieStartSector); + + } catch (IOException ex) { + throw new RuntimeException(ex); + } } - public static class StrFrame implements IDecodableFrame { + private class DemuxReceiver implements IDemuxReceiver { - private final DemuxImage _demux; - private int _iSectorsFromStart; + private VideoProcessor _vidProc; - public StrFrame(DemuxImage imgFile, int iSectorsFromStart) throws IOException { - _demux = imgFile; - _iSectorsFromStart = iSectorsFromStart; + public DemuxReceiver(VideoProcessor vidProc) { + _vidProc = vidProc; } + + @Override public int getWidth() { - return _demux.getWidth(); + return _vid.getWidth(); } + @Override public int getHeight() { - return _demux.getHeight(); + return _vid.getHeight(); } - public int getFrameNumber() { - return _demux.getFrameNumber(); + @Override + public void receive(byte[] abDemux, int iSize, int iFrameNumber, int iFrameEndSector) throws IOException { + StrFrame strFrame = _framePool.borrow(); + strFrame.init(abDemux, iSize, iFrameNumber, iFrameEndSector - _iMovieStartSector); + _vidProc.addFrame(strFrame); } + } - public long getPresentationTime() { - return (long)(_iSectorsFromStart * 1000 / 150); - } + public void seekToTime(long lngTime) { + if (_audioDecoder != null) + _audioDecoder.reset(); + _iSector = _iMovieStartSector + (int)(lngTime * _iSectorsPerSecond / 1000); + throw new RuntimeException(); + // TODO: either backup or move forward to the beginning of a frame (if there is video) + } + public void reset() { + if (_audioDecoder != null) + _audioDecoder.reset(); + _iSector = _iMovieStartSector; } - public static class XAAudioDecoder implements IAudioDecoder { + // ######################################################################### + // ######################################################################### - private IDiscItemAudioStream _audioDiscItem; - private IDiscItemAudioSectorDecoder _audioDecoder; - private final static boolean BIGENDIAN = true; + @Override + public AudioFormat getAudioFormat() { + if (_audioDecoder == null) + return null; + return _audioDecoder.getOutputFormat(); + } - public XAAudioDecoder(IDiscItemAudioStream audStream) { - _audioDiscItem = audStream; - } + private class XAAudioChunk implements IDecodableAudioChunk { - public void initialize(SourceDataLine dataLine) { - _audioDecoder = _audioDiscItem.makeDecoder( - new SourceDataLineAudioOutputStream(dataLine), BIGENDIAN, 1.0); + private IdentifiedSector __sector; + + public XAAudioChunk(IdentifiedSector sector) { + __sector = sector; } - public void decodeAudio(IDecodableAudioChunk audioChunk, SourceDataLine dataLine) - throws IOException - { - if (audioChunk instanceof XAAudioChunk) { - _audioDecoder.feedSector(((XAAudioChunk)audioChunk)._sector); + @Override + public void decodeAudio(SourceDataLine dataLine) throws IOException { + if (_audioOut == null) { + _audioOut = new SourceDataLineAudioReceiver(dataLine, _iSectorsPerSecond, _iMovieStartSector); + _audioDecoder.open(_audioOut); } + _audioDecoder.feedSector(__sector); } - public void reset() { - _audioDecoder.reset(); + } + + + // ######################################################################### + // ######################################################################### + + private class DecodableFramePool extends ObjectPool { + + @Override + protected StrFrame createExpensiveObject() { + if (DEBUG) System.err.println("Creating new pool object."); + return new StrFrame(); } - public AudioFormat getAudioFormat() { - return _audioDiscItem.getAudioFormat(BIGENDIAN); + } + private final DecodableFramePool _framePool = new DecodableFramePool(); + + + public void seekToFrame(int iFrame) { + if (_aiFrameIndexes[iFrame - _vid.getStartFrame()] < 1) { + try { + _iSector = _vid.seek(iFrame).getSectorNumber(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } else { + _iSector = _aiFrameIndexes[iFrame - _vid.getStartFrame()]; } + if (_audioDecoder != null) + _audioDecoder.reset(); + // TODO? backup and get the audio for this frame? + } + @Override + public boolean hasVideo() { + return _vid != null; + } + @Override + public int getVideoWidth() { + return _vid.getWidth(); } - public static class StrVideoDecoder implements IVideoDecoder { + @Override + public int getVideoHeight() { + return _vid.getHeight(); + } - private final DiscItemSTRVideo _sourceVidItem; - private final MdecDecoder_int _decoder; - private final RgbIntImage _rgb; - private DemuxFrameUncompressor _uncompressor; - public StrVideoDecoder(DiscItemSTRVideo sourceVidItem) { - _sourceVidItem = sourceVidItem; - _decoder = new MdecDecoder_int(new simple_idct(), - _sourceVidItem.getWidth(), - _sourceVidItem.getHeight()); - _rgb = new RgbIntImage(_sourceVidItem.getWidth(), - _sourceVidItem.getHeight()); + private class StrFrame implements IDecodableFrame { + + private byte[] __abDemuxBuf; + private int __iFrame; + private int __iSectorFromStart; + + public void init(byte[] abDemuxBuf, int iSize, int iFrame, int iSectorFromStart) { + if (__abDemuxBuf == null || __abDemuxBuf.length < iSize) + __abDemuxBuf = new byte[iSize]; + System.arraycopy(abDemuxBuf, 0, __abDemuxBuf, 0, iSize); + __iSectorFromStart = iSectorFromStart; + __iFrame = iFrame; } - protected DemuxFrameUncompressor getUncompressor(DemuxImage demux) { + public long getPresentationTime() { + return (long)(__iSectorFromStart * 1000 / _iSectorsPerSecond); + } + + public void decodeVideo(RgbIntImage drawHere) { if (_uncompressor == null) { - _uncompressor = JPSXPlugin.identifyUncompressor(demux); + _uncompressor = JPSXPlugin.identifyUncompressor(__abDemuxBuf, 0, __iFrame); if (_uncompressor == null) { - return null; + System.err.println("Unable to identify frame type."); + return; } } try { - _uncompressor.reset(demux.getData()); + _uncompressor.reset(__abDemuxBuf, 0); } catch (NotThisTypeException ex) { - _uncompressor = JPSXPlugin.identifyUncompressor(demux); + _uncompressor = JPSXPlugin.identifyUncompressor(__abDemuxBuf, 0, __iFrame); if (_uncompressor == null) { - return null; + System.err.println("Unable to identify frame type."); + return; } } - return _uncompressor; - } + try { + _decoder.decode(_uncompressor); + } catch (UncompressionException ex) { + ex.printStackTrace(); + } - public void reset() { - // nothing to do + _decoder.readDecodedRGB(drawHere); } - public BufferedImage decodeVideo(IDecodableFrame frame, BufferedImage usable) { - if (frame instanceof StrFrame) { - StrFrame pframe = (StrFrame)frame; - DemuxFrameUncompressor uncompressor = getUncompressor(pframe._demux); - try { - _decoder.decode(uncompressor); - } catch (UncompressionException ex) { - ex.printStackTrace(); - } - _decoder.readDecodedRGB(_rgb); - return _rgb.toBufferedImage(); - } - return null; - } + private long _lngContiguousId; + @Override + public void setContiguiousId(long lngId) { _lngContiguousId = lngId; } + @Override + public long getContigiousId() { return _lngContiguousId; } - public int getWidth() { - return _sourceVidItem.getWidth(); + @Override + public void returnToPool() { + if (DEBUG) System.err.println("Returning object to pool."); + _framePool.giveBack(this); } - public int getHeight() { - return _sourceVidItem.getHeight(); - } } } diff --git a/src/jpsxdec/util/SourceDataLineAudioOutputStream.java b/src/jpsxdec/SourceDataLineAudioReceiver.java similarity index 67% rename from src/jpsxdec/util/SourceDataLineAudioOutputStream.java rename to src/jpsxdec/SourceDataLineAudioReceiver.java index f5ed18e..a80dbc3 100644 --- a/src/jpsxdec/util/SourceDataLineAudioOutputStream.java +++ b/src/jpsxdec/SourceDataLineAudioReceiver.java @@ -35,19 +35,28 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package jpsxdec.util; +package jpsxdec; +import jpsxdec.plugins.xa.AudioSync; import java.io.IOException; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.SourceDataLine; +import jpsxdec.plugins.xa.IAudioReceiver; /** Wraps SourceDataLine with my AudioOutputStream interface. */ -public class SourceDataLineAudioOutputStream implements AudioOutputStream { +public class SourceDataLineAudioReceiver implements IAudioReceiver { - private SourceDataLine _dataLine; + private final SourceDataLine _dataLine; + private AudioSync _audioSync; - public SourceDataLineAudioOutputStream(SourceDataLine dataLine) { + private final int _iFrameSize; + private byte[] _abZeroBuff; + + public SourceDataLineAudioReceiver(SourceDataLine dataLine, int iSectorsPerSecond, int iMovieStartSector) { _dataLine = dataLine; + _iFrameSize = _dataLine.getFormat().getFrameSize(); + + _audioSync = new AudioSync(iMovieStartSector, iSectorsPerSecond, _dataLine.getFormat().getSampleRate()); } public void close() throws IOException { @@ -58,10 +67,22 @@ public AudioFormat getFormat() { return _dataLine.getFormat(); } - public void write(AudioFormat inFormat, byte[] abData, int iOffset, int iLength) throws IOException { + public void write(AudioFormat inFormat, byte[] abData, int iOffset, int iLength, int iEndingSector) throws IOException { if (!inFormat.matches(_dataLine.getFormat())) throw new IllegalArgumentException("Incompatable audio format."); + + long lngSampleDiff = _audioSync.calculateNeededSilence(iEndingSector, iLength / _iFrameSize); + + if (lngSampleDiff > 0) { + System.out.println("Audio out of sync " + lngSampleDiff + " samples, adding silence."); + if (_abZeroBuff == null) + _abZeroBuff = new byte[_iFrameSize * 2048]; + long lngBytesToWrite = lngSampleDiff * _iFrameSize; + while (lngBytesToWrite > 0) { + lngBytesToWrite -= _dataLine.write(_abZeroBuff, 0, (int)Math.min(lngBytesToWrite, _abZeroBuff.length)); + } + } + _dataLine.write(abData, iOffset, iLength); } - } diff --git a/src/jpsxdec/cdreaders/CDFileSectorReader.java b/src/jpsxdec/cdreaders/CDFileSectorReader.java index e893990..e16cb0c 100644 --- a/src/jpsxdec/cdreaders/CDFileSectorReader.java +++ b/src/jpsxdec/cdreaders/CDFileSectorReader.java @@ -235,8 +235,7 @@ public void writeSector(int iSector, byte[] abSrcUserData) /* Private Functions ---------------------------------------------------- */ /* ---------------------------------------------------------------------- */ - /** Searches through the entire file for a full XA audio sector. - * TODO: That probably isn't a very good idea. + /** Searches through the first 33 sectors for a full XA audio sector. *

* Note: This assumes the input file has the data aligned at every 4 bytes! */ @@ -326,7 +325,7 @@ private boolean test2352() throws IOException { } public String serialize() { - return String.format("SourceFile|%s|%d|%d|%d", + return String.format("Filename:%s|Sector size:%d|Sector count:%d|First sector offset:%d", _sSourceFilePath, _iRawSectorTypeSize, _iSectorCount, diff --git a/src/jpsxdec/cdreaders/CDSector.java b/src/jpsxdec/cdreaders/CDSector.java index b369873..0f96844 100644 --- a/src/jpsxdec/cdreaders/CDSector.java +++ b/src/jpsxdec/cdreaders/CDSector.java @@ -189,7 +189,7 @@ public DATA_AUDIO_VIDEO getDataAudioVideo() { case 1: data_audio_video = DATA_AUDIO_VIDEO.VIDEO; break; case 0: data_audio_video = DATA_AUDIO_VIDEO.NULL; break; default: throw new NotThisTypeException( - "CD sector submode data,audio,video is corrupted: " + Misc.bitsToString(iBits, 3) + "b"); + "CD sector submode data|audio|video is corrupted: " + Misc.bitsToString(iBits, 3) + "b"); } end_audio = (b & 1) > 0; } @@ -409,6 +409,10 @@ public byte[] getCdUserDataCopy() { return jpsxdec.util.Misc.copyOfRange(_abSectorBytes, _iUserDataStart, _iUserDataStart+_iUserDataSize); } + public byte readUserDataByte(int i) { + return _abSectorBytes[_iUserDataStart + 1]; + } + public void getCdUserDataCopy(int iSourcePos, byte[] abOut, int iOutPos, int iLength) { System.arraycopy(_abSectorBytes, _iUserDataStart + iSourcePos, abOut, iOutPos, iLength); } diff --git a/src/jpsxdec/formats/JavaImageFormat.java b/src/jpsxdec/formats/JavaImageFormat.java index a95be24..624f192 100644 --- a/src/jpsxdec/formats/JavaImageFormat.java +++ b/src/jpsxdec/formats/JavaImageFormat.java @@ -124,7 +124,7 @@ public static List getAvailable() { /* ###################################################################### */ public static enum JpgQualities { - LOW_QUALITY("small", "Low quality (small)", 0.25f), + LOW_QUALITY("low", "Low quality (small)", 0.25f), MEDIUM_QUALITY("medium","Medium quality", 0.5f), GOOD_QUALITY("good","Good quality (default)", 0.75f), HIGH_QUALITY("high","High quality", 0.95f); diff --git a/src/jpsxdec/formats/RgbIntImage.java b/src/jpsxdec/formats/RgbIntImage.java index 9405481..9034964 100644 --- a/src/jpsxdec/formats/RgbIntImage.java +++ b/src/jpsxdec/formats/RgbIntImage.java @@ -38,6 +38,7 @@ package jpsxdec.formats; import java.awt.image.BufferedImage; +import java.awt.image.PixelGrabber; import jpsxdec.util.Imaging; /** Simplest image format containing a buffer and dimensions. */ @@ -60,10 +61,29 @@ public String toString() { private final int _iWidth, _iHeight; - private final int[] _aiData; + private int[] _aiData; public RgbIntImage(BufferedImage bi) { this(bi.getWidth(), bi.getHeight()); + // TODO: WARNING! This may not return accurate colors! + PixelGrabber grabber = new PixelGrabber(bi, 0, 0, bi.getWidth(), bi.getHeight(), false); + try + { + if(grabber.grabPixels() != true) { + throw new RuntimeException("Grabber returned false: " + grabber.status()); + } + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + + Object pixels = grabber.getPixels(); + if (pixels instanceof int[]) { + _aiData = (int[]) pixels; + } else { + throw new RuntimeException("Got byte pixels"); + } + bi.getRGB(0, 0, _iWidth, _iHeight, _aiData, 0, _iWidth); } diff --git a/src/jpsxdec/formats/Yuv4mpeg2.java b/src/jpsxdec/formats/Yuv4mpeg2.java index 7b44e7f..b4c15bc 100644 --- a/src/jpsxdec/formats/Yuv4mpeg2.java +++ b/src/jpsxdec/formats/Yuv4mpeg2.java @@ -120,13 +120,13 @@ public String toString() { /** Holds luminance values. * Package private so Yuv4mpeg2Writer can access it. */ - double[] _adblY; + public byte[] _abY; /** Holds chrominance blue values, subsampled like jpeg or mpeg1. * Package private so Yuv4mpeg2Writer can access it. */ - double[] _adblCb; + public byte[] _abCb; /** Holds chorminance red values, subsampled like jpeg or mpeg1. * Package private so Yuv4mpeg2Writer can access it. */ - double[] _adblCr; + public byte[] _abCr; /** Creates a new instance of Yuv4mpeg2 * @param iSrcWidth - Width of image (in Luminance values) @@ -141,11 +141,36 @@ public Yuv4mpeg2(int iWidth, int iHeight) { _iLuminHeight = iHeight; _iChromWidth = iWidth / 2; _iChromHeight = iHeight / 2; - _adblY = new double[_iLuminWidth * _iLuminHeight]; - _adblCb = new double[_iChromWidth * _iChromHeight]; - _adblCr = new double[_adblCb.length]; + _abY = new byte[_iLuminWidth * _iLuminHeight]; + _abCb = new byte[_iChromWidth * _iChromHeight]; + _abCr = new byte[_abCb.length]; } - + + /** Very slow, wasteful, and impercise conversion. */ + public Yuv4mpeg2(BufferedImage rgb) { + this(rgb.getWidth(), rgb.getHeight()); + + double[] adblCb = new double[_iChromWidth * _iChromHeight]; + double[] adblCr = new double[adblCb.length]; + + for (int x = 0; x < _iLuminWidth; x++) { + for (int y = 0; y < _iLuminHeight; y++) { + int iRgb = rgb.getRGB(x, y); + CCIR601YCbCr ycc = new CCIR601YCbCr(new RGB((iRgb >> 16) & 0xFF, + (iRgb >> 8) & 0xFF, + (iRgb ) & 0xFF)); + _abY[ x + y * _iLuminWidth ] = (byte)ycc.y; + adblCb[x/2 + (y/2) * _iChromWidth] += ycc.cb + 128.0; + adblCr[x/2 + (y/2) * _iChromWidth] += ycc.cr + 128.0; + } + } + + for (int i = 0; i < _abCb.length; i++) { + _abCb[i] = (byte) (_abCb[i] / 4.0); + _abCr[i] = (byte) (_abCr[i] / 4.0); + } + } + public int getWidth() { return _iLuminWidth; } @@ -154,6 +179,16 @@ public int getHeight() { return _iLuminHeight; } + public byte[] getY() { + return _abY; + } + public byte[] getCb() { + return _abCb; + } + public byte[] getCr() { + return _abCr; + } + /** Converts yuv image to a BufferedImage, converting, rounding, and * clamping RGB values. Uses default image type. */ public BufferedImage toBufferedImage() { @@ -170,9 +205,9 @@ public BufferedImage toBufferedImage(int iImgType) { for (int iLinePos = 0, iY = 0; iY < _iLuminHeight; iLinePos += _iLuminWidth, iY++) { for (int iX = 0; iX < _iLuminWidth; iX++) { - double y = _adblY[iLinePos + iX] + 128; - double cb = _adblCb[iLinePos + iX / 2]; - double cr = _adblCr[iLinePos + iX / 2]; + int y = _abY[iLinePos + iX] & 0xff; + int cb = (_abCb[iLinePos + iX / 2] & 0xff) - 128; + int cr = (_abCr[iLinePos + iX / 2] & 0xff) - 128; int r = (int)jpsxdec.util.Maths.round(y + 1.402 * cr); int g = (int)jpsxdec.util.Maths.round(y - 0.3437 * cb - 0.7143 * cr); @@ -198,26 +233,26 @@ public BufferedImage toBufferedImage(int iImgType) { /** Sets a luminance value. * @param iLuminX X lumin pixel to set. * @param iLuminY Y lumin pixel to set. - * @param dblY New value. + * @param bY New value. */ - public void setY(int iLuminX, int iLuminY, double dblY) { - _adblY[iLuminX + iLuminY * _iLuminWidth] = dblY; + public void setY(int iLuminX, int iLuminY, byte bY) { + _abY[iLuminX + iLuminY * _iLuminWidth] = bY; } /** Sets chrominance blue value. * @param iChromX X chrom pixel (1/2 lumin width) * @param iChromY Y chrom pixel (1/2 lumin width) - * @param dblCb New value. + * @param bCb New value. */ - public void setCb(int iChromX, int iChromY, double dblCb) { - _adblCb[iChromX + iChromY * _iChromWidth] = dblCb; + public void setCb(int iChromX, int iChromY, byte bCb) { + _abCb[iChromX + iChromY * _iChromWidth] = bCb; } /** Sets chrominance red value. * @param iChromX X chrom pixel (1/2 lumin width) * @param iChromY Y chrom pixel (1/2 lumin width) - * @param dblCr New value. + * @param bCr New value. */ - public void setCr(int iChromX, int iChromY, double dblCr) { - _adblCr[iChromX + iChromY * _iChromWidth] = dblCr; + public void setCr(int iChromX, int iChromY, byte bCr) { + _abCr[iChromX + iChromY * _iChromWidth] = bCr; } /** Set a block of luminance values @@ -225,46 +260,46 @@ public void setCr(int iChromX, int iChromY, double dblCr) { * @param iDestY Top left corner where block starts (in Luminance pixels) * @param iSrcWidth Width of block (in Luminance pixels) * @param iSrcHeight Height of block (in Luminance pixels) - * @param adblCb Array of block values with the color space -128 to +127.*/ + * @param abY Array of block values with the color space -128 to +127.*/ public void setY(int iDestX, int iDestY, int iSrcOfs, int iSrcWidth, int iCopyWidth, int iCopyHeight, - double[] adblY) + byte[] abY) { - set(_adblY, iDestX + iDestY * _iLuminWidth, _iLuminHeight, - adblY, iSrcOfs, iSrcWidth, + set(_abY, iDestX + iDestY * _iLuminWidth, _iLuminHeight, + abY, iSrcOfs, iSrcWidth, iCopyWidth, iCopyHeight); } public void setCb(int iDestX, int iDestY, int iSrcOfs, int iSrcWidth, int iCopyWidth, int iCopyHeight, - double[] adblCb) + byte[] abCb) { - set(_adblCb, iDestX + iDestY * _iChromWidth, _iChromWidth, - adblCb, iSrcOfs, iSrcWidth, + set(_abCb, iDestX + iDestY * _iChromWidth, _iChromWidth, + abCb, iSrcOfs, iSrcWidth, iCopyWidth, iCopyHeight); } public void setCr(int iDestX, int iDestY, int iSrcOfs, int iSrcWidth, int iCopyWidth, int iCopyHeight, - double[] adblCr) + byte[] abCr) { - set(_adblCr, iDestX + iDestY * _iChromWidth, _iChromWidth, - adblCr, iSrcOfs, iSrcWidth, + set(_abCr, iDestX + iDestY * _iChromWidth, _iChromWidth, + abCr, iSrcOfs, iSrcWidth, iCopyWidth, iCopyHeight); } - private void set(double[] adblDest, int iDestOfs, int iDestWidth, - double[] adblSrc, int iSrcOfs, int iSrcWidth, + private void set(byte[] abDest, int iDestOfs, int iDestWidth, + byte[] abSrc, int iSrcOfs, int iSrcWidth, int iCopyWidth, int iCopyHeight) { for (int iLine = 0; iLine < iCopyHeight; iLine++, iDestOfs += iDestWidth, iSrcOfs += iSrcWidth) { - System.arraycopy(adblSrc, iSrcOfs, adblDest, iDestOfs, iCopyWidth); + System.arraycopy(abSrc, iSrcOfs, abDest, iDestOfs, iCopyWidth); } } diff --git a/src/jpsxdec/formats/Yuv4mpeg2Writer.java b/src/jpsxdec/formats/Yuv4mpeg2Writer.java index cf75a89..1b0afe9 100644 --- a/src/jpsxdec/formats/Yuv4mpeg2Writer.java +++ b/src/jpsxdec/formats/Yuv4mpeg2Writer.java @@ -82,7 +82,15 @@ public Yuv4mpeg2Writer(File file, int iWidth, int iHeight, _txtWriter.write(" Ip"); // none/progressive _txtWriter.write('\n'); } - + + public int getWidth() { + return _iWidth; + } + + public int getHeight() { + return _iHeight; + } + /** Write a yuv4mpeg2 image file. */ public void writeFrame(Yuv4mpeg2 yuv) throws IOException { @@ -94,37 +102,9 @@ public void writeFrame(Yuv4mpeg2 yuv) throws IOException { _txtWriter.flush(); // write the data - // "The values 0 and 255 are used for sync encoding." - // According to MrVacBob, analog encoding is dead, so it's not really important - int i; - for (double d : yuv._adblY) { - i = (int)(d + 0.5); - if (i < -128) - _outStream.write(0); - else if (i > 127) - _outStream.write(255); - else - _outStream.write(i + 128); - } - for (double d : yuv._adblCb) { - i = (int)(d + 0.5); - if (i < -128) - _outStream.write(0); - else if (i > 127) - _outStream.write(255); - else - _outStream.write(i + 128); - } - for (double d : yuv._adblCr) { - i = (int)(d + 0.5); - if (i < -128) - _outStream.write(0); - else if (i > 127) - _outStream.write(255); - else - _outStream.write(i + 128); - } - + _outStream.write(yuv._abY, 0, yuv._abY.length); + _outStream.write(yuv._abCb, 0, yuv._abCb.length); + _outStream.write(yuv._abCr, 0, yuv._abCr.length); } public void close() throws IOException { diff --git a/src/jpsxdec/maincmdlinehelp.txt b/src/jpsxdec/main_cmdline_help.dat similarity index 93% rename from src/jpsxdec/maincmdlinehelp.txt rename to src/jpsxdec/main_cmdline_help.dat index 5dfa7c8..b5608fb 100644 --- a/src/jpsxdec/maincmdlinehelp.txt +++ b/src/jpsxdec/main_cmdline_help.dat @@ -9,7 +9,7 @@ java -jar jpsxdec.jar -index/-idx/-i Create an index for in_file - -?/-h/-help # + -index/-idx/-i -?/-h/-help # Display help about the disc item # [-index/-idx/-i ] -decode/-d # [processing_options] diff --git a/src/jpsxdec/player/AudioChunkQueue.java b/src/jpsxdec/player/AudioChunkQueue.java new file mode 100644 index 0000000..44302de --- /dev/null +++ b/src/jpsxdec/player/AudioChunkQueue.java @@ -0,0 +1,39 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package jpsxdec.player; + + +public class AudioChunkQueue extends MultiStateBlockingQueue { + + private static final boolean DEBUG = true; + + VideoProcessor _vidProd; + + public AudioChunkQueue(int iCapacity, VideoProcessor vidProc) { + super(iCapacity); + _vidProd = vidProc; + } + + @Override + protected IDecodableAudioChunk dequeue() { + IDecodableAudioChunk chnk = super.dequeue(); + if (_vidProd != null && isEmpty()) { + if (DEBUG) System.out.println(Thread.currentThread().getName() + " audio queue empty, telling video queue to overwrite"); + _vidProd.overwriteWhenFull(); + } + return chnk; + } + + @Override + protected void enqueue(IDecodableAudioChunk o) { + boolean blnWasEmpty = isEmpty(); + super.enqueue(o); + if (_vidProd != null && blnWasEmpty) { + if (DEBUG) System.out.println(Thread.currentThread().getName() + " audio queue is no longer empty, telling video queue to block"); + _vidProd.play(); + } + } +} diff --git a/src/jpsxdec/player/AudioPositionTest.java b/src/jpsxdec/player/AudioPositionTest.java new file mode 100644 index 0000000..d0d0370 --- /dev/null +++ b/src/jpsxdec/player/AudioPositionTest.java @@ -0,0 +1,66 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package jpsxdec.player; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import javax.sound.sampled.*; + +public class AudioPositionTest { + + + public static void main(String[] args) throws LineUnavailableException, IOException { + final BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + + Mixer.Info[] aoMixerInfos = AudioSystem.getMixerInfo(); + System.out.println("Select output mixer:"); + for (int i = 0; i < aoMixerInfos.length; i++) { + System.out.println(" " + i + ". " + aoMixerInfos[i].toString()); + } + + Mixer.Info mixInfo = aoMixerInfos[Integer.parseInt(br.readLine())]; + Mixer mixer = AudioSystem.getMixer(mixInfo); + for (Line.Info info1 : mixer.getSourceLineInfo()) { + System.out.println(info1); + } + + System.out.println("Enter sample rate (e.g. 18900, 44100):"); + int iSampleRate = Integer.parseInt(br.readLine()); + + final AudioFormat audFmt = new AudioFormat(iSampleRate, 16, 1, true, true); + final DataLine.Info dataInfo = new DataLine.Info(SourceDataLine.class, audFmt); + + SourceDataLine player = (SourceDataLine) mixer.getLine(dataInfo); + System.out.println("Buffer size = " + player.getBufferSize()); + player.open(audFmt); + player.start(); + + byte[] abBuf = new byte[2 * 400]; + + long lngTestLength = 5 * 1000; // 20 seconds + long lngTestStart = System.currentTimeMillis(); + long lngTestEnd = lngTestStart + lngTestLength; + + System.out.println(System.getProperty("os.name") + "\tJava " + + System.getProperty("java.version")); + StringBuilder sb = new StringBuilder(400); + while (System.currentTimeMillis() < lngTestEnd) { + player.write(abBuf, 0, abBuf.length); + long lngTime = System.currentTimeMillis(); + long lngPos = player.getLongFramePosition(); + sb.append(lngTime - lngTestStart); + sb.append('\t'); + sb.append(lngPos); + System.out.println(sb); + sb.setLength(0); + Thread.yield(); + } + + player.stop(); + player.close(); + } +} diff --git a/src/jpsxdec/player/AudioProcessor.java b/src/jpsxdec/player/AudioProcessor.java index a18eaf2..7562cc8 100644 --- a/src/jpsxdec/player/AudioProcessor.java +++ b/src/jpsxdec/player/AudioProcessor.java @@ -43,18 +43,18 @@ public class AudioProcessor implements Runnable { - private static final int CAPACITY = 20; + private static final int CAPACITY = 50; - private MultiStateBlockingQueue _audioProcessingQueue - = new MultiStateBlockingQueue(CAPACITY); - private SourceDataLine _audPlayer; - private IAudioDecoder _decoder; + private final AudioChunkQueue _audioProcessingQueue; + private final SourceDataLine _audPlayer; private Thread _thread; - public AudioProcessor(IAudioDecoder decoder, SourceDataLine audioPlayer) { - _decoder = decoder; + AudioProcessor(SourceDataLine audioPlayer, VideoProcessor vidProc) { _audPlayer = audioPlayer; + _audioProcessingQueue = new AudioChunkQueue(CAPACITY, vidProc); + if (vidProc != null) + vidProc.overwriteWhenFull(); } public final void addDecodableAudioChunk(IDecodableAudioChunk audioChunk) { @@ -69,10 +69,8 @@ public final void run() { try { IDecodableAudioChunk audChunk; while ((audChunk = _audioProcessingQueue.take()) != null) { - _decoder.decodeAudio(audChunk, _audPlayer); // decode audio chunk and feed it to the player - if (!_audPlayer.isRunning()) - _audPlayer.start(); + audChunk.decodeAudio(_audPlayer); } } catch (IOException ex) { } catch (InterruptedException ex) { @@ -80,33 +78,33 @@ public final void run() { } } - public final void startup() { + final void startup() { _thread = new Thread(this, "Audio Processor"); _thread.start(); + _audPlayer.start(); // audio will start playing once it has data in the buffer } - public final void play() { + final void play() { _audioProcessingQueue.play(); } - public final void stop() { + final void stop() { _audioProcessingQueue.stop(); _audPlayer.flush(); _audPlayer.stop(); } - public final void pause() { + final void pause() { _audioProcessingQueue.pause(); } - public final void clearQueue() { + final void clearQueue() { _audioProcessingQueue.clear(); - _decoder.reset(); _audPlayer.flush(); _audPlayer.stop(); } - public void stopWhenEmpty() { + void stopWhenEmpty() { _audioProcessingQueue.stopWhenEmpty(); } diff --git a/src/jpsxdec/player/DemuxReader.java b/src/jpsxdec/player/DemuxReader.java index d8fe6d7..32240b3 100644 --- a/src/jpsxdec/player/DemuxReader.java +++ b/src/jpsxdec/player/DemuxReader.java @@ -40,7 +40,7 @@ import java.util.concurrent.atomic.AtomicInteger; -public class DemuxReader implements Runnable { +class DemuxReader implements Runnable { public static boolean DEBUG = false; @@ -87,13 +87,13 @@ public void run() { case STATE_PLAYING: // proces a sector // crap, how do you stop when waiting for more to read? - boolean blnContinue = true; + int iProgress = -1; try { - blnContinue = _reader.readNext(_videoProcessor, _audioProcessor); + iProgress = _reader.readNext(_videoProcessor, _audioProcessor); } catch (Throwable ex) { ex.printStackTrace(); } - if (!blnContinue) { + if (iProgress < 0) { System.out.println("Reader says it is the end. Telling everyone to stop when empty."); _oiState.set(STATE_STOPPED); _controller.endOfPlay(); diff --git a/src/jpsxdec/player/IAudioDecoder.java b/src/jpsxdec/player/IAudioDecoder.java deleted file mode 100644 index 93e3fc9..0000000 --- a/src/jpsxdec/player/IAudioDecoder.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * jPSXdec: PlayStation 1 Media Decoder/Converter in Java - * Copyright (C) 2007-2010 Michael Sabin - * All rights reserved. - * - * Redistribution and use of the jPSXdec code or any derivative works are - * permitted provided that the following conditions are met: - * - * * Redistributions may not be sold, nor may they be used in commercial - * or revenue-generating business activities. - * - * * Redistributions that are modified from the original source must - * include the complete source code, including the source code for all - * components used by a binary built from the modified sources. However, as - * a special exception, the source code distributed need not include - * anything that is normally distributed (in either source or binary form) - * with the major components (compiler, kernel, and so on) of the operating - * system on which the executable runs, unless that component itself - * accompanies the executable. - * - * * Redistributions must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS - * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package jpsxdec.player; - -import java.io.IOException; -import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.SourceDataLine; - - -public interface IAudioDecoder { - - /** Called before initialize. */ - AudioFormat getAudioFormat(); - - /** Provides the decoder with the output dataline in case it needs it. */ - void initialize(SourceDataLine dataLine); - - /** Lets the decoder process the audio chunk and writes audio the SourceDataLine. - * The SourceDataLine is the same one sent at initialize(), and remains - * the same during the play session. It is provided in case it is needed. */ - void decodeAudio(IDecodableAudioChunk audioChunk, SourceDataLine dataLine) - throws IOException; - - /** Resets the decoder (specifically the ADPCM state). */ - void reset(); - -} diff --git a/src/jpsxdec/player/IAudioVideoReader.java b/src/jpsxdec/player/IAudioVideoReader.java index a0d237b..7a9f3ef 100644 --- a/src/jpsxdec/player/IAudioVideoReader.java +++ b/src/jpsxdec/player/IAudioVideoReader.java @@ -37,10 +37,17 @@ package jpsxdec.player; +import javax.sound.sampled.AudioFormat; + public interface IAudioVideoReader { /** Return true if playing should continue. */ - boolean readNext(VideoProcessor vidProc, AudioProcessor audProc); + int readNext(VideoProcessor vidProc, AudioProcessor audProc); void seekToTime(long lngTime); void seekToFrame(int iFrame); + /** Return null if no audio. */ + AudioFormat getAudioFormat(); + boolean hasVideo(); + int getVideoWidth(); + int getVideoHeight(); void reset(); } diff --git a/src/jpsxdec/player/IDecodableAudioChunk.java b/src/jpsxdec/player/IDecodableAudioChunk.java index fe6dcf5..02dc432 100644 --- a/src/jpsxdec/player/IDecodableAudioChunk.java +++ b/src/jpsxdec/player/IDecodableAudioChunk.java @@ -37,5 +37,9 @@ package jpsxdec.player; +import java.io.IOException; +import javax.sound.sampled.SourceDataLine; + public interface IDecodableAudioChunk { + void decodeAudio(SourceDataLine dataLine) throws IOException; } diff --git a/src/jpsxdec/player/IDecodableFrame.java b/src/jpsxdec/player/IDecodableFrame.java index 79d0560..23c155d 100644 --- a/src/jpsxdec/player/IDecodableFrame.java +++ b/src/jpsxdec/player/IDecodableFrame.java @@ -37,11 +37,18 @@ package jpsxdec.player; +import jpsxdec.formats.RgbIntImage; + public interface IDecodableFrame { - int getWidth(); - int getHeight(); + + void decodeVideo(RgbIntImage drawHere); /** Returns the time the frame should be displayed, in milliseconds * from the beginning of the movie. */ long getPresentationTime(); + + void setContiguiousId(long lngId); + long getContigiousId(); + + void returnToPool(); } diff --git a/src/jpsxdec/player/IVideoDecoder.java b/src/jpsxdec/player/IVideoDecoder.java deleted file mode 100644 index 9025304..0000000 --- a/src/jpsxdec/player/IVideoDecoder.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * jPSXdec: PlayStation 1 Media Decoder/Converter in Java - * Copyright (C) 2007-2010 Michael Sabin - * All rights reserved. - * - * Redistribution and use of the jPSXdec code or any derivative works are - * permitted provided that the following conditions are met: - * - * * Redistributions may not be sold, nor may they be used in commercial - * or revenue-generating business activities. - * - * * Redistributions that are modified from the original source must - * include the complete source code, including the source code for all - * components used by a binary built from the modified sources. However, as - * a special exception, the source code distributed need not include - * anything that is normally distributed (in either source or binary form) - * with the major components (compiler, kernel, and so on) of the operating - * system on which the executable runs, unless that component itself - * accompanies the executable. - * - * * Redistributions must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS - * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package jpsxdec.player; - -import java.awt.image.BufferedImage; - -public interface IVideoDecoder { - BufferedImage decodeVideo(IDecodableFrame frame, BufferedImage usable); - - int getWidth(); - int getHeight(); - - void reset(); -} diff --git a/src/jpsxdec/player/MultiStateBlockingQueue.java b/src/jpsxdec/player/MultiStateBlockingQueue.java index 8abc210..874e957 100644 --- a/src/jpsxdec/player/MultiStateBlockingQueue.java +++ b/src/jpsxdec/player/MultiStateBlockingQueue.java @@ -37,46 +37,37 @@ package jpsxdec.player; +import java.util.Arrays; -/** Special threadsafe blocking queue that can be in one of several states: - * Playing but empty - Taking will block until an element is added - * Playing with one or more elements in queue - Taking returns immediately with an element - * Paused - Taking will block until playing & not empty - * Closed - Will not block at all. Ignores adds and returns null. - * Full - - * Stop when empty - - * - */ +/** Very powerful, thread-safe, blocking queue with the ability to specify exact + * behavior when taking and adding. */ public class MultiStateBlockingQueue { - public static boolean DEBUG = false; - - final private Object _eventSync = new Object(); + private static final boolean DEBUG = false; - private static final int RUNNING = 1; - private static final int STOPPED = 2; - private static final int PAUSED = 3; - private static final int STOPPING_WHEN_EMPTY = 4; - private static final int OVERWRITE_WHEN_FULL = 5; - private int _iState = PAUSED; - - private static final int ADD__IGNORE = 0; - private static final int ADD__BLOCK_WHEN_FULL = 1; - private static final int ADD__IGNORE_WHEN_FULL = 2; - private static final int ADD__OVERWRITE_WHEN_FULL = 3; - private int _iAddingResponse = ADD__BLOCK_WHEN_FULL; + private static enum ADD { + IGNORE, + BLOCK, + BLOCK_WHEN_FULL, + IGNORE_WHEN_FULL, + OVERWRITE_WHEN_FULL + } - private static final int TAKE__IGNORE = 10; - private static final int TAKE__BLOCK = 11; - private static final int TAKE__BLOCK_WHEN_EMPTY = 12; - private static final int TAKE__IGNORE_WHEN_EMPTY = 13; - private int _iTakingResponse = TAKE__BLOCK; + private static enum TAKE { + IGNORE, + BLOCK, + BLOCK_WHEN_EMPTY, + IGNORE_WHEN_EMPTY + } - private boolean _blnChangeToIgnoreWhenEmpty = false; + private final Object _eventSync = new Object(); + + private ADD _eAddingResponse = ADD.BLOCK_WHEN_FULL; + private TAKE _eTakingResponse = TAKE.BLOCK; - private T[] _aoQueue; - private int _iHeadPos; - private int _iTailPos; + protected T[] _aoQueue; + protected int _iHeadPos; + protected int _iTailPos; private int _iSize; public MultiStateBlockingQueue(int iCapacity) { @@ -87,51 +78,53 @@ public MultiStateBlockingQueue(int iCapacity) { ////////////////////////////////// public void play() { - synchronized (_eventSync) { - _iState = RUNNING; + setAddTakeResponse(ADD.BLOCK_WHEN_FULL, TAKE.BLOCK_WHEN_EMPTY); + } - _iAddingResponse = ADD__BLOCK_WHEN_FULL; - _iTakingResponse = TAKE__BLOCK_WHEN_EMPTY; + public void stop() { + setAddTakeResponse(ADD.IGNORE, TAKE.IGNORE); + } - _eventSync.notifyAll(); - } + public void stopWhenEmpty() { + setAddTakeResponse(ADD.BLOCK_WHEN_FULL, TAKE.IGNORE_WHEN_EMPTY); } - public void stop() { - synchronized (_eventSync) { - _iState = STOPPED; + public void overwriteWhenFull() { + setAddTakeResponse(ADD.OVERWRITE_WHEN_FULL, TAKE.BLOCK_WHEN_EMPTY); + } - _iAddingResponse = ADD__IGNORE; - _iTakingResponse = TAKE__IGNORE; + public void pause() { + setAddTakeResponse(ADD.BLOCK_WHEN_FULL, TAKE.BLOCK); + } + ///////////////////////////////// + + private void setAddResponse(ADD iResponse) { + synchronized (_eventSync) { + _eAddingResponse = iResponse; _eventSync.notifyAll(); } } - public void stopWhenEmpty() { + private void setTakeResponse(TAKE iResponse) { synchronized (_eventSync) { - _iState = STOPPING_WHEN_EMPTY; - - _blnChangeToIgnoreWhenEmpty = true; - + _eTakingResponse = iResponse; _eventSync.notifyAll(); } } - public void pause() { + private void setAddTakeResponse(ADD iAddResponse, TAKE iTakeResponse) { synchronized (_eventSync) { - _iState = PAUSED; - - _iAddingResponse = ADD__BLOCK_WHEN_FULL; - _iTakingResponse = TAKE__BLOCK; - + _eAddingResponse = iAddResponse; + _eTakingResponse = iTakeResponse; _eventSync.notifyAll(); } } ///////////////////////////////// - + /** Returns true if object was added, or false if it wasn't. + * This method may block. The object must not be null. */ public boolean add(T o) throws InterruptedException { if (o == null) throw new IllegalArgumentException(); @@ -140,21 +133,29 @@ public boolean add(T o) throws InterruptedException { synchronized (_eventSync) { while (true) { - if (_iState == STOPPED) { + if (_eAddingResponse == ADD.IGNORE) { if (DEBUG) System.out.println(Thread.currentThread().getName() + " stopped: returning false"); return false; - } else if (isFull() && _iState != OVERWRITE_WHEN_FULL) { - if (DEBUG) System.out.println(Thread.currentThread().getName() + " full: waiting"); + } else if (_eAddingResponse == ADD.BLOCK) { _eventSync.wait(); + } else if (isFull()) { + switch (_eAddingResponse) { + case IGNORE_WHEN_FULL: + return false; + case BLOCK_WHEN_FULL: + if (DEBUG) System.out.println(Thread.currentThread().getName() + " full: waiting"); + _eventSync.wait(); + break; + case OVERWRITE_WHEN_FULL: + enqueue(o); + return true; + default: + throw new IllegalStateException(); + } } else { if (DEBUG) System.out.println(Thread.currentThread().getName() + " adding " + o.toString()); - _aoQueue[_iTailPos] = o; - _iTailPos++; - if (_iTailPos >= _aoQueue.length) { - _iTailPos = 0; - } - if (_iState != OVERWRITE_WHEN_FULL) - _iSize++; + enqueue(o); + _iSize++; if (DEBUG) System.out.println(Thread.currentThread().getName() + " notifying other threads and returning"); _eventSync.notifyAll(); return true; @@ -163,56 +164,72 @@ public boolean add(T o) throws InterruptedException { } } + protected void enqueue(T o) { + _aoQueue[_iTailPos] = o; + _iTailPos++; + if (_iTailPos >= _aoQueue.length) { + _iTailPos = 0; + } + } + + /** Retrieves the head of the queue. May return null if no object is removed. + * This method may block. */ public T take() throws InterruptedException { if (DEBUG) System.out.println(Thread.currentThread().getName() + " enter take()"); + synchronized (_eventSync) { while (true) { - if (_iState == STOPPED) { + if (_eTakingResponse == TAKE.IGNORE) { if (DEBUG) System.out.println(Thread.currentThread().getName() + " stopped: returning null"); return null; - } else if (_iState == PAUSED) { - if (DEBUG) System.out.println(Thread.currentThread().getName() + " paused: waiting"); - _eventSync.wait(); + } else if (_eTakingResponse == TAKE.BLOCK) { + if (DEBUG) System.out.println(Thread.currentThread().getName() + " paused: waiting"); + _eventSync.wait(); } else if (isEmpty()) { - if (_iState == STOPPING_WHEN_EMPTY) { - _iState = STOPPED; - return null; - } else { - if (DEBUG) System.out.println(Thread.currentThread().getName() + " empty: waiting"); - _eventSync.wait(); + switch (_eTakingResponse) { + case IGNORE_WHEN_EMPTY: + return null; + case BLOCK_WHEN_EMPTY: + if (DEBUG) System.out.println(Thread.currentThread().getName() + " empty: waiting"); + _eventSync.wait(); + break; + default: + throw new IllegalStateException(); } } else { - T o = _aoQueue[_iHeadPos]; - if (DEBUG) System.out.println(Thread.currentThread().getName() + " removing object: " + o.toString()); - _aoQueue[_iHeadPos] = null; - _iHeadPos++; - if (_iHeadPos >= _aoQueue.length) - _iHeadPos = 0; - _iSize--; - _eventSync.notifyAll(); - return o; + return dequeue(); } } } } + protected T dequeue() { + T o = _aoQueue[_iHeadPos]; + if (DEBUG) System.out.println(Thread.currentThread().getName() + " removing object: " + o.toString()); + _aoQueue[_iHeadPos] = null; + _iHeadPos++; + if (_iHeadPos >= _aoQueue.length) + _iHeadPos = 0; + _iSize--; + _eventSync.notifyAll(); + return o; + } + public boolean isFull() { synchronized (_eventSync) { - return _iSize == _aoQueue.length; + return _iSize >= _aoQueue.length; } } public boolean isEmpty() { synchronized (_eventSync) { - return _iSize == 0; + return _iSize <= 0; } } public void clear() { synchronized (_eventSync) { - for (int i = 0; i < _aoQueue.length; i++) { - _aoQueue[i] = null; - } + Arrays.fill(_aoQueue, null); _iSize = _iHeadPos = _iTailPos = 0; _eventSync.notifyAll(); } diff --git a/src/jpsxdec/player/ObjectPool.java b/src/jpsxdec/player/ObjectPool.java new file mode 100644 index 0000000..b2c5a2b --- /dev/null +++ b/src/jpsxdec/player/ObjectPool.java @@ -0,0 +1,44 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package jpsxdec.player; + +import java.util.Collection; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public abstract class ObjectPool { + + private int _iBalance = 0; + + private final Queue objects; + + public ObjectPool() { + this.objects = new ConcurrentLinkedQueue(); + } + + public ObjectPool(Collection objects) { + this.objects = new ConcurrentLinkedQueue(objects); + } + + protected abstract T createExpensiveObject(); + + public T borrow() { + T t; + if ((t = objects.poll()) == null) { + t = createExpensiveObject(); + } + _iBalance++; + return t; + } + + public void giveBack(T object) { + this.objects.offer(object); // no point to wait for free space, just return + _iBalance--; + if (_iBalance == 0) { + System.err.println("Object pool balanced."); + } + } +} diff --git a/src/jpsxdec/player/PlayController.java b/src/jpsxdec/player/PlayController.java index a6fdcaf..ad558b6 100644 --- a/src/jpsxdec/player/PlayController.java +++ b/src/jpsxdec/player/PlayController.java @@ -63,33 +63,30 @@ public class PlayController { private final Object _oTimeSync = new Object(); - public PlayController(IAudioVideoReader reader, IAudioDecoder audDecoder, IVideoDecoder vidDecoder) + public PlayController(IAudioVideoReader reader) throws LineUnavailableException { - if (audDecoder == null && vidDecoder == null) + AudioFormat format = reader.getAudioFormat(); + if (format == null && !reader.hasVideo()) throw new IllegalArgumentException("No audio or video?"); - if (audDecoder == null) { + if (reader.hasVideo()) { + _vidPlayer = new VideoPlayer(this, reader.getVideoWidth(), reader.getVideoHeight()); + _vidProcessor = new VideoProcessor(this, _vidPlayer); + _vidProcessor.startup(); + _vidPlayer.startup(); + } + if (format == null) { _vidTimer = new VideoTimer(); } else { - AudioFormat format = audDecoder.getAudioFormat(); - DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); _audPlayer = (SourceDataLine) AudioSystem.getLine(info); _audPlayer.open(format); - audDecoder.initialize(_audPlayer); - - _audProcessor = new AudioProcessor(audDecoder, _audPlayer); + _audProcessor = new AudioProcessor(_audPlayer, _vidProcessor); _audProcessor.startup(); } - if (vidDecoder != null) { - _vidPlayer = new VideoPlayer(this, vidDecoder.getWidth(), vidDecoder.getHeight()); - _vidProcessor = new VideoProcessor(this, vidDecoder, _vidPlayer); - _vidProcessor.startup(); - _vidPlayer.startup(); - } _demuxReader = new DemuxReader(reader, _audProcessor, _vidProcessor, this); _demuxReader.startup(); @@ -108,11 +105,8 @@ public long getCurrentPlayTime() { synchronized (_oTimeSync) { long lngPos; if (_audPlayer != null) { + //lngPos = _audPlayer.getMicrosecondPosition(); lngPos = (long)(_audPlayer.getLongFramePosition() / _audPlayer.getFormat().getSampleRate() * 1000); - if (lngPos > 0) - lngPos += 0; - else if (lngPos < 0) - lngPos = 0; } else { lngPos = _vidTimer.getPlayTimeAndStart(); } diff --git a/src/jpsxdec/player/VideoFrame.java b/src/jpsxdec/player/VideoFrame.java deleted file mode 100644 index 0803420..0000000 --- a/src/jpsxdec/player/VideoFrame.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * jPSXdec: PlayStation 1 Media Decoder/Converter in Java - * Copyright (C) 2007-2010 Michael Sabin - * All rights reserved. - * - * Redistribution and use of the jPSXdec code or any derivative works are - * permitted provided that the following conditions are met: - * - * * Redistributions may not be sold, nor may they be used in commercial - * or revenue-generating business activities. - * - * * Redistributions that are modified from the original source must - * include the complete source code, including the source code for all - * components used by a binary built from the modified sources. However, as - * a special exception, the source code distributed need not include - * anything that is normally distributed (in either source or binary form) - * with the major components (compiler, kernel, and so on) of the operating - * system on which the executable runs, unless that component itself - * accompanies the executable. - * - * * Redistributions must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS - * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package jpsxdec.player; - -import java.awt.image.BufferedImage; - - -public class VideoFrame { - public final BufferedImage Frame; - public final long PresentationTime; - public final long ContigusPlayUniqueId; - - public VideoFrame(BufferedImage image, long lngPresentationTime, long lngContigusPlayUniqueId) { - Frame = image; - PresentationTime = lngPresentationTime; - ContigusPlayUniqueId = lngContigusPlayUniqueId; - } -} diff --git a/src/jpsxdec/player/VideoPlayer.java b/src/jpsxdec/player/VideoPlayer.java index 75c4aae..ee9be4e 100644 --- a/src/jpsxdec/player/VideoPlayer.java +++ b/src/jpsxdec/player/VideoPlayer.java @@ -41,15 +41,18 @@ import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; +import java.awt.Image; import java.awt.RenderingHints; import java.awt.image.BufferedImage; +import java.awt.image.MemoryImageSource; +import jpsxdec.formats.RgbIntImage; -public class VideoPlayer implements Runnable { +class VideoPlayer implements Runnable { public static boolean DEBUG = false; - private static final int CAPACITY = 20; + private static final int CAPACITY = 50; private MultiStateBlockingQueue _frameDisplayQueue = new MultiStateBlockingQueue(CAPACITY); @@ -62,10 +65,13 @@ public class VideoPlayer implements Runnable { /** Only used for video only streams. */ private Thread _thread; + private VideoScreen _screen; + public VideoPlayer(PlayController controller, int iWidth, int iHeight) { _controller = controller; _iWidth = iWidth; _iHeight = iHeight; + _screen = new VideoScreen(); } public void run() { @@ -78,13 +84,13 @@ public void run() { { // otherwise sleep until time to display long lngDiff; - while ((lngDiff = frame.PresentationTime - _controller.getCurrentPlayTime()) > 30) { + while ((lngDiff = frame.PresentationTime - _controller.getCurrentPlayTime()) > 15) { if (DEBUG) System.out.println("Player sleeping " + lngDiff); Thread.sleep(lngDiff); } if (DEBUG) System.out.println("===Displaying frame==="); // update canvas with frame - _screen.updateRepaint(frame.Frame); + _screen.updateRepaint(frame); } else { if (DEBUG) System.out.println("Different contigouous id"); } @@ -138,21 +144,29 @@ public void setZoom(int iZoom) { throw new IllegalArgumentException("Invalid zoom scale " + iZoom); } _iZoom = iZoom; + _screen.updateDims(); } - private VideoScreen _screen; public Canvas getVideoCanvas() { - if (_screen == null) - _screen = new VideoScreen(); return _screen; } + public int getWidth() { + return _iWidth; + } + public int getHeight() { + return _iHeight; + } + private class VideoScreen extends Canvas { /* ************************************************************ - * DEFINITELY WANT TO USE MemoryImageSource FOR THE IMAGE DATA + * Use MemoryImageSource? Or maybe it's not so helpful? + * MemoryImageSource ret = new MemoryImageSource(width, height, pixels, 0, width); + ret.setAnimated(true); + ret.setFullBufferUpdates(true); ************************************************************* */ - private BufferedImage __lastImg; + private VideoFrame __lastFrame; private Dimension __dims = updateDims(); @@ -160,17 +174,25 @@ public Dimension updateDims() { return __dims = new Dimension(_iWidth * _iZoom, _iHeight * _iZoom * 59 / 54); } - public void updateRepaint(BufferedImage bi) { - __lastImg = bi; + public void updateRepaint(VideoFrame frame) { + if (__lastFrame == null) { + __lastFrame = frame; + } else { + // XXX: race condition + VideoFrame lastFrame = __lastFrame; + __lastFrame = frame; + lastFrame.returnToPool(); + } repaint(); + Thread.yield(); } @Override public void update(Graphics g) { if (g instanceof Graphics2D) { - ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); } - g.drawImage(__lastImg, 0, 0, __dims.width, __dims.height, this); + g.drawImage(__lastFrame.Img, 0, 0, __dims.width, __dims.height, this); } @Override @@ -189,5 +211,45 @@ public Dimension getPreferredSize() { } } - + + + public final VideoFramePool _videoFramePool = new VideoFramePool(); + public class VideoFramePool extends ObjectPool { + + @Override + protected VideoFrame createExpensiveObject() { + return new VideoFrame(); + } + } + + + class VideoFrame { + public RgbIntImage Memory; + /** MemoryImageSource is tricky if you've never used it before. + * http://rsb.info.nih.gov/plasma/ */ + public MemoryImageSource MemImgSrc; + private Image Img; + + public long PresentationTime; + public long ContigusPlayUniqueId; + + public void init(IDecodableFrame decodeFrame) { + if (Memory == null) { + Memory = new RgbIntImage(_iWidth, _iHeight); + MemImgSrc = new MemoryImageSource(_iWidth, _iHeight, Memory.getData(), 0, _iWidth); + MemImgSrc.setAnimated(true); + MemImgSrc.setFullBufferUpdates(true); + Img = _screen.createImage(MemImgSrc); + Img.setAccelerationPriority(1.0f); + } + PresentationTime = decodeFrame.getPresentationTime(); + ContigusPlayUniqueId = decodeFrame.getContigiousId(); + } + + public void returnToPool() { + Img.flush(); + _videoFramePool.giveBack(this); + } + } + } diff --git a/src/jpsxdec/player/VideoProcessor.java b/src/jpsxdec/player/VideoProcessor.java index 16b4e26..e6e3cfc 100644 --- a/src/jpsxdec/player/VideoProcessor.java +++ b/src/jpsxdec/player/VideoProcessor.java @@ -37,54 +37,44 @@ package jpsxdec.player; -import java.awt.image.BufferedImage; - public class VideoProcessor implements Runnable { public static boolean DEBUG = false; - private static class DecodableFrameWrapper { - IDecodableFrame decodableFrame; - long contiguousPlayUniqueId; - - public DecodableFrameWrapper(IDecodableFrame decodableFrame, long contigusPlayUniqueId) { - this.decodableFrame = decodableFrame; - this.contiguousPlayUniqueId = contigusPlayUniqueId; - } - - } - - private static final int CAPACITY = 20; + private static final int CAPACITY = 50; - private MultiStateBlockingQueue _framesProcessingQueue = - new MultiStateBlockingQueue(CAPACITY); - private IVideoDecoder _vidDecoder; + private MultiStateBlockingQueue _framesProcessingQueue = + new MultiStateBlockingQueue(CAPACITY); private Thread _thread; private PlayController _controller; private VideoPlayer _vidPlayer; - public VideoProcessor(PlayController controller, IVideoDecoder decoder, VideoPlayer player) { + VideoProcessor(PlayController controller, VideoPlayer player) { _controller = controller; - _vidDecoder = decoder; _vidPlayer = player; } public void run() { - DecodableFrameWrapper wrapper; + IDecodableFrame decodeFrame; try { - while ((wrapper = _framesProcessingQueue.take()) != null) { + while ((decodeFrame = _framesProcessingQueue.take()) != null) { // check if this frame is part of current play sequence // and if we haven't passed presentation time - if (_controller.shouldBeProcessed(wrapper.decodableFrame.getPresentationTime(), - wrapper.contiguousPlayUniqueId)) + if (_controller.shouldBeProcessed(decodeFrame.getPresentationTime(), + decodeFrame.getContigiousId())) { if (DEBUG) System.out.println("Processor processing frame :)"); + VideoPlayer.VideoFrame frame = _vidPlayer._videoFramePool.borrow(); + frame.init(decodeFrame); // decode frame - BufferedImage bi = _vidDecoder.decodeVideo(wrapper.decodableFrame, null); + decodeFrame.decodeVideo(frame.Memory); + frame.MemImgSrc.newPixels(); // submit to vid player - _vidPlayer.addFrame(new VideoFrame(bi, wrapper.decodableFrame.getPresentationTime(), wrapper.contiguousPlayUniqueId)); + _vidPlayer.addFrame(frame); + + decodeFrame.returnToPool(); } else { System.out.println("Processor not processing frame :("); } @@ -96,39 +86,42 @@ public void run() { public void addFrame(IDecodableFrame frame) { try { - DecodableFrameWrapper wrapper = new DecodableFrameWrapper( - frame, - _controller.getContiguousPlayUniqueId()); - _framesProcessingQueue.add(wrapper); + frame.setContiguiousId(_controller.getContiguousPlayUniqueId()); + _framesProcessingQueue.add(frame); } catch (InterruptedException ex) { ex.printStackTrace(); } } - public void clearQueue() { + void overwriteWhenFull() { + _framesProcessingQueue.overwriteWhenFull(); + } + + void clearQueue() { _framesProcessingQueue.clear(); } - public void play() { + void play() { _framesProcessingQueue.play(); } - public void pause() { + void pause() { _framesProcessingQueue.pause(); } - public void stopWhenEmpty() { + void stopWhenEmpty() { _framesProcessingQueue.stopWhenEmpty(); } - public void startup() { + void startup() { _thread = new Thread(this, "Video Processor"); _framesProcessingQueue.play(); _thread.start(); } - public void shutdown() { + void shutdown() { _framesProcessingQueue.stop(); } + } diff --git a/src/jpsxdec/player/VideoTimer.java b/src/jpsxdec/player/VideoTimer.java index 7d8abb9..83e0095 100644 --- a/src/jpsxdec/player/VideoTimer.java +++ b/src/jpsxdec/player/VideoTimer.java @@ -38,7 +38,7 @@ package jpsxdec.player; -public class VideoTimer { +class VideoTimer { private long _lngStartTime; private boolean _blnStarted = false; diff --git a/src/jpsxdec/plugins/ConsoleProgressListener.java b/src/jpsxdec/plugins/ConsoleProgressListener.java index 1bf1308..ec55050 100644 --- a/src/jpsxdec/plugins/ConsoleProgressListener.java +++ b/src/jpsxdec/plugins/ConsoleProgressListener.java @@ -131,4 +131,12 @@ public void warning(String sDescription) { _fbs.printlnWarn(sDescription); } + public void more(String s) { + if (_blnNewLine) { + _fbs.nl(); + _blnNewLine = false; + } + _fbs.printlnMore(s); + } + } diff --git a/src/jpsxdec/plugins/DiscIndex.java b/src/jpsxdec/plugins/DiscIndex.java index 0219633..86518f6 100644 --- a/src/jpsxdec/plugins/DiscIndex.java +++ b/src/jpsxdec/plugins/DiscIndex.java @@ -38,39 +38,38 @@ package jpsxdec.plugins; import java.io.BufferedReader; +import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.Hashtable; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.logging.Level; import java.util.logging.Logger; -import javax.swing.AbstractListModel; import jpsxdec.Main; import jpsxdec.cdreaders.CDSector; import jpsxdec.cdreaders.CDSectorReader; +import jpsxdec.util.FeedbackStream; import jpsxdec.util.NotThisTypeException; -/** Searches for, and manages the collection of DiscItem items in a file. - * Playstation files (discs, STR, XA, etc.) can contain many media items. +/** Searches for, and manages the collection of DiscItems in a file. + * PlayStation files (discs, STR, XA, etc.) can contain multiple media items. * This will search for them, and hold the resulting list. It will also - * serialize to and deserialize from a stream. - * - * Note that this doesn't try to match parallel media items. See the - * ParallelMediaList for that. - */ -public class DiscIndex extends AbstractListModel implements Iterable { + * serialize/deserialize to/from a file. */ +public class DiscIndex implements Iterable { private static final Logger log = Logger.getLogger(DiscIndex.class.getName()); private static final String INDEX_HEADER = "[" + Main.VerString + "]"; + + private static final String COMMENT_LINE_START = ";"; + private static final String SOURCE_FILE_START = "Source File "; - private final Hashtable _mediaHash = new Hashtable(); - private final ArrayList _mediaList = new ArrayList(); - private final CDSectorReader _sourceCD; + private final LinkedHashMap _mediaHash = new LinkedHashMap(); + private CDSectorReader _sourceCD; private String _sDiscName = null; /** Count how many disc items report single speed playback. */ @@ -78,37 +77,19 @@ public class DiscIndex extends AbstractListModel implements Iterable { /** Count how many disc items report double speed playback. */ private int _iDoubleSpeedItemCount = 0; - public DiscIndex(CDSectorReader cdReader) { + /** Finds all the media on the CD. */ + public DiscIndex(CDSectorReader cdReader, ProgressListener pl) { _sourceCD = cdReader; - } - - /* - * This appears to focus on just reading static data, but behind the scenes - * it is also reading streams. - * - * After all the stream vectors have been identified - * Then we need to find - * -audio stream vectors that don't overlap video stream vectors - * -video stream vectors that don't overlap audio stream vectors - * -and overlapping audio/video stream vectors - * -compare the ISO9660 file list to the sector ranges of the media - * - */ - - /** Finds all the media on the CD. - * @param oListener Optional progress listener. */ - public void indexDisc(ProgressListener pl) { // ready to start indexing, so clear the existing lists _mediaHash.clear(); - _mediaList.clear(); JPSXPlugin[] aoPlugins = JPSXPlugin.getPlugins(); - ArrayList oCompletedItems = new ArrayList(); + ArrayList completedItems = new ArrayList(); - for (JPSXPlugin oIndexer : aoPlugins) { - oIndexer.putYourCompletedMediaItemsHere(oCompletedItems); + for (JPSXPlugin plugin : aoPlugins) { + plugin.putYourCompletedMediaItemsHere(completedItems); } pl.progressStart(); @@ -131,7 +112,7 @@ public void indexDisc(ProgressListener pl) { log.finer(cdSector.toString()); } - pl.event("Sector " + iSector + " / " + _sourceCD.size()); + pl.event(String.format("Sector %d / %d %d items found", iSector, _sourceCD.size(), completedItems.size())); pl.progressUpdate(iSector / (double)_sourceCD.size()); } catch (IOException ex) { @@ -149,7 +130,7 @@ public void indexDisc(ProgressListener pl) { } // sort the list according to the start sector - Collections.sort(oCompletedItems, new Comparator() { + Collections.sort(completedItems, new Comparator() { public int compare(DiscItem o1, DiscItem o2) { if (o1.getStartSector() < o2.getStartSector()) return -1; @@ -165,7 +146,7 @@ else if (o1.getEndSector() < o2.getEndSector()) }); // copy the items to this class - for (DiscItem item : oCompletedItems) { + for (DiscItem item : completedItems) { addMediaItemInc(item); } // notify the plugins that the list has been generated @@ -178,24 +159,27 @@ else if (o1.getEndSector() < o2.getEndSector()) } } + + /** Deserializes the CD index file, and tries to open the CD listed in the index. */ + public DiscIndex(String sSerialFile, FeedbackStream fbs) + throws IOException, NotThisTypeException + { + this(sSerialFile, null, fbs); + } - /** Deserializes the CD index file, and creates a - * list of media items on the CD */ - public void deserializeIndex(String sSerialFile) + /** Deserializes the CD index file, and creates a list of media items on the CD */ + public DiscIndex(String sIndexFile, CDSectorReader cdReader, FeedbackStream fbs) throws IOException, NotThisTypeException { - //TODO: Want to read as much as we can from the file before an exception - // and return what we can get, but we also want to return that - // there was an error. + _sourceCD = cdReader; - // TODO: Check that the disc matches what the serial file says + File indexFile = new File(sIndexFile); - BufferedReader reader = new BufferedReader(new FileReader(sSerialFile)); + BufferedReader reader = new BufferedReader(new FileReader(indexFile)); String sLine; // ready to deserialize, so clear the existing lists _mediaHash.clear(); - _mediaList.clear(); ArrayList itemList = new ArrayList(); @@ -214,14 +198,21 @@ public void deserializeIndex(String sSerialFile) while ((sLine = reader.readLine()) != null) { // comments - if (sLine.startsWith(";")) + if (sLine.startsWith(COMMENT_LINE_START)) continue; // source file - if (sLine.startsWith("SourceFile|")) { - String[] asParts = sLine.split("|"); - if (asParts.length != 5) continue; - //TODO: Check if cd length matches serialized length + if (sLine.startsWith(SOURCE_FILE_START)) { + String sCdSerial = sLine.substring(SOURCE_FILE_START.length()); + if (cdReader != null) { + if (!sCdSerial.equals(cdReader.toString())) { + // TODO: finish adding proper disc serialization to index file + //fbs.printlnWarn("Warning: Disc format does not match what index says."); + } + } else { + throw new RuntimeException("Case not finished yet."); + //_sourceCD = CDSectorReader.deserialize(sCdSerial, indexFile); + } continue; } @@ -232,7 +223,7 @@ public void deserializeIndex(String sSerialFile) plugin.deserialize_lineRead(oDeserializedLine); } } catch (NotThisTypeException e) { - System.err.println("Failed to parse line: " + sLine); + if (fbs != null) fbs.printlnWarn("Failed to parse line: " + sLine); } } reader.close(); @@ -253,12 +244,27 @@ public void deserializeIndex(String sSerialFile) } + /** Serializes the list of media items to a file. */ + public void serializeIndex(PrintStream ps) + throws IOException + { + // TODO: Serialize the CD file location relative to where this index file is being saved + ps.println(INDEX_HEADER); + ps.println(COMMENT_LINE_START + " Lines that begin with "+COMMENT_LINE_START+" are ignored"); + ps.println(SOURCE_FILE_START + _sourceCD.serialize()); + for (DiscItem item : this) { + ps.println(item.serialize().serialize()); + } + ps.close(); + } + public void setDiscName(String sName) { _sDiscName = sName; } /** Based on how many disc items report single or double speed playback, - * returns the best guess of disc speed for unknown disc items. */ + * returns the best guess of disc speed (used for streaming disc items + * with unknown disc speed). */ public int getDefaultDiscSpeed() { if (_iSingleSpeedItemCount > _iDoubleSpeedItemCount) return 1; @@ -268,43 +274,24 @@ public int getDefaultDiscSpeed() { /** Adds a media item to the internal hash and array. */ private void addMediaItemInc(DiscItem item) { - int iIndex = _mediaList.size(); + int iIndex = _mediaHash.size(); item.setIndex(iIndex); item.setSourceCD(_sourceCD); - _mediaHash.put(new Integer(iIndex), item); - _mediaList.add(item); + _mediaHash.put(Integer.valueOf(iIndex), item); } /** Adds a media item to the internal hash and array. */ private void addMediaItem(DiscItem item) { item.setSourceCD(_sourceCD); - _mediaHash.put(new Integer(item.getIndex()), item); - _mediaList.add(item); - } - - /** Serializes the list of media items to a file. */ - public void serializeIndex(PrintStream ps) - throws IOException - { - ps.println(INDEX_HEADER); - ps.println("; Lines that begin with ; are ignored"); - ps.println(_sourceCD.serialize()); - for (DiscItem oMedia : this) { - ps.println(oMedia.serialize().serialize()); - } - ps.close(); + _mediaHash.put(Integer.valueOf(item.getIndex()), item); } public DiscItem getByIndex(int iIndex) { - return _mediaHash.get(new Integer(iIndex)); + return _mediaHash.get(Integer.valueOf(iIndex)); } - public DiscItem getByString(String sId) { - return _mediaHash.get(sId); - } - public boolean hasIndex(int iIndex) { - return _mediaHash.containsKey(new Integer(iIndex)); + return _mediaHash.containsKey(Integer.valueOf(iIndex)); } public CDSectorReader getSourceCD() { @@ -312,27 +299,17 @@ public CDSectorReader getSourceCD() { } public int size() { - return _mediaList.size(); + return _mediaHash.size(); } /* [implements Iterable] */ public Iterator iterator() { - return _mediaList.iterator(); - } - - // AbstractListModel stuff ///////////////////////////////////////////////// - public int getSize() { - return _mediaList.size(); - } - - public DiscItem getElementAt(int iIndex) { - return _mediaList.get(iIndex); + return _mediaHash.values().iterator(); } @Override public String toString() { - return String.format("%s (%s) %d items", _sourceCD.getSourceFile(), _sDiscName, _mediaList.size()); + return String.format("%s (%s) %d items", _sourceCD.getSourceFile(), _sDiscName, _mediaHash.size()); } - } diff --git a/src/jpsxdec/plugins/DiscItemSerialization.java b/src/jpsxdec/plugins/DiscItemSerialization.java index 167e13e..3b573a7 100644 --- a/src/jpsxdec/plugins/DiscItemSerialization.java +++ b/src/jpsxdec/plugins/DiscItemSerialization.java @@ -53,12 +53,13 @@ */ public class DiscItemSerialization { - private LinkedHashMap _fields = new LinkedHashMap(); private static final String KEY_VALUE_DELIMITER = ":"; private static final String FIELD_DELIMITER = "|"; private static final String INDEX_KEY = "#"; private static final String SECTORRANGE_KEY = "Sectors"; private static final String TYPE_KEY = "Type"; + + private LinkedHashMap _fields = new LinkedHashMap(); private String _sSerizedString = null; /** Creates a new serialization class to accept information. */ diff --git a/src/jpsxdec/plugins/DiscItemStreaming.java b/src/jpsxdec/plugins/DiscItemStreaming.java index 9ea2b09..4ac1e2f 100644 --- a/src/jpsxdec/plugins/DiscItemStreaming.java +++ b/src/jpsxdec/plugins/DiscItemStreaming.java @@ -67,13 +67,6 @@ protected DiscItemSerialization superSerial(String sType) { public void setDiscSpeed(int iSpeed) {} - /** Get the play time in milliseconds based on the sector number - * from the start of the media. */ - public final long calclateTime(IdentifiedSector sector) { - return calclateTime(sector.getSectorNumber()); - } - /** Get the play time in milliseconds based on the sector number - * from the start of the media. */ - abstract public long calclateTime(int iSect); + abstract public int getPresentationStartSector(); } diff --git a/src/jpsxdec/plugins/IdentifiedSector.java b/src/jpsxdec/plugins/IdentifiedSector.java index 1513bf3..ce59aed 100644 --- a/src/jpsxdec/plugins/IdentifiedSector.java +++ b/src/jpsxdec/plugins/IdentifiedSector.java @@ -43,45 +43,49 @@ * special meaning. */ public abstract class IdentifiedSector implements IIdentifiedSector { - private CDSector m_oSourceCDSector; + private CDSector _sourceCdSector; public IdentifiedSector(CDSector cdSector) { - m_oSourceCDSector = cdSector; + _sourceCdSector = cdSector; } /** Returns a string description of the sector type. */ public String toString() { - return m_oSourceCDSector.toString(); + return _sourceCdSector.toString(); } /** The 'file' value in the raw CD header, or -1 if there was no header. */ public long getFile() { - return m_oSourceCDSector.getFile(); + return _sourceCdSector.getFile(); } /** @return The 'channel' value in the raw CDXA header, * or -1 if there was no header, or if it is a 'NULL' sector * (overridden by PSXSectorNull).*/ public int getChannel() { - return m_oSourceCDSector.getChannel(); + return _sourceCdSector.getChannel(); } public boolean getEOFBit() { - if (m_oSourceCDSector.hasSectorHeader()) - return m_oSourceCDSector.getSubMode().getEofMarker(); + if (_sourceCdSector.hasSectorHeader()) + return _sourceCdSector.getSubMode().getEofMarker(); else return false; } /** @return The sector number from the start of the source file. */ public int getSectorNumber() { - return m_oSourceCDSector.getSectorNumberFromStart(); + return _sourceCdSector.getSectorNumberFromStart(); } public CDSector getCDSector() { - return m_oSourceCDSector; + return _sourceCdSector; + } + + protected String cdToString() { + return _sourceCdSector.toString(); } } diff --git a/src/jpsxdec/plugins/JPSXPlugin.java b/src/jpsxdec/plugins/JPSXPlugin.java index 9bc81e8..eb8116d 100644 --- a/src/jpsxdec/plugins/JPSXPlugin.java +++ b/src/jpsxdec/plugins/JPSXPlugin.java @@ -50,7 +50,6 @@ import jpsxdec.cdreaders.CDSectorReader; import jpsxdec.plugins.psx.alice.JPSXPluginAlice; import jpsxdec.plugins.psx.lain.JPSXPluginLain; -import jpsxdec.plugins.psx.video.DemuxImage; import jpsxdec.plugins.psx.tim.JPSXPluginTIM; import jpsxdec.plugins.psx.video.decode.DemuxFrameUncompressor; @@ -83,10 +82,12 @@ public static IdentifiedSector identifyPluginSector(CDSector cdSector) { return null; } - public static DemuxFrameUncompressor identifyUncompressor(DemuxImage demux) { + public static DemuxFrameUncompressor identifyUncompressor(byte[] abDemuxBuf, int iStart, int iFrame) { for (JPSXPlugin plugin : aoPlugins) { + if (iStart != 0) + throw new RuntimeException("oops"); DemuxFrameUncompressor oUncompressor = - plugin.identifyVideoFrame(demux.getData(), demux.getFrameNumber()); + plugin.identifyVideoFrame(abDemuxBuf, iFrame); if (oUncompressor != null) return oUncompressor; } diff --git a/src/jpsxdec/plugins/ProgressListener.java b/src/jpsxdec/plugins/ProgressListener.java index 6d95131..c96f729 100644 --- a/src/jpsxdec/plugins/ProgressListener.java +++ b/src/jpsxdec/plugins/ProgressListener.java @@ -54,6 +54,7 @@ public static ProgressListener ignore() { @Override public void warning(Throwable ex) {} @Override public void warning(String sDescription) {} @Override public void progressStart(String s) {} + @Override public void more(String s) {} }; } return IGNORE; @@ -69,11 +70,19 @@ public void progressUpdate(double dblPercentComplete) {} public void event(String sDescription) {} + public void warning(String sMessage, Throwable cause) { + warning(sMessage + " " + cause.getMessage()); + } public void warning(Throwable ex) { warning(ex.getMessage()); } public void warning(String sDescription) {} + public void error(String sMessage, Throwable ex) { + error(sMessage + " " + ex.getMessage()); + } public void error(Throwable ex) { error(ex.getMessage()); } public void error(String sDescription) {} public void info(String s) {} + + abstract public void more(String s); } diff --git a/src/jpsxdec/plugins/iso9660/SectorISO9660DirectoryRecords.java b/src/jpsxdec/plugins/iso9660/SectorISO9660DirectoryRecords.java index 405b3dd..bf140df 100644 --- a/src/jpsxdec/plugins/iso9660/SectorISO9660DirectoryRecords.java +++ b/src/jpsxdec/plugins/iso9660/SectorISO9660DirectoryRecords.java @@ -96,6 +96,6 @@ public ArrayList getRecords() { @Override public String toString() { return String.format("ISO DirRec %s %s", - super.toString(), _dirRecords.toString()); + super.cdToString(), _dirRecords.toString()); } } \ No newline at end of file diff --git a/src/jpsxdec/plugins/iso9660/SectorISO9660PathTable.java b/src/jpsxdec/plugins/iso9660/SectorISO9660PathTable.java index cca21ea..9d8c58a 100644 --- a/src/jpsxdec/plugins/iso9660/SectorISO9660PathTable.java +++ b/src/jpsxdec/plugins/iso9660/SectorISO9660PathTable.java @@ -69,6 +69,6 @@ public String getTypeName() { @Override public String toString() { return String.format("ISO PathTable %s %s", - super.toString(), _pathTable.toString()); + super.cdToString(), _pathTable.toString()); } } diff --git a/src/jpsxdec/plugins/iso9660/SectorISO9660VolumePrimaryDescriptor.java b/src/jpsxdec/plugins/iso9660/SectorISO9660VolumePrimaryDescriptor.java index 579d197..a45e12e 100644 --- a/src/jpsxdec/plugins/iso9660/SectorISO9660VolumePrimaryDescriptor.java +++ b/src/jpsxdec/plugins/iso9660/SectorISO9660VolumePrimaryDescriptor.java @@ -78,7 +78,7 @@ public VolumePrimaryDescriptor getVPD() { @Override public String toString() { return String.format("ISO PriDesc %s %s", - super.toString(), _primaryDescriptor.toString()); + super.cdToString(), _primaryDescriptor.toString()); } } diff --git a/src/jpsxdec/plugins/psx/alice/SectorAliceFrameChunk.java b/src/jpsxdec/plugins/psx/alice/SectorAliceFrameChunk.java index 5cd025e..25d463f 100644 --- a/src/jpsxdec/plugins/psx/alice/SectorAliceFrameChunk.java +++ b/src/jpsxdec/plugins/psx/alice/SectorAliceFrameChunk.java @@ -80,7 +80,7 @@ public String getTypeName() { } public boolean matchesPrevious(IVideoSector prevSector) { - if (!(prevSector instanceof SectorAliceFrameChunk)) + if (!(prevSector.getClass().equals(prevSector.getClass()))) return false; SectorAliceFrameChunk oAliceFrame = (SectorAliceFrameChunk) prevSector; @@ -98,21 +98,21 @@ public boolean matchesPrevious(IVideoSector prevSector) { lngNextFrameNum == getFrameNumber()); } + public DiscItem createMedia(int iStartSector, int iStartFrame, int iFrame1LastSector) + { + int iSectors = getSectorNumber() - iStartSector + 1; + int iFrames = getFrameNumber() - iStartFrame + 1; + return createMedia(iStartSector, iStartFrame, iFrame1LastSector, iSectors, iFrames); + } public DiscItem createMedia(int iStartSector, int iStartFrame, - int iFrame1End, + int iFrame1LastSector, int iSectors, int iPerFrame) { return new DiscItemSTRVideo(iStartSector, getSectorNumber(), iStartFrame, getFrameNumber(), getWidth(), getHeight(), iSectors, iPerFrame, - iFrame1End); - } - public DiscItem createMedia(int iStartSector, int iStartFrame, int iFrame1End) - { - int iSectors = getSectorNumber() - iStartSector; - int iFrames = getFrameNumber() - iStartFrame; - return createMedia(iStartSector, iStartFrame, iFrame1End, iSectors, iFrames); + iFrame1LastSector); } } diff --git a/src/jpsxdec/plugins/psx/alice/SectorAliceFrameChunkNull.java b/src/jpsxdec/plugins/psx/alice/SectorAliceFrameChunkNull.java index 8e18100..e7af79a 100644 --- a/src/jpsxdec/plugins/psx/alice/SectorAliceFrameChunkNull.java +++ b/src/jpsxdec/plugins/psx/alice/SectorAliceFrameChunkNull.java @@ -59,12 +59,12 @@ public class SectorAliceFrameChunkNull extends IdentifiedSector // .. Instance fields .................................................. - // Magic // 0 [4 bytes] - private int _iChunkNumber; // 4 [2 bytes] - private int _iChunksInThisFrame; // 6 [2 bytes] - protected int _iFrameNumber; // 8 [4 bytes] - private long _lngUsedDemuxSize; // 12 [4 bytes] - // 16 bytes -- all zeros // 16 [16 bytes] + // Magic // 0 [4 bytes] + private final int _iChunkNumber; // 4 [2 bytes] + private final int _iChunksInThisFrame; // 6 [2 bytes] + protected final int _iFrameNumber; // 8 [4 bytes] + private final long _lngUsedDemuxSize; // 12 [4 bytes] + // 16 bytes -- all zeros // 16 [16 bytes] // 32 TOTAL // .. Constructor ..................................................... @@ -82,37 +82,33 @@ public SectorAliceFrameChunkNull(CDSector cdSector) throws NotThisTypeException } } try { - readHeader(cdSector.getCDUserDataStream()); - } catch (IOException ex) { - throw new NotThisTypeException(); - } - } + ByteArrayFPIS bais = cdSector.getCDUserDataStream(); + if (IO.readUInt32LE(bais) != VIDEO_CHUNK_MAGIC) + throw new NotThisTypeException(); - protected void readHeader(ByteArrayInputStream bais) - throws NotThisTypeException, IOException - { - if (IO.readUInt32LE(bais) != VIDEO_CHUNK_MAGIC) - throw new NotThisTypeException(); + _iChunkNumber = IO.readSInt16LE(bais); + if (_iChunkNumber < 0) + throw new NotThisTypeException(); + _iChunksInThisFrame = IO.readSInt16LE(bais); + if (_iChunksInThisFrame < 3) + throw new NotThisTypeException(); + _iFrameNumber = IO.readSInt32LE(bais); - _iChunkNumber = IO.readSInt16LE(bais); - if (_iChunkNumber < 0) - throw new NotThisTypeException(); - _iChunksInThisFrame = IO.readSInt16LE(bais); - if (_iChunksInThisFrame < 3) - throw new NotThisTypeException(); - _iFrameNumber = IO.readSInt32LE(bais); + // null frames between movies have a frame number of 0xFFFF + // the high bit signifies the end of a video + if (_iFrameNumber < 0) + throw new NotThisTypeException(); - // null frames between movies have a frame number of 0xFFFF - // the high bit signifies the end of a video - if (_iFrameNumber < 0) + _lngUsedDemuxSize = IO.readUInt32LE(bais); + + // make sure all 16 bytes are zero + for (int i = 0; i < 16; i++) + if (bais.read() != 0) + throw new NotThisTypeException(); + + } catch (IOException ex) { throw new NotThisTypeException(); - - _lngUsedDemuxSize = IO.readUInt32LE(bais); - - // make sure all 16 bytes are zero - for (int i = 0; i < 16; i++) - if (bais.read() != 0) - throw new NotThisTypeException(); + } } // .. Public functions ................................................. diff --git a/src/jpsxdec/plugins/psx/lain/JPSXPluginLain.java b/src/jpsxdec/plugins/psx/lain/JPSXPluginLain.java index 34042c0..27e4afd 100644 --- a/src/jpsxdec/plugins/psx/lain/JPSXPluginLain.java +++ b/src/jpsxdec/plugins/psx/lain/JPSXPluginLain.java @@ -45,6 +45,7 @@ import jpsxdec.plugins.JPSXPlugin; import jpsxdec.plugins.psx.video.decode.DemuxFrameUncompressor; import jpsxdec.plugins.psx.video.decode.DemuxFrameUncompressor_Lain; +import jpsxdec.util.NotThisTypeException; public class JPSXPluginLain extends JPSXPlugin { @@ -79,6 +80,8 @@ public void deserialize_lineRead(DiscItemSerialization serial) { @Override public IdentifiedSector identifySector(CDSector cdSector) { + try { return new SectorLainVideo(cdSector); } + catch (NotThisTypeException e) {} return null; } diff --git a/src/jpsxdec/plugins/psx/lain/Lain_LAPKS.java b/src/jpsxdec/plugins/psx/lain/Lain_LAPKS.java index b1e76e5..af25b76 100644 --- a/src/jpsxdec/plugins/psx/lain/Lain_LAPKS.java +++ b/src/jpsxdec/plugins/psx/lain/Lain_LAPKS.java @@ -85,7 +85,7 @@ public static int decodeLAPKS(String sInLAPKS_BIN, String sOutFileBase) { while ((oCell = lnpk.nextCell()) != null ){ MdecDecoder_double oDecoder = new MdecDecoder_double( new StephensIDCT(), oCell.Width, oCell.Height); - uncompresor.reset(oCell.Data); + uncompresor.reset(oCell.Data, 0); oDecoder.decode(uncompresor); RgbIntImage oRgb = new RgbIntImage(oCell.Width, oCell.Height); oDecoder.readDecodedRGB(oRgb); diff --git a/src/jpsxdec/plugins/psx/lain/SectorLainVideo.java b/src/jpsxdec/plugins/psx/lain/SectorLainVideo.java new file mode 100644 index 0000000..681f032 --- /dev/null +++ b/src/jpsxdec/plugins/psx/lain/SectorLainVideo.java @@ -0,0 +1,105 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package jpsxdec.plugins.psx.lain; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import jpsxdec.cdreaders.CDSector; +import jpsxdec.plugins.JPSXPlugin; +import jpsxdec.plugins.psx.str.SectorSTR; +import jpsxdec.util.IO; +import jpsxdec.util.NotThisTypeException; + + +public class SectorLainVideo extends SectorSTR { + + private byte _bQuantizationScaleLumin; + private byte _bQuantizationScaleChrom; + + public SectorLainVideo(CDSector cdSector) throws NotThisTypeException { + super(cdSector); // will call overridden readHeader() method + } + + @Override + protected void readHeader(ByteArrayInputStream inStream) throws NotThisTypeException, IOException { + _lngMagic = IO.readUInt32LE(inStream); + if (_lngMagic != VIDEO_CHUNK_MAGIC) + throw new NotThisTypeException(); + + _iChunkNumber = IO.readSInt16LE(inStream); + if (_iChunkNumber < 0) + throw new NotThisTypeException(); + _iChunksInThisFrame = IO.readSInt16LE(inStream); + if (_iChunksInThisFrame < 1) + throw new NotThisTypeException(); + _iFrameNumber = IO.readSInt32LE(inStream); + if (_iFrameNumber < 1) + throw new NotThisTypeException(); + + _lngUsedDemuxedSize = IO.readSInt32LE(inStream); + if (_lngUsedDemuxedSize < 0) + throw new NotThisTypeException(); + + _iWidth = IO.readSInt16LE(inStream); + if (_iWidth != 320) + throw new NotThisTypeException(); + _iHeight = IO.readSInt16LE(inStream); + if (_iHeight != 240) + throw new NotThisTypeException(); + + _bQuantizationScaleLumin = IO.readSInt8(inStream); + if (_bQuantizationScaleLumin < 0) + throw new NotThisTypeException(); + _bQuantizationScaleChrom = IO.readSInt8(inStream); + if (_bQuantizationScaleChrom < 0) + throw new NotThisTypeException(); + + _lngMagic3800 = IO.readUInt16LE(inStream); + if (_lngMagic3800 != 0x3800 && _lngMagic3800 != 0x0000 && _lngMagic3800 != _iFrameNumber) + throw new NotThisTypeException(); + + _lngRunLengthCodeCount = IO.readUInt16LE(inStream); + + _lngVersion = IO.readUInt16LE(inStream); + if (_lngVersion != 0) + throw new NotThisTypeException(); + + _lngFourZeros = IO.readUInt32LE(inStream); + if (_lngFourZeros != 0) + throw new NotThisTypeException(); + } + + public String toString() { + return String.format("%s %s frame:%d chunk:%d/%d %dx%d ver:%d " + + "{demux frame size=%d rlc=%d 3800=%04x qscaleL=%d qscaleC=%d 4*00=%08x}", + getTypeName(), + super.cdToString(), + _iFrameNumber, + _iChunkNumber, + _iChunksInThisFrame, + _iWidth, + _iHeight, + _lngVersion, + _lngUsedDemuxedSize, + _lngRunLengthCodeCount, + _lngMagic3800, + _bQuantizationScaleLumin, + _bQuantizationScaleChrom, + _lngFourZeros + ); + } + + @Override + public JPSXPlugin getSourcePlugin() { + return JPSXPluginLain.getPlugin(); + } + + @Override + public String getTypeName() { + return "Lain Video"; + } + +} diff --git a/src/jpsxdec/plugins/psx/square/DiscItemSquareAudioStream.java b/src/jpsxdec/plugins/psx/square/DiscItemSquareAudioStream.java index d87bd91..4cb6392 100644 --- a/src/jpsxdec/plugins/psx/square/DiscItemSquareAudioStream.java +++ b/src/jpsxdec/plugins/psx/square/DiscItemSquareAudioStream.java @@ -41,20 +41,19 @@ import javax.sound.sampled.AudioFormat; import javax.swing.JPanel; import jpsxdec.cdreaders.CDSector; -import jpsxdec.plugins.DiscItemStreaming; import jpsxdec.plugins.DiscItemSerialization; import jpsxdec.plugins.DiscItemSaver; import jpsxdec.plugins.IdentifiedSector; import jpsxdec.plugins.ProgressListener; -import jpsxdec.plugins.xa.IDiscItemAudioStream; -import jpsxdec.plugins.xa.IDiscItemAudioSectorDecoder; -import jpsxdec.plugins.xa.PCM16bitAudioWriter; -import jpsxdec.plugins.xa.PCM16bitAudioWriterBuilder; -import jpsxdec.util.AudioOutputStream; +import jpsxdec.plugins.xa.DiscItemAudioStream; +import jpsxdec.plugins.xa.IAudioReceiver; +import jpsxdec.plugins.xa.IAudioSectorDecoder; +import jpsxdec.plugins.xa.SectorAudioWriter; +import jpsxdec.plugins.xa.SectorAudioWriterBuilder; import jpsxdec.util.FeedbackStream; import jpsxdec.util.NotThisTypeException; -public class DiscItemSquareAudioStream extends DiscItemStreaming implements IDiscItemAudioStream { +public class DiscItemSquareAudioStream extends DiscItemAudioStream { public static final String TYPE_ID = "SquareAudio"; @@ -125,25 +124,35 @@ public int getDiscSpeed() { return 2; } + @Override + public int getPresentationStartSector() { + return getStartSector() + 1; + } + public AudioFormat getAudioFormat(boolean blnBigEndian) { return new AudioFormat(_iSamplesPerSecond, 16, 2, true, blnBigEndian); } - public IDiscItemAudioSectorDecoder makeDecoder(AudioOutputStream outStream, boolean blnBigEndian, double dblVolume) { - return new SquareConverter(new SquareADPCMDecoder(blnBigEndian, dblVolume), outStream); + public IAudioSectorDecoder makeDecoder(boolean blnBigEndian, double dblVolume) { + return new SquareConverter(new SquareADPCMDecoder(blnBigEndian, dblVolume)); } - private class SquareConverter implements IDiscItemAudioSectorDecoder { + private class SquareConverter implements IAudioSectorDecoder { private final SquareADPCMDecoder __decoder; - private final AudioOutputStream __audioWriter; + private IAudioReceiver __audioWriter; private ISquareAudioSector __leftAudioSector, __rightAudioSector; private byte[] __abTempBuffer; + private AudioFormat __format; - public SquareConverter(SquareADPCMDecoder decoder, AudioOutputStream outStream) { + public SquareConverter(SquareADPCMDecoder decoder) { __decoder = decoder; - __audioWriter = outStream; - __abTempBuffer = new byte[100000]; // TODO: calculate this as needed + __abTempBuffer = new byte[2*2*4000]; // TODO: calculate this as needed + } + + @Override + public void open(IAudioReceiver audioOut) { + __audioWriter = audioOut; } public void feedSector(IdentifiedSector sector) throws IOException { @@ -163,8 +172,9 @@ public void feedSector(IdentifiedSector sector) throws IOException { int iSize = __decoder.decode(__leftAudioSector.getIdentifiedUserDataStream(), __rightAudioSector.getIdentifiedUserDataStream(), audSector.getAudioDataSize(), __abTempBuffer); - __audioWriter.write(__decoder.getOutputFormat(__rightAudioSector.getSamplesPerSecond()), - __abTempBuffer, 0, iSize); + if (__format == null) + __format = __decoder.getOutputFormat(__rightAudioSector.getSamplesPerSecond()); + __audioWriter.write(__format, __abTempBuffer, 0, iSize, __rightAudioSector.getSectorNumber()); } else { throw new RuntimeException("Invalid audio channel " + audSector.getAudioChannel()); } @@ -179,7 +189,7 @@ public void setVolume(double dblVolume) { } public AudioFormat getOutputFormat() { - return __audioWriter.getFormat(); + return __decoder.getOutputFormat(_iSamplesPerSecond); } public void reset() { @@ -194,16 +204,12 @@ public int getStartSector() { return DiscItemSquareAudioStream.this.getStartSector(); } + public int getPresentationStartSector() { + return DiscItemSquareAudioStream.this.getPresentationStartSector(); + } } - @Override - public long calclateTime(int iSect) { - if (iSect < getStartSector() || iSect > getEndSector()) - throw new IllegalArgumentException("Sector number is out of media item bounds."); - return ( iSect - getStartSector() ) * 2 * 75; - } - @Override public DiscItemSaver getSaver() { return new SquareAudioSaver(this); @@ -215,14 +221,12 @@ public int getSampleRate() { private static class SquareAudioSaver extends DiscItemSaver { - private final PCM16bitAudioWriterBuilder _builder; + private final SectorAudioWriterBuilder _builder; private final DiscItemSquareAudioStream _audItem; public SquareAudioSaver(DiscItemSquareAudioStream audStream) { _audItem = audStream; - _builder = new PCM16bitAudioWriterBuilder( - audStream.isStereo(), audStream.getSampleRate(), - audStream.getSuggestedBaseName()); + _builder = new SectorAudioWriterBuilder(audStream); } @Override @@ -242,17 +246,15 @@ public JPanel getOptionPane() { @Override public void startSave(ProgressListener pl) throws IOException { - PCM16bitAudioWriter audioWriter = _builder.getAudioWriter(); + SectorAudioWriter audioWriter = _builder.getAudioWriter(); int iSector = _audItem.getStartSector(); - IDiscItemAudioSectorDecoder decoder = _audItem.makeDecoder(audioWriter, audioWriter.getFormat().isBigEndian(), audioWriter.getVolume()); try { final double SECTOR_LENGTH = _audItem.getEndSector() - _audItem.getStartSector(); pl.progressStart("Writing " + audioWriter.getOutputFile()); - audioWriter.open(); for (; iSector <= _audItem.getEndSector(); iSector++) { CDSector cdSector = _audItem.getSourceCD().getSector(iSector); IdentifiedSector identifiedSect = _audItem.identifySector(cdSector); - decoder.feedSector(identifiedSect); + audioWriter.feedSector(identifiedSect); pl.progressUpdate((iSector - _audItem.getStartSector()) / SECTOR_LENGTH); } pl.progressEnd(); diff --git a/src/jpsxdec/plugins/psx/square/SectorChronoXAudio.java b/src/jpsxdec/plugins/psx/square/SectorChronoXAudio.java index d8c5e28..4e7f655 100644 --- a/src/jpsxdec/plugins/psx/square/SectorChronoXAudio.java +++ b/src/jpsxdec/plugins/psx/square/SectorChronoXAudio.java @@ -99,13 +99,13 @@ public SectorChronoXAudio(CDSector oCDSect) throws NotThisTypeException { throw new NotThisTypeException(); _iAudioChunkNumber = IO.readSInt16LE(is); - if (_iAudioChunkNumber < 0) + if (_iAudioChunkNumber < 0 || _iAudioChunkNumber > 1) throw new NotThisTypeException(); _iAudioChunksInFrame = IO.readSInt16LE(is); - if (_iAudioChunksInFrame < 0) + if (_iAudioChunksInFrame != 2) throw new NotThisTypeException(); _iFrameNumber = IO.readSInt16LE(is); - if (_iFrameNumber < 0) + if (_iFrameNumber < 1) throw new NotThisTypeException(); IO.skip(is, 118); @@ -176,7 +176,7 @@ public int getSectorType() { } public String getTypeName() { - return "ChronoCrossAudio"; + return "CX Audio"; } public boolean isStereo() { @@ -209,9 +209,9 @@ public boolean matchesPrevious(ISquareAudioSector oPrevSect) { oPrevSect.getSectorNumber() + 1 != getSectorNumber()) return false; } else if (oPrevSect.getAudioChunkNumber() == 1) { - if (getAudioChunkNumber() != 0 || - oPrevSect.getFrameNumber() + 1 != getFrameNumber() || - oPrevSect.getSectorNumber() + 9 != getSectorNumber()) + if (getAudioChunkNumber() != 0) + return false; + if (oPrevSect.getFrameNumber() > getFrameNumber()) return false; } diff --git a/src/jpsxdec/plugins/psx/square/SectorChronoXVideo.java b/src/jpsxdec/plugins/psx/square/SectorChronoXVideo.java index 079a4bc..dbe1060 100644 --- a/src/jpsxdec/plugins/psx/square/SectorChronoXVideo.java +++ b/src/jpsxdec/plugins/psx/square/SectorChronoXVideo.java @@ -70,7 +70,6 @@ protected void readHeader(ByteArrayInputStream inStream) { _lngMagic = IO.readUInt32LE(inStream); - // TODO: This extra Chrono Cross hack is ugly, fix it if (_lngMagic != CHRONO_CROSS_VIDEO_CHUNK_MAGIC1 && _lngMagic != CHRONO_CROSS_VIDEO_CHUNK_MAGIC2) throw new NotThisTypeException(); @@ -116,34 +115,6 @@ public String getTypeName() { return "CX Video"; } - @Override - public boolean matchesPrevious(IVideoSector prevSector) { - if (!(prevSector instanceof SectorChronoXVideo)) - return false; - - SectorChronoXVideo xcrossVid = (SectorChronoXVideo) prevSector; - - if (getWidth() != xcrossVid.getWidth() || - getHeight() != xcrossVid.getHeight()) - return false; - - long iNextChunk = xcrossVid.getChunkNumber() + 1; - long iNextFrame = xcrossVid.getFrameNumber(); - if (iNextChunk >= xcrossVid.getChunksInFrame()) { - iNextChunk = 0; - iNextFrame++; - } - - if (iNextChunk != getChunkNumber() || iNextFrame != getFrameNumber()) - return false; - - if (xcrossVid.getFrameNumber() == getFrameNumber() && - xcrossVid.getChunksInFrame() != getChunksInFrame()) - return false; - - return true; - } - @Override public JPSXPlugin getSourcePlugin() { return JPSXPluginSquare.getPlugin(); diff --git a/src/jpsxdec/plugins/psx/square/SectorChronoXVideoNull.java b/src/jpsxdec/plugins/psx/square/SectorChronoXVideoNull.java index 8fafa0c..b793261 100644 --- a/src/jpsxdec/plugins/psx/square/SectorChronoXVideoNull.java +++ b/src/jpsxdec/plugins/psx/square/SectorChronoXVideoNull.java @@ -88,7 +88,6 @@ protected void readHeader(ByteArrayInputStream inStream) { _lngMagic = IO.readUInt32LE(inStream); - // TODO: This extra Chrono Cross hack is ugly, fix it if (_lngMagic != CHRONO_CROSS_VIDEO_CHUNK_MAGIC1 && _lngMagic != CHRONO_CROSS_VIDEO_CHUNK_MAGIC2) throw new NotThisTypeException(); @@ -113,7 +112,7 @@ protected void readHeader(ByteArrayInputStream inStream) // .. Public functions ................................................. public String getTypeName() { - return "CX Video"; + return "CX Video Null"; } @@ -133,5 +132,10 @@ public ByteArrayFPIS getIdentifiedUserDataStream() { return null; } + @Override + public String toString() { + return getTypeName() + " " + super.toString(); + } + } diff --git a/src/jpsxdec/plugins/psx/square/SectorFF7Video.java b/src/jpsxdec/plugins/psx/square/SectorFF7Video.java index 58e150e..9785f46 100644 --- a/src/jpsxdec/plugins/psx/square/SectorFF7Video.java +++ b/src/jpsxdec/plugins/psx/square/SectorFF7Video.java @@ -37,73 +37,40 @@ package jpsxdec.plugins.psx.square; -import jpsxdec.plugins.IdentifiedSector; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.logging.Logger; import jpsxdec.cdreaders.CDSector; -import jpsxdec.cdreaders.CDSector.CDXAHeader.SubMode.DATA_AUDIO_VIDEO; -import jpsxdec.plugins.DiscItem; import jpsxdec.plugins.JPSXPlugin; -import jpsxdec.plugins.psx.str.DiscItemSTRVideo; import jpsxdec.plugins.psx.str.IVideoSector; +import jpsxdec.plugins.psx.str.SectorSTR; import jpsxdec.util.ByteArrayFPIS; import jpsxdec.util.IO; import jpsxdec.util.NotThisTypeException; /** This is the header for FF7 (v1) video sectors. */ -public class SectorFF7Video extends IdentifiedSector +public class SectorFF7Video extends SectorSTR implements IVideoSector { private static final Logger log = Logger.getLogger(SectorFF7Video.class.getName()); - // .. Static stuff ..................................................... - - public static final long VIDEO_CHUNK_MAGIC = 0x80010160; - public static final int FRAME_SECTOR_HEADER_SIZE = 32; - // .. Instance fields .................................................. - protected long _lngMagic; // 0 [4 bytes] - protected int _iChunkNumber; // 4 [2 bytes] - protected int _iChunksInThisFrame; // 6 [2 bytes] - protected int _iFrameNumber; // 8 [4 bytes] - protected long _lngUsedDemuxedSize; // 12 [4 bytes] - protected int _iWidth; // 16 [2 bytes] - protected int _iHeight; // 18 [2 bytes] - protected long _lngRunLengthCodeCount; // 20 [2 bytes] - protected long _lngMagic3800; // 22 [2 bytes] always 0x3800 - protected int _iQuantizationScale; // 24 [2 bytes] - protected long _lngVersion; // 26 [2 bytes] - protected long _lngFourZeros; // 28 [4 bytes] - // 32 TOTAL + private int _iUserDataStart; + private long _lngUnknown8bytes; + // .. Constructor ..................................................... public SectorFF7Video(CDSector cdSector) throws NotThisTypeException { - super(cdSector); - // only if it has a sector header should we check if it reports DATA or VIDEO - if (cdSector.hasSectorHeader()) { - DATA_AUDIO_VIDEO eType = cdSector.getSubMode().getDataAudioVideo(); - if (eType != DATA_AUDIO_VIDEO.DATA && - eType != DATA_AUDIO_VIDEO.VIDEO) - { - throw new NotThisTypeException(); - } - } - try { - readHeader(cdSector.getCDUserDataStream()); - } catch (IOException ex) { - throw new NotThisTypeException(); - } + super(cdSector); // will call overridden readHeader() method } - private void readHeader(ByteArrayInputStream inStream) - throws NotThisTypeException, IOException + @Override + protected void readHeader(ByteArrayInputStream inStream) + throws NotThisTypeException, IOException { _lngMagic = IO.readUInt32LE(inStream); - - // TODO: This extra Chrono Cross hack is ugly, fix it if (_lngMagic != VIDEO_CHUNK_MAGIC) throw new NotThisTypeException(); @@ -118,141 +85,91 @@ private void readHeader(ByteArrayInputStream inStream) throw new NotThisTypeException(); _lngUsedDemuxedSize = IO.readSInt32LE(inStream); - if (_lngUsedDemuxedSize < 0) + if (_lngUsedDemuxedSize < 2000) throw new NotThisTypeException(); _iWidth = IO.readSInt16LE(inStream); - if (_iWidth < 1) + if (_iWidth != 320 && _iWidth != 640) throw new NotThisTypeException(); _iHeight = IO.readSInt16LE(inStream); - if (_iHeight < 1) + if (_iHeight != 224 && _iHeight != 192 && _iHeight != 240) throw new NotThisTypeException(); - - _lngRunLengthCodeCount = IO.readUInt16LE(inStream); - _lngMagic3800 = IO.readUInt16LE(inStream); - // None of the FF7 vid sectors have the magic 3800. - // it's usually 0000, but does have other values. - // So just make sure it's not 3800. - if (_lngMagic3800 == 0x3800) - throw new NotThisTypeException(); + _lngUnknown8bytes = IO.readSInt64BE(inStream); + + if (_iHeight == 240) { + // if movie height is 240, then the unknown data must all be 0 + if (_lngUnknown8bytes != 0) + throw new NotThisTypeException(); + // and the real-time flag should be set + if (!getCDSector().getSubMode().getRealTime()) + throw new NotThisTypeException(); + // (this block is unfortunately necessary to prevent + // false-positives with Lain sectors) + } - _iQuantizationScale = IO.readSInt16LE(inStream); - // do not check the qscale because it can sometimes be invalid - //if (_iQuantizationScale < 1) - // throw new NotThisTypeException(); - _lngVersion = IO.readUInt16LE(inStream); _lngFourZeros = IO.readUInt32LE(inStream); + if (_lngFourZeros != 0) + throw new NotThisTypeException(); + + if (_iChunkNumber == 0) { + inStream.skip(2); + if (IO.readUInt16LE(inStream) != 0x3800) { + IO.skip(inStream, 40 - 4 + 2); + if (IO.readUInt16LE(inStream) != 0x3800) + throw new NotThisTypeException(); + _iUserDataStart = FRAME_SECTOR_HEADER_SIZE + 40; + } else { + _iUserDataStart = FRAME_SECTOR_HEADER_SIZE; + } + } else { + _iUserDataStart = FRAME_SECTOR_HEADER_SIZE; + } } // .. Public functions ................................................. public String toString() { - return String.format("FF7 Vid %s frame:%d chunk:%d/%d %dx%d ver:%d " + - "{demux frame size=%d rlc=%d 3800=%04x qscale=%d 4*00=%08x}", - super.toString(), + + String sRet = String.format("%s %s frame:%d chunk:%d/%d %dx%d " + + "{unknown=%016x 4*00=%08x}", + getTypeName(), + super.cdToString(), _iFrameNumber, _iChunkNumber, _iChunksInThisFrame, _iWidth, _iHeight, - _lngVersion, - _lngUsedDemuxedSize, - _lngRunLengthCodeCount, - _lngMagic3800, - _iQuantizationScale, + _lngUnknown8bytes, _lngFourZeros ); - } - public int getChunkNumber() { - return _iChunkNumber; - } - - public int getChunksInFrame() { - return _iChunksInThisFrame; - } - - public int getFrameNumber() { - return _iFrameNumber; - } - - public int getWidth() { - return _iWidth; - } - - public int getHeight() { - return _iHeight; + if (_iUserDataStart == FRAME_SECTOR_HEADER_SIZE) { + return sRet; + } else { + return sRet + " + Camera data"; + } } public int getPSXUserDataSize() { return super.getCDSector().getCdUserDataSize() - - FRAME_SECTOR_HEADER_SIZE; + _iUserDataStart; } public ByteArrayFPIS getIdentifiedUserDataStream() { return new ByteArrayFPIS(super.getCDSector().getCDUserDataStream(), - FRAME_SECTOR_HEADER_SIZE, getPSXUserDataSize()); + _iUserDataStart, getPSXUserDataSize()); } public void copyIdentifiedUserData(byte[] abOut, int iOutPos) { - super.getCDSector().getCdUserDataCopy(FRAME_SECTOR_HEADER_SIZE, abOut, + super.getCDSector().getCdUserDataCopy(_iUserDataStart, abOut, iOutPos, getPSXUserDataSize()); } - public int getSectorType() { - return SECTOR_VIDEO; - } - public String getTypeName() { - return "Video"; - } - - public boolean matchesPrevious(IVideoSector prevSector) { - if (!(prevSector instanceof SectorFF7Video)) - return false; - - SectorFF7Video ff7VidSect = (SectorFF7Video) prevSector; - - if (getWidth() != ff7VidSect.getWidth() || - getHeight() != ff7VidSect.getHeight()) - return false; - - long iNextChunk = ff7VidSect.getChunkNumber() + 1; - long iNextFrame = ff7VidSect.getFrameNumber(); - if (iNextChunk >= ff7VidSect.getChunksInFrame()) { - iNextChunk = 0; - iNextFrame++; - } - - if (iNextChunk != getChunkNumber() || iNextFrame != getFrameNumber()) - return false; - - if (ff7VidSect.getFrameNumber() == getFrameNumber() && - ff7VidSect.getChunksInFrame() != getChunksInFrame()) - return false; - - return true; + return "FF7 Video"; } - public DiscItem createMedia(int iStartSector, int iStartFrame, int iFrame1End) - { - int iSectors = getSectorNumber() - iStartSector; - int iFrames = getFrameNumber() - iStartFrame; - return createMedia(iStartSector, iStartFrame, iFrame1End, iSectors, iFrames); - } - public DiscItem createMedia(int iStartSector, int iStartFrame, - int iFrame1End, - int iSectors, int iPerFrame) - { - return new DiscItemSTRVideo(iStartSector, getSectorNumber(), - iStartFrame, getFrameNumber(), - getWidth(), getHeight(), - iSectors, iPerFrame, - iFrame1End); - } - - public JPSXPlugin getSourcePlugin() { return JPSXPluginSquare.getPlugin(); } diff --git a/src/jpsxdec/plugins/psx/square/SectorFF8.java b/src/jpsxdec/plugins/psx/square/SectorFF8.java index 7d1ea8c..acd3851 100644 --- a/src/jpsxdec/plugins/psx/square/SectorFF8.java +++ b/src/jpsxdec/plugins/psx/square/SectorFF8.java @@ -214,20 +214,18 @@ public boolean matchesPrevious(IVideoSector prevSect) { } public DiscItem createMedia(int iStartSector, int iStartFrame, - int iFrame1End, + int iFrame1LastSector, int iSectors, int iPerFrame) { return new DiscItemSTRVideo(iStartSector, getSectorNumber(), iStartFrame, getFrameNumber(), getWidth(), getHeight(), iSectors, iPerFrame, - iFrame1End); + iFrame1LastSector); } - public DiscItem createMedia(int iStartSector, int iStartFrame, int iFrame1End) + public DiscItem createMedia(int iStartSector, int iStartFrame, int iFrame1LastSector) { - int iSectors = getSectorNumber() - iStartSector; - int iFrames = getFrameNumber() - iStartFrame; - return createMedia(iStartSector, iStartFrame, iFrame1End, iSectors, iFrames); + return createMedia(iStartSector, iStartFrame, iFrame1LastSector, 10, 1); } } diff --git a/src/jpsxdec/plugins/psx/square/SectorFF9.java b/src/jpsxdec/plugins/psx/square/SectorFF9.java index d80482c..8c96d37 100644 --- a/src/jpsxdec/plugins/psx/square/SectorFF9.java +++ b/src/jpsxdec/plugins/psx/square/SectorFF9.java @@ -285,20 +285,18 @@ public boolean matchesPrevious(IVideoSector prevSector) { } public DiscItem createMedia(int iStartSector, int iStartFrame, - int iFrame1End, + int iFrame1LastSector, int iSectors, int iPerFrame) { return new DiscItemSTRVideo(iStartSector, getSectorNumber(), iStartFrame, getFrameNumber(), getWidth(), getHeight(), iSectors, iPerFrame, - iFrame1End); + iFrame1LastSector); } - public DiscItem createMedia(int iStartSector, int iStartFrame, int iFrame1End) + public DiscItem createMedia(int iStartSector, int iStartFrame, int iFrame1LastSector) { - int iSectors = getSectorNumber() - iStartSector; - int iFrames = getFrameNumber() - iStartFrame; - return createMedia(iStartSector, iStartFrame, iFrame1End, iSectors, iFrames); + return createMedia(iStartSector, iStartFrame, iFrame1LastSector, 10, 1); } } diff --git a/src/jpsxdec/plugins/psx/str/AudioVideoSync.java b/src/jpsxdec/plugins/psx/str/AudioVideoSync.java new file mode 100644 index 0000000..e352d7b --- /dev/null +++ b/src/jpsxdec/plugins/psx/str/AudioVideoSync.java @@ -0,0 +1,71 @@ +package jpsxdec.plugins.psx.str; + +import jpsxdec.plugins.xa.AudioSync; +import jpsxdec.util.Fraction; + +public class AudioVideoSync extends VideoSync { + + private AudioSync _audSync; + private final Fraction _samplesPerFrame; + private final int _iInitialFrameDelay; + private final long _lngInitialSampleDelay; + + public AudioVideoSync(int iFirstVideoPresentationSector, + int iSectorsPerSecond, + Fraction sectorsPerFrame, + int iFirstAudioPresentationSector, + float fltSamplesPerSecond, + boolean blnPreciseAv) + { + super(iFirstVideoPresentationSector, + iSectorsPerSecond, sectorsPerFrame); + _audSync = new AudioSync(iFirstAudioPresentationSector, + iSectorsPerSecond, fltSamplesPerSecond); + + _samplesPerFrame = _audSync.getSamplesPerSecond().multiply(super.getSecondsPerFrame()); + + if (blnPreciseAv) { + + int iPresentationSectorDiff = iFirstAudioPresentationSector - iFirstVideoPresentationSector; + + Fraction initialSampleDelay = _audSync.getSamplesPerSecond().divide(getSectorsPerSecond()).multiply(iPresentationSectorDiff); + if (initialSampleDelay.compareTo(0) < 0) { + _iInitialFrameDelay = -(int) Math.floor(initialSampleDelay.divide(_samplesPerFrame).asDouble()); + _lngInitialSampleDelay = Math.round(initialSampleDelay.add(_samplesPerFrame.multiply(_iInitialFrameDelay)).asDouble()); + } else { + _lngInitialSampleDelay = Math.round(initialSampleDelay.asDouble()); + _iInitialFrameDelay = 0; + } + } else { + _lngInitialSampleDelay = 0; + _iInitialFrameDelay = 0; + } + + } + + @Override + public int calculateFramesToCatchup(int iSector, long lngFramesWritten) { + return super.calculateFramesToCatchup(iSector, lngFramesWritten - getInitialVideo()); + } + + public Fraction getSamplesPerSector() { + return _audSync.getSamplesPerSector(); + } + + public Fraction getSamplesPerSecond() { + return _audSync.getSamplesPerSecond(); + } + + public long getInitialAudio() { + return _lngInitialSampleDelay; + } + + @Override + public int getInitialVideo() { + return _iInitialFrameDelay; + } + + public long calculateNeededSilence(int iSector, long lngSampleCount) { + return _audSync.calculateNeededSilence(iSector, lngSampleCount); + } +} diff --git a/src/jpsxdec/plugins/psx/str/DemuxMovieWriterBuilder.java b/src/jpsxdec/plugins/psx/str/DemuxMovieWriterBuilder.java deleted file mode 100644 index a3ce924..0000000 --- a/src/jpsxdec/plugins/psx/str/DemuxMovieWriterBuilder.java +++ /dev/null @@ -1,1145 +0,0 @@ -/* - * jPSXdec: PlayStation 1 Media Decoder/Converter in Java - * Copyright (C) 2007-2010 Michael Sabin - * All rights reserved. - * - * Redistribution and use of the jPSXdec code or any derivative works are - * permitted provided that the following conditions are met: - * - * * Redistributions may not be sold, nor may they be used in commercial - * or revenue-generating business activities. - * - * * Redistributions that are modified from the original source must - * include the complete source code, including the source code for all - * components used by a binary built from the modified sources. However, as - * a special exception, the source code distributed need not include - * anything that is normally distributed (in either source or binary form) - * with the major components (compiler, kernel, and so on) of the operating - * system on which the executable runs, unless that component itself - * accompanies the executable. - * - * * Redistributions must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS - * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER - * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package jpsxdec.plugins.psx.str; - -import jpsxdec.plugins.psx.video.DemuxImage; -import jpsxdec.plugins.xa.PCM16bitAudioWriter; -import argparser.ArgParser; -import argparser.BooleanHolder; -import argparser.IntHolder; -import argparser.StringHolder; -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.beans.PropertyChangeSupport; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.imageio.ImageIO; -import javax.sound.sampled.AudioFormat; -import javax.swing.JFrame; -import jpsxdec.MainCommandLineParser; -import jpsxdec.formats.JavaImageFormat; -import jpsxdec.formats.JavaImageFormat.JpgQualities; -import jpsxdec.formats.RgbIntImage; -import jpsxdec.formats.Yuv4mpeg2; -import jpsxdec.formats.Yuv4mpeg2Writer; -import jpsxdec.plugins.DiscItem; -import jpsxdec.plugins.JPSXPlugin; -import jpsxdec.plugins.ProgressListener; -import jpsxdec.util.NotThisTypeException; -import jpsxdec.util.aviwriter.AviWriter; -import jpsxdec.plugins.psx.video.decode.DemuxFrameUncompressor; -import jpsxdec.plugins.psx.video.decode.UncompressionException; -import jpsxdec.plugins.psx.video.mdec.MdecDecoder; -import jpsxdec.plugins.psx.video.mdec.MdecDecoder_double; -import jpsxdec.plugins.psx.video.mdec.MdecDecoder_int; -import jpsxdec.plugins.psx.video.mdec.MdecInputStream; -import jpsxdec.plugins.psx.video.mdec.MdecInputStream.MdecCode; -import jpsxdec.plugins.psx.video.mdec.idct.PsxMdecIDCT; -import jpsxdec.plugins.psx.video.mdec.idct.StephensIDCT; -import jpsxdec.plugins.psx.video.mdec.idct.simple_idct; -import jpsxdec.plugins.xa.IDiscItemAudioSectorDecoder; -import jpsxdec.plugins.xa.IDiscItemAudioStream; -import jpsxdec.util.AudioOutputStream; -import jpsxdec.util.FeedbackStream; -import jpsxdec.util.Fraction; -import jpsxdec.util.IO; -import jpsxdec.util.Misc; -import jpsxdec.util.TabularFeedback; - - -public class DemuxMovieWriterBuilder { - - private static final Logger log = Logger.getLogger(DemuxMovieWriterBuilder.class.getName()); - - private final DiscItemSTRVideo _sourceVidItem; - - public DemuxMovieWriterBuilder(DiscItemSTRVideo vidItem) { - _sourceVidItem = vidItem; - setVideoFormat(VideoFormat.AVI_MJPG); - _blnSaveAudio = _sourceVidItem.hasAudio(); - setSaveStartFrame(_sourceVidItem.getStartFrame()); - setSaveEndFrame(_sourceVidItem.getEndFrame()); - setParallelAudioBySizeOrder(0); - } - - - public static final String PROP_1X_DISC_SPEED = "singleSpeed"; - private boolean _blnSingleSpeed; - public boolean getSingleSpeed() { - switch (_sourceVidItem.getDiscSpeed()) { - case 1: - return true; - case 2: - return false; - default: - return _blnSingleSpeed; - } - } - public void setSingleSpeed(boolean val) { - boolean old = getSingleSpeed(); - _blnSingleSpeed = val; - } - - public static final String PROP_SAVE_AUDIO = "saveAudio"; - private boolean _blnSaveAudio; - public boolean getSaveAudio() { - // can only decode audio if we're saving avi and - // we're starting from the first frame (otherwise the ADPCM contex is unreliable) - if (getVideoFormat().getContainer() != Container.AVI || - getSaveStartFrame() != _sourceVidItem.getStartFrame() || - !_sourceVidItem.hasAudio()) - return false; - else - return _blnSaveAudio; - } - public void setSaveAudio(boolean val) { - _blnSaveAudio = val; - } - - public static enum Container { - AVI, - IMGSEQ, - YUV4MPEG2 - } - - public static enum VideoFormat { - AVI_MJPG ("AVI: Compressed (MJPG)" , "avi:mjpg", Container.AVI, JavaImageFormat.JPG), - AVI_BMP ("AVI: Uncompressed (BMP)" , "avi:bmp", Container.AVI), - IMGSEQ_PNG ("Image sequence: png" , "png", Container.IMGSEQ, JavaImageFormat.PNG), - IMGSEQ_JPG ("Image sequence: jpg" , "jpg", Container.IMGSEQ, JavaImageFormat.JPG), - IMGSEQ_BMP ("Image sequence: bmp" , "bmp", Container.IMGSEQ, JavaImageFormat.BMP), - //IMGSEQ_RAW ("Image sequence: raw" , "raw", Container.IMGSEQ), - //IMGSEQ_YUV ("Image sequence: yuv" , "yuv", Container.IMGSEQ), - //IMGSEQ_PSXYUV ("Image sequence: PSX yuv", "psxyuv", Container.IMGSEQ), - IMGSEQ_DEMUX ("Image sequence: demux" , "demux", Container.IMGSEQ), - IMGSEQ_MDEC ("Image sequence: mdec" , "mdec", Container.IMGSEQ), - YUV4MPEG2_YUV ("yuv4mpeg2" , "y4m", Container.YUV4MPEG2), - //YUV4MPEG2_PSXYUV("yuv4mpeg2 w/ PSX yuv" , "y4m:psx", Container.YUV4MPEG2), - ; - - private final String _sGui; - private final String _sCmdLine; - private final Container _eContainer; - private final JavaImageFormat _eImgFmt; - - VideoFormat(String sGui, String sCmdLine, Container eContainer) { - this(sGui, sCmdLine, eContainer, null); - } - - VideoFormat(String sGui, String sCmdLine, Container eContainer, JavaImageFormat eDepends) { - _sGui = sGui; - _sCmdLine = sCmdLine; - _eContainer = eContainer; - _eImgFmt = eDepends; - } - - public String toString() { return _sGui; } - public String getCmdLine() { return _sCmdLine; } - public Container getContainer() { return _eContainer; } - public boolean isAvailable() { - return _eImgFmt == null ? true : _eImgFmt.isAvailable(); - } - - public boolean canSaveAudio() { return _eContainer == Container.AVI; } - - public JpgQualities getDefaultCompression() { - return _eImgFmt == null ? null : _eImgFmt.getDefaultCompression(); - } - public List getCompressionOptions() { - return _eImgFmt == null ? null : _eImgFmt.getCompressionQualityDescriptions(); - } - public JavaImageFormat getImgFmt() { return _eImgFmt; } - - public boolean isCropable() { - return this != IMGSEQ_DEMUX && this != IMGSEQ_MDEC; - } - public boolean hasDecodableQuality() { return isCropable(); } - - ///////////////////////////////////////////////////////// - - public static VideoFormat fromCmdLine(String sCmdLine) { - for (VideoFormat fmt : values()) { - if (fmt.getCmdLine().equalsIgnoreCase(sCmdLine)) - return fmt; - } - return null; - } - - public static String getCmdLineList() { - StringBuilder sb = new StringBuilder(); - for (VideoFormat fmt : values()) { - if (fmt.isAvailable()) { - if (sb.length() > 0) - sb.append(", "); - sb.append(fmt.getCmdLine()); - } - } - return sb.toString(); - } - - public static List getAvailable() { - ArrayList avalable = new ArrayList(); - for (VideoFormat fmt : values()) { - if (fmt.isAvailable()) - avalable.add(fmt); - } - return avalable; - } - - } - - public static final String PROP_VIDEO_FORMAT_LIST = "videoFormatList"; - private final List _imgFmtList = VideoFormat.getAvailable(); - public List getVideoFormatList() { - return _imgFmtList; - } - - public static final String PROP_VIDEO_FORMAT = "imageFormat"; - private VideoFormat _videoFormat; - public VideoFormat getVideoFormat() { - return _videoFormat; - } - public void setVideoFormat(VideoFormat val) { - _videoFormat = val; - } - - public static final String PROP_JPG_COMPRESSION_LIST = "jpgCompressionList"; - private List _jpgList = JpgQualities.getList(); - public List getJpgCompressionList() { - return _jpgList; - } - - public static final String PROP_JPG_COMPRESSION_OPTION = "jpgCompressionOption"; - private JpgQualities _jpgCompressionOption = JpgQualities.GOOD_QUALITY; - public JpgQualities getJpgCompressionOption() { - return _jpgCompressionOption; - } - public void setJpgCompressionOption(JpgQualities val) { - if (_jpgList != null && _jpgList.contains(val)) { - _jpgCompressionOption = val; - } - } - - public static final String PROP_CROP = "noCrop"; - private boolean _blnCrop = true; - public boolean getCrop() { - return _blnCrop; - } - public void setCrop(boolean val) { - _blnCrop = val; - } - - public int getParallelAudioCount() { - IDiscItemAudioStream[] aoAudStream = _sourceVidItem.getParallelAudioStreams(); - if (aoAudStream == null) - return 0; - else - return aoAudStream.length; - } - - private IDiscItemAudioStream _parallelAudio = null; - - public IDiscItemAudioStream getParallelAudio() { - return _parallelAudio; - } - - public boolean setParallelAudio(IDiscItemAudioStream parallelAudio) { - if (_sourceVidItem.isAudioVideoAligned(_sourceVidItem)) { - _parallelAudio = parallelAudio; - return true; - } else { - return false; - } - } - - /** Returns the new setting, if valid, otherwise null. */ - public IDiscItemAudioStream setParallelAudioBySizeOrder(int iSizeIndex) { - IDiscItemAudioStream[] aoAudStream = _sourceVidItem.getParallelAudioStreams(); - if (aoAudStream == null) - return null; - if (iSizeIndex < 0 || iSizeIndex >= aoAudStream.length) - return null; - - IDiscItemAudioStream[] aoSorted = new IDiscItemAudioStream[aoAudStream.length]; - System.arraycopy(aoAudStream, 0, aoSorted, 0, aoAudStream.length); - Arrays.sort(aoSorted, new Comparator() { - public int compare(IDiscItemAudioStream o1, IDiscItemAudioStream o2) { - int i1Overlap = _sourceVidItem.overlap((DiscItem)o1); - int i2Overlap = _sourceVidItem.overlap((DiscItem)o2); - if (i1Overlap > i2Overlap) - return -1; - else if (i1Overlap < i2Overlap) - return 1; - else return 0; - } - }); - - return _parallelAudio = aoSorted[iSizeIndex]; - } - - /** Returns the new setting, if valid, otherwise null. */ - public IDiscItemAudioStream setParallelAudioByIndexNumber(int iIndex) { - IDiscItemAudioStream[] aoAudStream = _sourceVidItem.getParallelAudioStreams(); - if (aoAudStream == null) - return null; - - for (IDiscItemAudioStream audStream : aoAudStream) { - if (audStream.getIndex() == iIndex) - return _parallelAudio = audStream; - } - return null; - } - - public static enum DecodeQualities { - LOW("Fast (lower quality)", "low"), - HIGH("High quality (slower)", "high"), - PSX("Exact PSX quality", "psx"); - - public static String getCmdLineList() { - StringBuilder sb = new StringBuilder(); - for (DecodeQualities dq : DecodeQualities.values()) { - if (sb.length() > 0) - sb.append(", "); - sb.append(dq.getCmdLine()); - } - return sb.toString(); - } - - public static DecodeQualities fromCmdLine(String sCmdLine) { - for (DecodeQualities dq : DecodeQualities.values()) { - if (dq.getCmdLine().equals(sCmdLine)) - return dq; - } - return null; - } - - private final String _sGui; - private final String _sCmdLine; - - private DecodeQualities(String sDescription, String sCmdLine) { - _sGui = sDescription; - _sCmdLine = sCmdLine; - } - - public String getCmdLine() { return _sCmdLine; } - public String toString() { return _sGui; } - - public static List getList() { - return Arrays.asList(DecodeQualities.values()); - } - } - public List getDecodeQualities() { - return DecodeQualities.getList(); - } - - public static final String PROP_DECODE_QUALITY = "decodeQuality"; - private DecodeQualities _decodeQuality = DecodeQualities.LOW; - public DecodeQualities getDecodeQuality() { - return _decodeQuality; - } - public void setDecodeQuality(DecodeQualities val) { - _decodeQuality = val; - } - - public static final String PROP_PRECISE_FRAME_TIMING = "preciseFrameTiming"; - private boolean _blnPreciseFrameTiming = false; - public boolean getPreciseFrameTiming() { - return _blnPreciseFrameTiming; - } - public void setPreciseFrameTiming(boolean val) { - _blnPreciseFrameTiming = val; - } - - public static final String PROP_PRECISE_AUDIOVIDEO_SYNC = "preciseAVSync"; - private boolean _blnPreciseAVSync = false; - public boolean getPreciseAVSync() { - return _blnPreciseAVSync; - } - public void setPreciseAVSync(boolean val) { - _blnPreciseAVSync = val; - } - - public static final String PROP_SAVE_START_FRAME = "saveStartFrame"; - private int _iSaveStartFrame; - public int getSaveStartFrame() { - return _iSaveStartFrame; - } - public void setSaveStartFrame(int val) { - _iSaveStartFrame = val; - } - - public static final String PROP_SAVE_END_FRAME = "saveEndFrame"; - private int _iSaveEndFrame; - public int getSaveEndFrame() { - return _iSaveEndFrame; - } - public void setSaveEndFrame(int val) { - _iSaveEndFrame = val; - } - - //////////////////////////////////////////////////////////////////////////// - - public String[] commandLineOptions(String[] asArgs, FeedbackStream fbs) { - if (asArgs == null) return null; - - ArgParser parser = new ArgParser("", false); - - IntHolder discSpeed = new IntHolder(-10); - parser.addOption("-x %i {[1, 2]}", discSpeed); - - StringHolder vidfmt = new StringHolder(); - parser.addOption("-vidfmt,-vf %s", vidfmt); - - StringHolder jpg = null; - JavaImageFormat JPG = JavaImageFormat.JPG; - if (JPG.isAvailable()) { - jpg = new StringHolder(); - String sParam = "-"+JPG.getId()+" %s {" + JpgQualities.getCmdLineList() +"}"; - parser.addOption(sParam, jpg); - } - - StringHolder frames = new StringHolder(); - parser.addOption("-frame,-frames,-f %s", frames); - - BooleanHolder nocrop = new BooleanHolder(false); - parser.addOption("-nocrop %v", nocrop); // only non demux & mdec formats - - StringHolder quality = new StringHolder(); - parser.addOption("-quality,-q %s", quality); - - BooleanHolder noaud = new BooleanHolder(false); - parser.addOption("-noaud %v", noaud); // Only with AVI & audio - - BooleanHolder preciseav = new BooleanHolder(getPreciseAVSync()); - parser.addOption("-preciseav %v", preciseav); // Only with AVI & audio - - BooleanHolder precisefps = new BooleanHolder(getPreciseFrameTiming()); - parser.addOption("-precisefps %v", precisefps); // Mutually excusive with fps... - - // ------------------------- - String[] asRemain = null; - asRemain = parser.matchAllArgs(asArgs, 0, 0); - // ------------------------- - - if (frames.value != null) { - try { - int iFrame = Integer.parseInt(frames.value); - setSaveStartFrame(iFrame); - setSaveEndFrame(iFrame); - fbs.printlnNorm(String.format("Frames %d-%d", - getSaveStartFrame(), getSaveEndFrame())); - } catch (NumberFormatException ex) { - int[] aiRange = Misc.splitInt(frames.value, "/"); - if (aiRange != null && aiRange.length == 2) { - setSaveStartFrame(aiRange[0]); - setSaveEndFrame(aiRange[1]); - fbs.printlnNorm(String.format("Frames %d-%d", - getSaveStartFrame(), getSaveEndFrame())); - } else { - fbs.printlnWarn("Invalid frame(s) " + frames.value); - } - } - } - - if (vidfmt.value != null) { - VideoFormat vf = VideoFormat.fromCmdLine(vidfmt.value); - if (vf != null) { - setVideoFormat(vf); - fbs.printlnNorm("Format " + getVideoFormat()); - } else { - fbs.printlnWarn("Invalid video format " + vidfmt.value); - } - } - - if (quality.value != null) { - DecodeQualities dq = DecodeQualities.fromCmdLine(quality.value); - if (dq != null) { - setDecodeQuality(dq); - fbs.printlnNorm("Using decode quality " + getDecodeQuality()); - } else { - fbs.printlnWarn("Invalid decode quality " + quality.value); - } - } - - // make sure to process this after the video format is set - if (jpg != null && jpg.value != null) { - JpgQualities q = JpgQualities.fromCmdLine(jpg.value); - if (q != null) { - setJpgCompressionOption(q); - fbs.printlnNorm("Jpg compression " + getJpgCompressionOption()); - } else { - fbs.printlnWarn("Invalid jpg compression " + jpg.value); - } - } - - if (!nocrop.value != getCrop()) { - fbs.printlnNorm("Not cropping"); - } - setCrop(!nocrop.value); - - if (!noaud.value != getSaveAudio()) { - fbs.printlnNorm("Not saving audio"); - } - setSaveAudio(!noaud.value); - - if (discSpeed.value == 1) { - setSingleSpeed(true); - fbs.printlnNorm("Forcing single disc speed"); - } else if (discSpeed.value == 2) { - setSingleSpeed(true); - fbs.printlnNorm("Forcing double disc speed"); - } - - return asRemain; - } - - public void printHelp(FeedbackStream fbs) { - TabularFeedback tfb = new TabularFeedback(); - - tfb.setRowSpacing(1); - - tfb.print("-vidfmt,-vf ").tab().print("Output video format (default avi:mjpg). Options:"); - tfb.indent(); - for (VideoFormat fmt : VideoFormat.values()) { - if (fmt.isAvailable()) { - tfb.ln().print(fmt.getCmdLine()); - } - } - tfb.newRow(); - - if (_sourceVidItem.hasAudio()) { - tfb.print("-noaud").tab().print("Don't save audio."); - tfb.newRow(); - } - - tfb.print("-quality,-q ").tab().println("Decoding quality (default low). Options:") - .indent().print(DecodeQualities.getCmdLineList()); - tfb.newRow(); - - JavaImageFormat JPG = JavaImageFormat.JPG; - if (JPG.isAvailable()) { - tfb.print("-"+JPG.getId()+" ").tab() - .println("Quality when saving as jpg or avi:mjpg (default good). Options:") - .indent().print(JpgQualities.getCmdLineList()); - tfb.newRow(); - } - - tfb.print("-frame,-frames ").tab().print("One frame, or range of frames to save."); - if (_sourceVidItem.hasAudio()) { - tfb.ln().indent().print("(audio isn't available when using this option)"); - } - tfb.newRow(); - - tfb.print("-x ").tab().print("Force disc speed of 1 or 2."); - - if (_sourceVidItem.shouldBeCropped()) { - tfb.newRow(); - tfb.print("-nocrop").tab().print("Don't crop data around unused frame edges."); - } - - tfb.write(fbs); - } - - //########################################################################## - //## The Writers ########################################################### - //########################################################################## - - private static class ImgSeqDemux implements DemuxMovieWriter { - - protected final String _sBaseName; - protected final DiscItemSTRVideo _vidItem; - private final int _iStartFrame; - private final int _iEndFrame; - private ProgressListener _progress; - - protected final int _iDigitCount; - - public ImgSeqDemux(DiscItemSTRVideo vidItem, String sBaseName, - int iStartFrame, int iEndFrame) - { - _sBaseName = sBaseName; - _iStartFrame = iStartFrame; - _iEndFrame = iEndFrame; - _vidItem = vidItem; - - _iDigitCount = String.valueOf(_iEndFrame).length(); - } - - public void open() throws IOException { /* nothing to do */ } - public void close() throws IOException { /* nothing to do */ } - public int getStartFrame() { return _iStartFrame; } - public int getEndFrame() { return _iEndFrame; } - public void repeatPreviousFrame() { /* nothing to do */ } - protected ProgressListener getListener() { return _progress; } - public void setListener(ProgressListener pl) { _progress = pl; } - - public void writeFrame(DemuxImage demux, int iSectorsFromStart) throws IOException { - File f = new File(makeFileName(demux.getFrameNumber())); - - FileOutputStream fos = new FileOutputStream(f); - try { - fos.write(demux.getData(), 0, demux.getBufferSize()); - } finally { - fos.close(); - } - } - - protected String makeFileName(int iFrame) { - return String.format("%s_%dx%d[%0"+_iDigitCount+"d].demux", - _sBaseName, - _vidItem.getWidth(), _vidItem.getHeight(), - iFrame); - } - - public String getOutputFile() { - return makeFileName(_iStartFrame) + " to " + makeFileName(_iEndFrame); - } - - public IDiscItemAudioSectorDecoder getAudioSectorDecoder() { - throw new UnsupportedOperationException("Cannot write audio."); - } - } - - //.......................................................................... - - private static class ImgSeqMdec extends ImgSeqDemux { - - private DemuxFrameUncompressor _uncompressor; - - public ImgSeqMdec(DiscItemSTRVideo vidItem, String sBaseName, - int iStartFrame, int iEndFrame) - { - super(vidItem, sBaseName, iStartFrame, iEndFrame); - } - - @Override - public void writeFrame(DemuxImage demux, int iSectorsFromStart) - throws IOException - { - - DemuxFrameUncompressor uncompressor = getUncompressor(demux); - if (uncompressor == null) - return; - - File f = new File(makeFileName(demux.getFrameNumber())); - - FileOutputStream fos = new FileOutputStream(f); - try { - writeMdec(uncompressor, fos); - } catch (UncompressionException ex) { - getListener().warning(ex); - log.log(Level.WARNING, "Error uncompressing frame " + demux.getFrameNumber(), ex); - } finally { - fos.close(); - } - } - - @Override - protected String makeFileName(int iFrame) { - return String.format("%s_%dx%d[%0"+_iDigitCount+"d].mdec", - _sBaseName, - _vidItem.getWidth(), _vidItem.getHeight(), - iFrame); - } - - private static void writeMdec(MdecInputStream oMdecIn, OutputStream oStreamOut) - throws UncompressionException, IOException - { - MdecCode oCode = new MdecCode(); - while (true) { - oMdecIn.readMdecCode(oCode); - IO.writeInt16LE(oStreamOut, oCode.toMdecWord()); - } - } - - - protected DemuxFrameUncompressor getUncompressor(DemuxImage demux) { - if (_uncompressor == null) { - _uncompressor = JPSXPlugin.identifyUncompressor(demux); - if (_uncompressor == null) { - getListener().warning("Unable to determine frame type."); - return null; - } else { - getListener().info("Using " + _uncompressor.toString() + " uncompressor"); - } - } - try { - _uncompressor.reset(demux.getData()); - } catch (NotThisTypeException ex) { - _uncompressor = JPSXPlugin.identifyUncompressor(demux); - if (_uncompressor == null) { - getListener().warning("Unable to determine frame type."); - return null; - } else { - getListener().info("Using " + _uncompressor.toString() + " uncompressor"); - } - } - return _uncompressor; - } - } - - //.......................................................................... - - private abstract static class ImgSeqRgbInt extends ImgSeqMdec { - - private final MdecDecoder _decoder; - private final RgbIntImage _imgBuff; - - public ImgSeqRgbInt(DiscItemSTRVideo vidItem, - String sBaseName, - int iStartFrame, int iEndFrame, - MdecDecoder decoder, boolean blnCrop) - { - super(vidItem, sBaseName, iStartFrame, iEndFrame); - _decoder = decoder; - int iWidth = _vidItem.getWidth(), iHeight = _vidItem.getHeight(); - if (!blnCrop) { - iWidth = (iWidth + 15) & ~15; - iHeight = (iHeight + 15) & ~15; - } - - _imgBuff = new RgbIntImage(iWidth, iHeight); - } - - @Override - abstract public void writeFrame(DemuxImage demux, int iSectorsFromStart) throws IOException; - - protected RgbIntImage getRgb(DemuxImage demux) - throws UncompressionException, NotThisTypeException - { - DemuxFrameUncompressor uncompressor = getUncompressor(demux); - if (uncompressor == null) - throw new NotThisTypeException("Unable to identify frame type."); - - try { - _decoder.decode(uncompressor); - } catch (UncompressionException ex) { - getListener().warning("Error uncompressing frame " + demux.getFrameNumber() + ": " + ex.getMessage()); - log.log(Level.WARNING, "Error uncompressing frame " + demux.getFrameNumber(), ex); - } - - _decoder.readDecodedRGB(_imgBuff); - return _imgBuff; - } - - protected int getWidth() { return _imgBuff.getWidth(); } - protected int getHeight() { return _imgBuff.getHeight(); } - - protected BufferedImage makeErrorImage(Throwable ex) { - // draw the error onto a blank image - BufferedImage bi = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); - Graphics2D g = bi.createGraphics(); - g.drawString(ex.getMessage(), 5, 20); - g.dispose(); - return bi; - } - - - } - - //.......................................................................... - - private static class ImgSeqJavaImage extends ImgSeqRgbInt { - - private final JavaImageFormat _eFmt; - - public ImgSeqJavaImage(DiscItemSTRVideo vidItem, - String sBaseName, - int iStartFrame, int iEndFrame, - MdecDecoder decoder, boolean blnCrop, - JavaImageFormat eFormat) - { - super(vidItem, sBaseName, iStartFrame, iEndFrame, decoder, blnCrop); - - _eFmt = eFormat; - } - - @Override - public void writeFrame(DemuxImage demux, int iSectorsFromStart) throws IOException { - BufferedImage bi; - try { - RgbIntImage imgBuff = getRgb(demux); - bi = imgBuff.toBufferedImage(); - } catch (Throwable ex) { - log.log(Level.WARNING, "Error with frame " + demux.getFrameNumber(), ex); - bi = makeErrorImage(ex); - } - - File f = new File(makeFileName(demux.getFrameNumber())); - ImageIO.write(bi, _eFmt.getId(), f); - } - - @Override - protected String makeFileName(int iFrame) { - return String.format("%s[%0"+_iDigitCount+"d].%s", - _sBaseName, iFrame, - _eFmt.getExtension()); - } - - } - - //.......................................................................... - - private static class AviDemuxWriter extends ImgSeqRgbInt { - private static final Fraction _150 = new Fraction(150); - private static final Fraction _75 = new Fraction(75); - - private final Fraction _frameRate; - private final float _fltJpgQuality; - private final int _iSectorsPerSecond; - - private final IDiscItemAudioStream _audioDiscItem; - private final AudioFormat _audioFmt; - private IDiscItemAudioSectorDecoder _audioSectorDecoder; - - private double _volume = 1.0; - - private AviWriter _aviWriter; - - public AviDemuxWriter(DiscItemSTRVideo vidItem, - String sBaseName, - int iStartFrame, int iEndFrame, - boolean blnSingleSpeed, - MdecDecoder decoder, - boolean blnCrop, float fltJpgQuality, - IDiscItemAudioStream audioDiscItem) - { - super(vidItem, sBaseName, - iStartFrame, iEndFrame, - decoder, blnCrop); - - _fltJpgQuality = fltJpgQuality; - if (blnSingleSpeed) { - _iSectorsPerSecond = 75; - _frameRate = _75.divide(_vidItem.getSectorsPerFrame()); - } else { - _iSectorsPerSecond = 150; - _frameRate = _150.divide(_vidItem.getSectorsPerFrame()); - } - - _audioDiscItem = audioDiscItem; - - if (_audioDiscItem != null) - _audioFmt = audioDiscItem.getAudioFormat(false); - else - _audioFmt = null; - } - - private class AviPsxAudioWriter implements AudioOutputStream { - public void close() { /* do nothing */ } - public AudioFormat getFormat() { return _audioFmt; } - - public void write(AudioFormat inFormat, byte[] abData, int iOffset, int iLength) throws IOException - { - _aviWriter.writeAudio(abData, iOffset, iLength); - } - - public String getOutputFile() { return AviDemuxWriter.this.getOutputFile(); } - } - @Override - public IDiscItemAudioSectorDecoder getAudioSectorDecoder() { - if (_audioDiscItem == null) - return null; - - if (_audioSectorDecoder == null) { - _audioSectorDecoder = _audioDiscItem.makeDecoder(new AviPsxAudioWriter(), false, _volume); - } - return _audioSectorDecoder; - } - - - @Override - public void open() throws IOException { - int iAvDelay = _vidItem.calculateAVoffset(); - double dblAvDelay = 0; - if (_vidItem.getDiscSpeed() == 1) { - dblAvDelay = iAvDelay / 75.0; - } else { - dblAvDelay = iAvDelay / 150.0; - } - // TODO: check selected frame rate to calculate av offset if disc speed is unknown - - if (_audioFmt != null) { - if (_fltJpgQuality < 0) { - _aviWriter = new AviWriter(new File(getOutputFile()), - _vidItem.getWidth(), _vidItem.getHeight(), - _frameRate.getNumerator(), - _frameRate.getDenominator(), - _audioFmt); - } else { - _aviWriter = new AviWriter(new File(getOutputFile()), - _vidItem.getWidth(), _vidItem.getHeight(), - _frameRate.getNumerator(), - _frameRate.getDenominator(), - _fltJpgQuality, - _audioFmt); - } - } else { - if (_fltJpgQuality < 0) { - _aviWriter = new AviWriter(new File(getOutputFile()), - _vidItem.getWidth(), _vidItem.getHeight(), - _frameRate.getNumerator(), - _frameRate.getDenominator()); - } else { - _aviWriter = new AviWriter(new File(getOutputFile()), - _vidItem.getWidth(), _vidItem.getHeight(), - _frameRate.getNumerator(), - _frameRate.getDenominator(), - _fltJpgQuality); - } - } - } - - @Override - public String getOutputFile() { - return _sBaseName + ".avi"; - } - - @Override - public void close() throws IOException { - if (_aviWriter != null) { - _aviWriter.close(); - _aviWriter = null; - _audioSectorDecoder = null; - } - } - - @Override - public void writeFrame(DemuxImage demux, int iSectorsFromStart) throws IOException { - - Fraction secondsPerFrame = _frameRate.reciprocal(); - - Fraction discTime = new Fraction(iSectorsFromStart, _iSectorsPerSecond); - Fraction movieTime = new Fraction(_aviWriter.getVideoFramesWritten(),1).multiply(secondsPerFrame); - Fraction closestTime = discTime.subtract(movieTime).abs(); - int iCount = 0; - while (true) { - movieTime = movieTime.add(secondsPerFrame); - - Fraction timeDiff = discTime.subtract(movieTime).abs(); - if (timeDiff.compareTo(closestTime) < 0) { - closestTime = timeDiff; -/* - if (timeDiff.compareTo(Fraction.ZERO) >= 0) { - break; - } -*/ - } else { - break; - } - iCount++; - } - - boolean blnWriteBefore = _aviWriter.getVideoFramesWritten() < 1; - - if (blnWriteBefore) { - BufferedImage bi = null; - try { - bi = getRgb(demux).toBufferedImage(); - } catch (Throwable ex) { - log.log(Level.WARNING, "Error with frame " + demux.getFrameNumber(), ex); - getListener().warning("Error with frame " + demux.getFrameNumber() + ": " + ex.getMessage()); - bi = makeErrorImage(ex); - } - _aviWriter.writeFrame(bi); - } - - while (iCount > 1) { - _aviWriter.repeatPreviousFrame(); - iCount--; - } - - if (!blnWriteBefore) { - BufferedImage bi = null; - try { - bi = getRgb(demux).toBufferedImage(); - } catch (Throwable ex) { - log.log(Level.WARNING, "Error with frame " + demux.getFrameNumber(), ex); - getListener().warning("Error with frame " + demux.getFrameNumber() + ": " + ex.getMessage()); - bi = makeErrorImage(ex); - } - _aviWriter.writeFrame(bi); - } - } - - } - - //.......................................................................... - - private static class Yuv4mpeg2MovieWriter extends ImgSeqMdec { - - private final MdecDecoder_double _decoder; - private final Yuv4mpeg2 _yuvImgBuff; - private Yuv4mpeg2Writer _writer; - private final Fraction _frameRate; - - public Yuv4mpeg2MovieWriter( - DiscItemSTRVideo vidItem, - String sBaseName, int iStartFrame, int iEndFrame, - boolean blnSingleSpeed, - boolean blnCrop) - { - super(vidItem, sBaseName, iStartFrame, iEndFrame); - - if (blnSingleSpeed) { - _frameRate = new Fraction(75).divide(_vidItem.getSectorsPerFrame()); - } else { - _frameRate = new Fraction(150).divide(_vidItem.getSectorsPerFrame()); - } - - int iWidth = _vidItem.getWidth(), iHeight = _vidItem.getHeight(); - if (!blnCrop) { - iWidth = (iWidth + 15) & ~15; - iHeight = (iHeight + 15) & ~15; - } - _decoder = new MdecDecoder_double(new StephensIDCT(), iWidth, iHeight); - _yuvImgBuff = new Yuv4mpeg2(iWidth, iHeight); - } - - @Override - public void open() throws IOException { - File f = new File(_sBaseName + ".y4m"); - - _writer = new Yuv4mpeg2Writer(f, _vidItem.getWidth(), _vidItem.getHeight(), - (int)_frameRate.getNumerator(), (int)_frameRate.getDenominator(), - Yuv4mpeg2.SUB_SAMPLING); - } - - @Override - public void close() throws IOException { - _writer.close(); - } - - @Override - public void writeFrame(DemuxImage demux, int iSectorsFromStart) throws IOException { - DemuxFrameUncompressor uncompressor = getUncompressor(demux); - if (uncompressor == null) - return; - - try { - _decoder.decode(uncompressor); - } catch (UncompressionException ex) { - log.log(Level.WARNING, "Error uncompressing frame " + demux.getFrameNumber(), ex); - } - - _decoder.readDecodedYuv4mpeg2(_yuvImgBuff); - - _writer.writeFrame(_yuvImgBuff); - } - - @Override - public String getOutputFile() { - return _sBaseName + ".y4m"; - } - } - - - - public DemuxMovieWriter createDemuxWriter() { - final MdecDecoder decoder; - switch (getDecodeQuality()) { - case HIGH: - decoder = new MdecDecoder_double(new StephensIDCT(), _sourceVidItem.getWidth(), _sourceVidItem.getHeight()); - break; - case LOW: - decoder = new MdecDecoder_int(new simple_idct(), _sourceVidItem.getWidth(), _sourceVidItem.getHeight()); - break; - case PSX: - decoder = new MdecDecoder_int(new PsxMdecIDCT(), _sourceVidItem.getWidth(), _sourceVidItem.getHeight()); - break; - default: - throw new RuntimeException("Oops"); - } - - final String sBaseName = _sourceVidItem.getSuggestedBaseName(); - int iDiscSpeed = _sourceVidItem.getDiscSpeed(); - if (iDiscSpeed < 1) - iDiscSpeed = 2; // TODO: - - switch (getVideoFormat()) { - case AVI_MJPG: - case AVI_BMP: - float fltJpgQuality = -1; - if (getVideoFormat() == VideoFormat.AVI_MJPG) { - fltJpgQuality = getJpgCompressionOption().getQuality(); - } - return new AviDemuxWriter( - _sourceVidItem, sBaseName, - getSaveStartFrame(), getSaveEndFrame(), - getSingleSpeed(), - decoder, - getCrop(), fltJpgQuality, - getParallelAudio()); - case IMGSEQ_DEMUX: - return new ImgSeqDemux(_sourceVidItem, sBaseName, - getSaveStartFrame(), getSaveEndFrame()); - case IMGSEQ_MDEC: - return new ImgSeqMdec(_sourceVidItem, sBaseName, - getSaveStartFrame(), getSaveEndFrame()); - case IMGSEQ_JPG: - case IMGSEQ_BMP: - case IMGSEQ_PNG: - return new ImgSeqJavaImage( - _sourceVidItem, sBaseName, - getSaveStartFrame(), getSaveEndFrame(), - decoder, getCrop(), - getVideoFormat().getImgFmt()); - - case YUV4MPEG2_YUV: - return new Yuv4mpeg2MovieWriter( - _sourceVidItem, sBaseName, - getSaveStartFrame(), getSaveEndFrame(), - getSingleSpeed(), _blnCrop); - - - } // end case - throw new UnsupportedOperationException(getVideoFormat() + " not implemented yet."); - } - -} diff --git a/src/jpsxdec/plugins/psx/str/DiscItemSTRVideo.java b/src/jpsxdec/plugins/psx/str/DiscItemSTRVideo.java index 83fc916..cf19beb 100644 --- a/src/jpsxdec/plugins/psx/str/DiscItemSTRVideo.java +++ b/src/jpsxdec/plugins/psx/str/DiscItemSTRVideo.java @@ -40,9 +40,11 @@ import jpsxdec.plugins.DiscItemStreaming; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import javax.sound.sampled.AudioFormat; import jpsxdec.cdreaders.CDSector; import jpsxdec.plugins.DiscItemSaver; import jpsxdec.plugins.DiscItemSerialization; @@ -50,7 +52,7 @@ import jpsxdec.plugins.DiscIndex; import jpsxdec.plugins.JPSXPlugin; import jpsxdec.plugins.IdentifiedSector; -import jpsxdec.plugins.xa.IDiscItemAudioStream; +import jpsxdec.plugins.xa.DiscItemAudioStream; import jpsxdec.util.Fraction; import jpsxdec.util.NotThisTypeException; @@ -65,7 +67,7 @@ public class DiscItemSTRVideo extends DiscItemStreaming { private static final String DIMENSIONS_KEY = "Dimentions"; private static final String DISC_SPEED_KEY = "Disc Speed"; private static final String SECTORSPERFRAME_KEY = "Sectors/Frame"; - private static final String SECTORS_TO_FRAME1_END_KEY = "Frame 1 end sector"; + private static final String FRAME1_LAST_SECTOR_KEY = "Frame 1 last sector"; /** Width of video in pixels. */ @@ -81,7 +83,7 @@ public class DiscItemSTRVideo extends DiscItemStreaming { private final long _lngSectors; private final long _lngPerFrame; - private final int _iSectorsToFrame1End; + private final int _iFrame1LastSector; private int _iDiscSpeed; @@ -89,7 +91,7 @@ public DiscItemSTRVideo(int iStartSector, int iEndSector, int lngStartFrame, int lngEndFrame, int lngWidth, int lngHeight, int iSectors, int iPerFrame, - int iSectorsToFrame1End) + int iFrame1LastSector) { super(iStartSector, iEndSector); @@ -99,7 +101,7 @@ public DiscItemSTRVideo(int iStartSector, int iEndSector, _iHeight = lngHeight; _lngSectors = iSectors; _lngPerFrame = iPerFrame; - _iSectorsToFrame1End = iSectorsToFrame1End; + _iFrame1LastSector = iFrame1LastSector; _iDiscSpeed = -1; } @@ -118,7 +120,7 @@ public DiscItemSTRVideo(DiscItemSerialization fields) throws NotThisTypeExceptio _lngSectors = alng[0]; _lngPerFrame = alng[1]; - _iSectorsToFrame1End = fields.getInt(SECTORS_TO_FRAME1_END_KEY); + _iFrame1LastSector = fields.getInt(FRAME1_LAST_SECTOR_KEY); _iDiscSpeed = fields.getInt(DISC_SPEED_KEY, -1); } @@ -128,7 +130,7 @@ public DiscItemSerialization serialize() { oSerial.addRange(FRAMES_KEY, _iStartFrame, _iEndFrame); oSerial.addDimensions(DIMENSIONS_KEY, _iWidth, _iHeight); oSerial.addFraction(SECTORSPERFRAME_KEY, _lngSectors, _lngPerFrame); - oSerial.addNumber(SECTORS_TO_FRAME1_END_KEY, _iSectorsToFrame1End); + oSerial.addNumber(FRAME1_LAST_SECTOR_KEY, _iFrame1LastSector); if (_iDiscSpeed > 0) oSerial.addNumber(DISC_SPEED_KEY, _iDiscSpeed); return oSerial; @@ -179,13 +181,9 @@ public Fraction getSectorsPerFrame() { return new Fraction(_lngSectors, _lngPerFrame); } - public IDiscItemAudioStream[] getParallelAudioStreams() { - return _aoAudioStreams; - } - @Override - public long calclateTime(int iSect) { - throw new UnsupportedOperationException("Not supported yet."); + public int getPresentationStartSector() { + return getStartSector() + _iFrame1LastSector; } /** @return Sector number where the frame begins. */ @@ -240,15 +238,15 @@ public DiscItemSaver getSaver() { - private IDiscItemAudioStream[] _aoAudioStreams; + private DiscItemAudioStream[] _aoAudioStreams; /** Called by plugin after index has been created. */ /*package private*/void collectParallelAudio(DiscIndex index) { - ArrayList parallelAudio = new ArrayList(); + ArrayList parallelAudio = new ArrayList(); for (DiscItem audioItem : index) { - if (audioItem instanceof IDiscItemAudioStream) { + if (audioItem instanceof DiscItemAudioStream) { if (isAudioVideoAligned(audioItem)) { - parallelAudio.add((IDiscItemAudioStream)audioItem); + parallelAudio.add((DiscItemAudioStream)audioItem); if (log.isLoggable(Level.INFO)) log.info("Parallel audio: " + audioItem.toString()); } @@ -257,7 +255,21 @@ public DiscItemSaver getSaver() { if (parallelAudio.size() > 0) { if (log.isLoggable(Level.INFO)) log.info("Added to this media item " + this.toString()); - _aoAudioStreams = parallelAudio.toArray(new IDiscItemAudioStream[parallelAudio.size()]); + _aoAudioStreams = parallelAudio.toArray(new DiscItemAudioStream[parallelAudio.size()]); + + // sort the parallel audio streams by size, in descending order + Arrays.sort(_aoAudioStreams, new Comparator() { + public int compare(DiscItemAudioStream o1, DiscItemAudioStream o2) { + int i1Overlap = overlap((DiscItem)o1); + int i2Overlap = overlap((DiscItem)o2); + if (i1Overlap > i2Overlap) + return -1; + else if (i1Overlap < i2Overlap) + return 1; + else return 0; + } + }); + } } @@ -303,24 +315,29 @@ public boolean hasAudio() { return _aoAudioStreams != null && _aoAudioStreams.length > 0; } - /** Returns the offset between the start of video playing vs. the - * start of audio playing, in sectors. - *

- * If the value is positive, then video starts before the audio. - * If the value is negative, then audio starts before the video. - */ - public int calculateAVoffset() { - if (_aoAudioStreams == null || _aoAudioStreams.length == 0) { - return 0; - } - // find the first parallel audio - int iEarliest = _aoAudioStreams[0].getStartSector(); - for (int i = 1; i < _aoAudioStreams.length; i++) { - if (_aoAudioStreams[i].getStartSector() < iEarliest) { - iEarliest = _aoAudioStreams[i].getStartSector(); + public int getParallelAudioStreamCount() { + return _aoAudioStreams == null ? 0 : _aoAudioStreams.length; + } + + public DiscItemAudioStream getParallelAudioStream(int i) { + if (i < 0 || i >= getParallelAudioStreamCount()) + throw new IllegalArgumentException("Video doens't have parllel audio stream " + i); + + return _aoAudioStreams[i]; + } + + public List getParallelAudio(boolean[] ablnFlags) { + if (ablnFlags.length < getParallelAudioStreamCount()) + throw new IllegalArgumentException(); + + ArrayList selected = new ArrayList(_aoAudioStreams.length); + for (int i = 0; i < getParallelAudioStreamCount(); i++) { + if (ablnFlags[i]) { + selected.add(_aoAudioStreams[i]); } } - - return _iSectorsToFrame1End - iEarliest; + selected.trimToSize(); + return selected; } + } diff --git a/src/jpsxdec/plugins/psx/str/StrFramePushDemuxer.java b/src/jpsxdec/plugins/psx/str/FrameDemuxer.java similarity index 61% rename from src/jpsxdec/plugins/psx/str/StrFramePushDemuxer.java rename to src/jpsxdec/plugins/psx/str/FrameDemuxer.java index 6b34f72..be324cb 100644 --- a/src/jpsxdec/plugins/psx/str/StrFramePushDemuxer.java +++ b/src/jpsxdec/plugins/psx/str/FrameDemuxer.java @@ -37,66 +37,60 @@ package jpsxdec.plugins.psx.str; -import jpsxdec.plugins.psx.video.DemuxImage; -import java.util.AbstractList; +import java.io.IOException; import java.util.logging.Logger; import jpsxdec.util.IWidthHeight; /** Demuxes a series of frame chunk sectors into a solid stream. * Sectors need to be added ('pushed') in their proper order. */ -public class StrFramePushDemuxer implements IWidthHeight { +public class FrameDemuxer implements IWidthHeight { - private static final Logger log = Logger.getLogger(StrFramePushDemuxer.class.getName()); + private static final Logger log = Logger.getLogger(FrameDemuxer.class.getName()); /* ---------------------------------------------------------------------- */ /* Fields --------------------------------------------------------------- */ /* ---------------------------------------------------------------------- */ private IVideoSector[] _aoChunks; - - private int _iWidth = -1; - private int _iHeight = -1; + private int _iChunkCount = -1; + private int _iFrame; private int _iDemuxFrameSize = 0; + private IDemuxReceiver _demuxReceiver; + private final int _iVideoStartSector, _iVideoEndSector; + private byte[] _abDemuxBuff; /* ---------------------------------------------------------------------- */ /* Constructors---------------------------------------------------------- */ /* ---------------------------------------------------------------------- */ - public StrFramePushDemuxer() { + public FrameDemuxer(IDemuxReceiver demuxFeeder, int iVideoStartSector, int iVideoEndSector) { _iFrame = -1; + _demuxReceiver = demuxFeeder; + _iVideoStartSector = iVideoStartSector; + _iVideoEndSector = iVideoEndSector; } - /** @param lngFrame -1 for the frame of the first chunk received. */ - public StrFramePushDemuxer(int iFrame) { - _iFrame = iFrame; - } - /* ---------------------------------------------------------------------- */ /* Properties ----------------------------------------------------------- */ /* ---------------------------------------------------------------------- */ /** [IWidthHeight] */ public int getWidth() { - return _iWidth; + return _demuxReceiver.getWidth(); } /** [IWidthHeight] */ public int getHeight() { - return _iHeight; - } - - /** Returns the frame number being demuxer, or -1 if still unknown. */ - public int getFrameNumber() { - return _iFrame; + return _demuxReceiver.getHeight(); } public int getDemuxFrameSize() { return _iDemuxFrameSize; } - public boolean isFull() { + private boolean isFull() { if (_aoChunks == null) return false; @@ -119,75 +113,111 @@ public boolean isEmpty() { public int getChunksInFrame() { return _aoChunks.length; } - + /* ---------------------------------------------------------------------- */ /* Public Functions ----------------------------------------------------- */ /* ---------------------------------------------------------------------- */ - public void addChunk(IVideoSector chunk) { - if (_iFrame < 0) - _iFrame = chunk.getFrameNumber(); - else if (_iFrame != chunk.getFrameNumber()) - throw new IllegalArgumentException("Not all chunks have the same frame number"); - - if (_iWidth < 0) - _iWidth = chunk.getWidth(); - else if (_iWidth != chunk.getWidth()) - throw new IllegalArgumentException("Not all chunks of this frame have the same width"); - - if (_iHeight < 0) - _iHeight = chunk.getHeight(); - else if (_iHeight != chunk.getHeight()) - throw new IllegalArgumentException("Not all chunks of this frame have the same height"); + public void feedSector(IVideoSector chunk) throws IOException { - // for easy reference - int iChkNum = chunk.getChunkNumber(); + if (chunk.getSectorNumber() < _iVideoStartSector || + chunk.getSectorNumber() > _iVideoEndSector) + return; + + if (chunk.getWidth() != _demuxReceiver.getWidth()) + throw new IllegalArgumentException("Inconsistent width."); + + if (chunk.getHeight() != _demuxReceiver.getHeight()) + throw new IllegalArgumentException("Inconsistent height."); + + + if (_iFrame < 0) { + newFrame(chunk); + } else if (_iFrame == chunk.getFrameNumber()) { + continueFrame(chunk); + } else { + endFrame(); + newFrame(chunk); + } - // if this is the first chunk added - if (_aoChunks == null) - _aoChunks = new IVideoSector[chunk.getChunksInFrame()]; - else if (chunk.getChunksInFrame() != _aoChunks.length) { + if (isFull()) { + endFrame(); + } + } + + public void flush() throws IOException { + endFrame(); + } + + private void continueFrame(IVideoSector chunk) { + // for easy reference + final int iChkNum = chunk.getChunkNumber(); + + _iFrame = chunk.getFrameNumber(); + + if (chunk.getChunksInFrame() != _iChunkCount) { // if the number of chunks in the frame suddenly changed - throw new IllegalArgumentException("Number of chunks in this frame changed from " + + throw new IllegalArgumentException("Number of chunks in this frame changed from " + _aoChunks.length + " to " + _aoChunks.length); - } else if (iChkNum >= _aoChunks.length) { + } else if (iChkNum >= _iChunkCount) { // if the chunk number is out of valid range throw new IllegalArgumentException("Frame chunk number " + iChkNum + " is outside the range of possible chunk numbers."); } - + // now add the chunk where it belongs in the list // but make sure we don't alrady have the chunk if (_aoChunks[iChkNum] != null) throw new IllegalArgumentException("Chunk number " + iChkNum + " already received."); - + _aoChunks[iChkNum] = chunk; // add the sector's data size to the total _iDemuxFrameSize += chunk.getPSXUserDataSize(); + } - - public void addChunks(AbstractList oChks) { - for (IVideoSector oChk : oChks) { - addChunk(oChk); - } + + private void newFrame(IVideoSector chunk) { + _iFrame = chunk.getFrameNumber(); + + if (_aoChunks == null || _aoChunks.length < chunk.getChunksInFrame()) + _aoChunks = new IVideoSector[chunk.getChunksInFrame()]; + _iChunkCount = chunk.getChunksInFrame(); + _iDemuxFrameSize = 0; + + _aoChunks[chunk.getChunkNumber()] = chunk; + // add the sector's data size to the total + _iDemuxFrameSize += chunk.getPSXUserDataSize(); } - public DemuxImage getDemuxFrame() { - byte[] ab = new byte[_iDemuxFrameSize]; - int iPos = 0; - if (_aoChunks == null) { + private void endFrame() throws IOException { + // need at least 1 chunk to continue + if (_aoChunks == null || _iDemuxFrameSize < 1 || _iChunkCount < 1) { log.warning("Frame " + _iFrame + " never received any frame chunks."); - } else { - for (int iChunk = 0; iChunk < _aoChunks.length; iChunk++) { - IVideoSector chunk = _aoChunks[iChunk]; - if (chunk != null) { - _aoChunks[iChunk].copyIdentifiedUserData(ab, iPos); - iPos += _aoChunks[iChunk].getPSXUserDataSize(); - } else { - log.warning("Frame " + _iFrame + " chunk " + iChunk + " missing."); - } + return; + } + + if (_abDemuxBuff == null || _abDemuxBuff.length < _iDemuxFrameSize) + _abDemuxBuff = new byte[_iDemuxFrameSize]; + + int iEndSector = -1; + int iPos = 0; + for (int iChunk = 0; iChunk < _iChunkCount; iChunk++) { + IVideoSector chunk = _aoChunks[iChunk]; + if (chunk != null) { + chunk.copyIdentifiedUserData(_abDemuxBuff, iPos); + iPos += chunk.getPSXUserDataSize(); + if (chunk.getSectorNumber() > iEndSector) + iEndSector = chunk.getSectorNumber(); + _aoChunks[iChunk] = null; + } else { + log.warning("Frame " + _iFrame + " chunk " + iChunk + " missing."); } } - return new DemuxImage(_iWidth, _iHeight, _iFrame, ab); + + _demuxReceiver.receive(_abDemuxBuff, _iDemuxFrameSize, _iFrame, iEndSector); + + _iDemuxFrameSize = 0; + _iChunkCount = 0; + _iFrame = -1; } } diff --git a/src/jpsxdec/plugins/psx/str/IDemuxReceiver.java b/src/jpsxdec/plugins/psx/str/IDemuxReceiver.java new file mode 100644 index 0000000..d4f4966 --- /dev/null +++ b/src/jpsxdec/plugins/psx/str/IDemuxReceiver.java @@ -0,0 +1,14 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package jpsxdec.plugins.psx.str; + +import java.io.IOException; + +public interface IDemuxReceiver { + int getWidth(); + int getHeight(); + void receive(byte[] abDemux, int iSize, int iFrameNumber, int iFrameEndSector) throws IOException; +} diff --git a/src/jpsxdec/plugins/psx/str/IVideoSector.java b/src/jpsxdec/plugins/psx/str/IVideoSector.java index 53c8556..8d01ae3 100644 --- a/src/jpsxdec/plugins/psx/str/IVideoSector.java +++ b/src/jpsxdec/plugins/psx/str/IVideoSector.java @@ -66,8 +66,8 @@ public interface IVideoSector extends IIdentifiedSector { void copyIdentifiedUserData(byte[] abOut, int iOutPos); - DiscItem createMedia(int iStartSector, int lngStartFrame, int iFrame1End); + DiscItem createMedia(int iStartSector, int lngStartFrame, int iFrame1LastSector); DiscItem createMedia(int iStartSector, int lngStartFrame, - int iFrame1End, + int iFrame1LastSector, int iSectors, int iPerFrame); } diff --git a/src/jpsxdec/plugins/psx/str/JPSXPluginVideo.java b/src/jpsxdec/plugins/psx/str/JPSXPluginVideo.java index 72ee1b0..9821ded 100644 --- a/src/jpsxdec/plugins/psx/str/JPSXPluginVideo.java +++ b/src/jpsxdec/plugins/psx/str/JPSXPluginVideo.java @@ -78,7 +78,7 @@ public static JPSXPluginVideo getPlugin() { private IVideoSector _prevSector; private int _iStartSector; private int _iStartFrame; - private int _iFirstSectorAfterFrame1 = -1; + private int _iFrame1LastSector = -1; private STRFrameRateCalc _fpsCalc; private JPSXPluginVideo() {} @@ -128,8 +128,8 @@ public void indexing_sectorRead(IdentifiedSector sector) { _prevSector.getChunksInFrame()); } if (vidSect.matchesPrevious(_prevSector)) { - if (_iFirstSectorAfterFrame1 < 0 && vidSect.getFrameNumber() > 1) - _iFirstSectorAfterFrame1 = _prevSector.getSectorNumber() + 1 - _iStartSector; + if (_iFrame1LastSector < 0 && vidSect.getFrameNumber() > _iStartFrame) + _iFrame1LastSector = _prevSector.getSectorNumber() - _iStartSector; } else { endOfMovie(); _iStartFrame = vidSect.getFrameNumber(); @@ -149,15 +149,15 @@ private void endOfMovie() { log.warning("Video stream first frame is not 0 or 1: " + _iStartFrame); } super.addDiscItem(_prevSector.createMedia(_iStartSector, _iStartFrame, - _iFirstSectorAfterFrame1, + _iFrame1LastSector, (int)oSectorsPerFrame.getNumerator(), (int)oSectorsPerFrame.getDenominator())); } else { - super.addDiscItem(_prevSector.createMedia(_iStartSector, _iStartFrame, _iFirstSectorAfterFrame1)); + super.addDiscItem(_prevSector.createMedia(_iStartSector, _iStartFrame, _iFrame1LastSector)); } _fpsCalc = null; _prevSector = null; - _iFirstSectorAfterFrame1 = -1; + _iFrame1LastSector = -1; } @Override diff --git a/src/jpsxdec/plugins/psx/str/STRVideoSaver.java b/src/jpsxdec/plugins/psx/str/STRVideoSaver.java index d113617..87c9d8e 100644 --- a/src/jpsxdec/plugins/psx/str/STRVideoSaver.java +++ b/src/jpsxdec/plugins/psx/str/STRVideoSaver.java @@ -37,8 +37,6 @@ package jpsxdec.plugins.psx.str; -import jpsxdec.plugins.psx.video.DemuxImage; -import jpsxdec.plugins.xa.IDiscItemAudioSectorDecoder; import java.awt.BorderLayout; import java.io.IOException; import java.util.logging.Level; @@ -57,12 +55,12 @@ public class STRVideoSaver extends DiscItemSaver { private static final Logger log = Logger.getLogger(STRVideoSaver.class.getName()); private DiscItemSTRVideo _vidItem; - private DemuxMovieWriterBuilder _demuxBuilder; + private SectorMovieWriterBuilder _demuxBuilder; public STRVideoSaver(DiscItemSTRVideo vidStream) { super(); _vidItem = vidStream; - _demuxBuilder = new DemuxMovieWriterBuilder(_vidItem); + _demuxBuilder = new SectorMovieWriterBuilder(_vidItem); } public JPanel getOptionPane() { @@ -92,53 +90,46 @@ public void startSave(ProgressListener pl) throws IOException { frame.setVisible(true); } else { - DemuxMovieWriter oMovieWriter = _demuxBuilder.createDemuxWriter(); + SectorMovieWriter movieWriter = _demuxBuilder.openDemuxWriter(); - oMovieWriter.setListener(pl); - - oMovieWriter.open(); + movieWriter.setListener(pl); + // TODO: change to be movieWriter.saveAudio() if (_demuxBuilder.getSaveAudio()) { - startVideoAndAudio(oMovieWriter, pl); + startVideoAndAudio(movieWriter, pl); } else { - startVideoOnly(oMovieWriter, pl); + startVideoOnly(movieWriter, pl); } } } - private void startVideoOnly(DemuxMovieWriter movieWriter, ProgressListener pl) + private void startVideoOnly(SectorMovieWriter movieWriter, ProgressListener pl) throws IOException { - final int iStartSector; - if (movieWriter.getStartFrame() != _vidItem.getStartFrame()) { - // find sector that starts the frame - iStartSector = _vidItem.seek(movieWriter.getStartFrame()).getSectorNumber(); - } else { - iStartSector = _vidItem.getStartSector(); - } - int iSector = iStartSector; + int iSector = movieWriter.getMovieStartSector(); + + final double SECTOR_LENGTH = movieWriter.getMovieEndSector() - iSector + 1; - int iFrame = movieWriter.getStartFrame(); - StrFramePushDemuxer demuxer = null; + int iCurrentFrame = movieWriter.getStartFrame(); try { - final double SECTOR_LENGTH = _vidItem.getEndSector() - iStartSector + 1; pl.progressStart("Writing " + movieWriter.getOutputFile()); - for (; iSector <= _vidItem.getEndSector(); iSector++) { - pl.event("Frame " + iFrame); + for (; iSector <= movieWriter.getMovieEndSector(); iSector++) { + CDSector cdSector = _vidItem.getSourceCD().getSector(iSector); IdentifiedSector identifiedSector = _vidItem.identifySector(cdSector); if (identifiedSector instanceof IVideoSector) { IVideoSector vidSector = (IVideoSector) identifiedSector; - int iSectFrame = vidSector.getFrameNumber(); - if (iSectFrame != iFrame) { - if (iSectFrame > movieWriter.getEndFrame()) { - break; - } else { - pl.progressUpdate((iSector - _vidItem.getStartSector()) / SECTOR_LENGTH); - iFrame = vidSector.getFrameNumber(); - } - } - demuxer = addToDemux(movieWriter, demuxer, vidSector, iSector - iStartSector); + int iFrame = vidSector.getFrameNumber(); + if (iFrame < movieWriter.getStartFrame()) + continue; + else if (iFrame > movieWriter.getEndFrame()) + break; + pl.event("Frame " + iCurrentFrame); + + if (iFrame != iCurrentFrame) + pl.progressUpdate((iSector - _vidItem.getStartSector()) / SECTOR_LENGTH); + + movieWriter.feedSectorForVideo(vidSector); } } pl.progressEnd(); @@ -147,9 +138,6 @@ private void startVideoOnly(DemuxMovieWriter movieWriter, ProgressListener pl) pl.error(ex); } finally { try { - if (demuxer != null && !demuxer.isEmpty()) { - movieWriter.writeFrame(demuxer.getDemuxFrame(), iSector - iStartSector); - } movieWriter.close(); } catch (Throwable ex) { log.log(Level.SEVERE, "", ex); @@ -158,59 +146,21 @@ private void startVideoOnly(DemuxMovieWriter movieWriter, ProgressListener pl) } } - /** Adds a video sector to a frame demuxer. It turns out to be more - * complicated than you'd think. */ - private static StrFramePushDemuxer addToDemux(DemuxMovieWriter movieWriter, - StrFramePushDemuxer demuxer, - IVideoSector vidSector, - int iSectorsFromStart) + private void startVideoAndAudio(SectorMovieWriter movieWriter, ProgressListener pl) throws IOException { - if (demuxer == null) { - // create the demuxer for the sector's frame - demuxer = new StrFramePushDemuxer(vidSector.getFrameNumber()); - } - if (demuxer.getFrameNumber() == vidSector.getFrameNumber()) { - // add the sector if it is the same frame number - demuxer.addChunk(vidSector); - } else { - // if sector has a different frame number, close off the demuxer - DemuxImage demuxFrame = demuxer.getDemuxFrame(); - // create a new one with this new sector - demuxer = new StrFramePushDemuxer(); - demuxer.addChunk(vidSector); - // and send the finished frame thru the pipe - // (wanted to wait in case of an error) - movieWriter.writeFrame(demuxFrame, iSectorsFromStart); - } - if (demuxer.isFull()) { - // send the image thru the pipe if it is complete - DemuxImage demuxFrame = demuxer.getDemuxFrame(); - demuxer = null; - movieWriter.writeFrame(demuxFrame, iSectorsFromStart); - } - return demuxer; - } - - private void startVideoAndAudio(DemuxMovieWriter movieWriter, ProgressListener pl) - throws IOException - { - IDiscItemAudioSectorDecoder audWriter = movieWriter.getAudioSectorDecoder(); - - final int iStartSector = Math.min(_vidItem.getStartSector(), audWriter.getStartSector()); + final int iStartSector = movieWriter.getMovieStartSector(); int iSector = iStartSector; - int iEndSector = Math.max(_vidItem.getEndSector(), audWriter.getEndSector()); + final int iEndSector = movieWriter.getMovieEndSector(); double SECTOR_LENGTH = iEndSector - iStartSector; - StrFramePushDemuxer demuxer = null; try { pl.progressStart("Writing " + movieWriter.getOutputFile()); int iFrame = movieWriter.getStartFrame(); - demuxer = new StrFramePushDemuxer(iFrame); for (; iSector <= iEndSector; iSector++) { pl.event("Frame " + iFrame); - // TODO: fix this logic like above + CDSector cdSector = _vidItem.getSourceCD().getSector(iSector); IdentifiedSector identifiedSector = JPSXPlugin.identifyPluginSector(cdSector); if (identifiedSector instanceof IVideoSector) { @@ -219,11 +169,12 @@ private void startVideoAndAudio(DemuxMovieWriter movieWriter, ProgressListener p iFrame <= movieWriter.getEndFrame()) { IVideoSector vidSector = (IVideoSector) identifiedSector; - demuxer = addToDemux(movieWriter, demuxer, vidSector, iSector - iStartSector); + movieWriter.feedSectorForVideo(vidSector); + iFrame = vidSector.getFrameNumber(); } } else if (identifiedSector != null) { - audWriter.feedSector(identifiedSector); + movieWriter.feedSectorForAudio(identifiedSector); } pl.progressUpdate((iSector - iStartSector) / SECTOR_LENGTH); } @@ -234,9 +185,6 @@ private void startVideoAndAudio(DemuxMovieWriter movieWriter, ProgressListener p pl.error(ex); } finally { try { - if (demuxer != null && !demuxer.isEmpty()) { - movieWriter.writeFrame(demuxer.getDemuxFrame(), iSector - iStartSector); - } movieWriter.close(); } catch (Throwable ex) { log.log(Level.SEVERE, "", ex); diff --git a/src/jpsxdec/plugins/psx/str/DemuxMovieWriter.java b/src/jpsxdec/plugins/psx/str/SectorMovieWriter.java similarity index 87% rename from src/jpsxdec/plugins/psx/str/DemuxMovieWriter.java rename to src/jpsxdec/plugins/psx/str/SectorMovieWriter.java index 5ca1a37..4903771 100644 --- a/src/jpsxdec/plugins/psx/str/DemuxMovieWriter.java +++ b/src/jpsxdec/plugins/psx/str/SectorMovieWriter.java @@ -37,20 +37,17 @@ package jpsxdec.plugins.psx.str; -import jpsxdec.plugins.psx.video.DemuxImage; import java.io.IOException; +import jpsxdec.plugins.IdentifiedSector; import jpsxdec.plugins.ProgressListener; -import jpsxdec.plugins.xa.IDiscItemAudioSectorDecoder; -public interface DemuxMovieWriter { - - void open() throws IOException; +public interface SectorMovieWriter { void close() throws IOException; - void writeFrame(DemuxImage demux, int iSectorsFromStart) throws IOException; + void feedSectorForVideo(IVideoSector sector) throws IOException; - IDiscItemAudioSectorDecoder getAudioSectorDecoder(); + void feedSectorForAudio(IdentifiedSector sector) throws IOException; int getStartFrame(); @@ -60,4 +57,8 @@ public interface DemuxMovieWriter { String getOutputFile(); + int getMovieStartSector(); + + int getMovieEndSector(); + } diff --git a/src/jpsxdec/plugins/psx/str/SectorMovieWriterBuilder.java b/src/jpsxdec/plugins/psx/str/SectorMovieWriterBuilder.java new file mode 100644 index 0000000..00df2a2 --- /dev/null +++ b/src/jpsxdec/plugins/psx/str/SectorMovieWriterBuilder.java @@ -0,0 +1,623 @@ +/* + * jPSXdec: PlayStation 1 Media Decoder/Converter in Java + * Copyright (C) 2007-2010 Michael Sabin + * All rights reserved. + * + * Redistribution and use of the jPSXdec code or any derivative works are + * permitted provided that the following conditions are met: + * + * * Redistributions may not be sold, nor may they be used in commercial + * or revenue-generating business activities. + * + * * Redistributions that are modified from the original source must + * include the complete source code, including the source code for all + * components used by a binary built from the modified sources. However, as + * a special exception, the source code distributed need not include + * anything that is normally distributed (in either source or binary form) + * with the major components (compiler, kernel, and so on) of the operating + * system on which the executable runs, unless that component itself + * accompanies the executable. + * + * * Redistributions must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package jpsxdec.plugins.psx.str; + +import argparser.ArgParser; +import argparser.BooleanHolder; +import argparser.IntHolder; +import argparser.StringHolder; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; +import jpsxdec.formats.JavaImageFormat; +import jpsxdec.formats.JavaImageFormat.JpgQualities; +import jpsxdec.plugins.psx.str.SectorMovieWriters.*; +import jpsxdec.plugins.psx.video.mdec.MdecDecoder; +import jpsxdec.plugins.psx.video.mdec.MdecDecoder_double; +import jpsxdec.plugins.psx.video.mdec.MdecDecoder_int; +import jpsxdec.plugins.psx.video.mdec.idct.PsxMdecIDCT; +import jpsxdec.plugins.psx.video.mdec.idct.StephensIDCT; +import jpsxdec.plugins.psx.video.mdec.idct.simple_idct; +import jpsxdec.plugins.xa.AudioStreamsCombiner; +import jpsxdec.plugins.xa.IAudioSectorDecoder; +import jpsxdec.plugins.xa.DiscItemAudioStream; +import jpsxdec.util.FeedbackStream; +import jpsxdec.util.Misc; +import jpsxdec.util.TabularFeedback; + + +public class SectorMovieWriterBuilder { + + private static final Logger log = Logger.getLogger(SectorMovieWriterBuilder.class.getName()); + + private final DiscItemSTRVideo _sourceVidItem; + + public SectorMovieWriterBuilder(DiscItemSTRVideo vidItem) { + _sourceVidItem = vidItem; + setVideoFormat(VideoFormat.AVI_MJPG); + _blnSaveAudio = _sourceVidItem.hasAudio(); + setSaveStartFrame(_sourceVidItem.getStartFrame()); + setSaveEndFrame(_sourceVidItem.getEndFrame()); + setParallelAudioBySizeOrder(0); + } + + + public static final String PROP_1X_DISC_SPEED = "singleSpeed"; + private boolean _blnSingleSpeed; + public boolean getSingleSpeed() { + switch (_sourceVidItem.getDiscSpeed()) { + case 1: + return true; + case 2: + return false; + default: + return _blnSingleSpeed; + } + } + public void setSingleSpeed(boolean val) { + boolean old = getSingleSpeed(); + _blnSingleSpeed = val; + } + + public static final String PROP_SAVE_AUDIO = "saveAudio"; + private boolean _blnSaveAudio; + public boolean getSaveAudio() { + // can only decode audio if we're saving avi and + // we're starting from the first frame (otherwise the ADPCM contex is unreliable) + if (getVideoFormat().getContainer() != Container.AVI || + getSaveStartFrame() != _sourceVidItem.getStartFrame() || + !_sourceVidItem.hasAudio()) + return false; + else + return _blnSaveAudio; + } + public void setSaveAudio(boolean val) { + _blnSaveAudio = val; + } + + public static enum Container { + AVI, + IMGSEQ, + YUV4MPEG2 + } + + public static enum VideoFormat { + AVI_MJPG ("AVI: Compressed (MJPG)" , "avi:mjpg", Container.AVI, JavaImageFormat.JPG), + AVI_BMP ("AVI: Uncompressed (BMP)" , "avi:bmp", Container.AVI), + AVI_YUV ("AVI: YUV" , "avi:yuv", Container.AVI), + IMGSEQ_PNG ("Image sequence: png" , "png", Container.IMGSEQ, JavaImageFormat.PNG), + IMGSEQ_JPG ("Image sequence: jpg" , "jpg", Container.IMGSEQ, JavaImageFormat.JPG), + IMGSEQ_BMP ("Image sequence: bmp" , "bmp", Container.IMGSEQ, JavaImageFormat.BMP), + //IMGSEQ_RAW ("Image sequence: raw" , "raw", Container.IMGSEQ), + //IMGSEQ_YUV ("Image sequence: yuv" , "yuv", Container.IMGSEQ), + //IMGSEQ_PSXYUV ("Image sequence: PSX yuv", "psxyuv", Container.IMGSEQ), + IMGSEQ_DEMUX ("Image sequence: demux" , "demux", Container.IMGSEQ), + IMGSEQ_MDEC ("Image sequence: mdec" , "mdec", Container.IMGSEQ), + YUV4MPEG2_YUV ("yuv4mpeg2" , "y4m", Container.YUV4MPEG2), + //YUV4MPEG2_PSXYUV("yuv4mpeg2 w/ PSX yuv" , "y4m:psx", Container.YUV4MPEG2), + ; + + private final String _sGui; + private final String _sCmdLine; + private final Container _eContainer; + private final JavaImageFormat _eImgFmt; + + VideoFormat(String sGui, String sCmdLine, Container eContainer) { + this(sGui, sCmdLine, eContainer, null); + } + + VideoFormat(String sGui, String sCmdLine, Container eContainer, JavaImageFormat eDepends) { + _sGui = sGui; + _sCmdLine = sCmdLine; + _eContainer = eContainer; + _eImgFmt = eDepends; + } + + public String toString() { return _sGui; } + public String getCmdLine() { return _sCmdLine; } + public Container getContainer() { return _eContainer; } + public boolean isAvailable() { + return _eImgFmt == null ? true : _eImgFmt.isAvailable(); + } + + public boolean canSaveAudio() { return _eContainer == Container.AVI; } + + public JpgQualities getDefaultCompression() { + return _eImgFmt == null ? null : _eImgFmt.getDefaultCompression(); + } + public List getCompressionOptions() { + return _eImgFmt == null ? null : _eImgFmt.getCompressionQualityDescriptions(); + } + public JavaImageFormat getImgFmt() { return _eImgFmt; } + + public boolean isCropable() { + return this != IMGSEQ_DEMUX && this != IMGSEQ_MDEC; + } + public boolean hasDecodableQuality() { return isCropable(); } + + ///////////////////////////////////////////////////////// + + public static VideoFormat fromCmdLine(String sCmdLine) { + for (VideoFormat fmt : values()) { + if (fmt.getCmdLine().equalsIgnoreCase(sCmdLine)) + return fmt; + } + return null; + } + + public static String getCmdLineList() { + StringBuilder sb = new StringBuilder(); + for (VideoFormat fmt : values()) { + if (fmt.isAvailable()) { + if (sb.length() > 0) + sb.append(", "); + sb.append(fmt.getCmdLine()); + } + } + return sb.toString(); + } + + public static List getAvailable() { + ArrayList avalable = new ArrayList(); + for (VideoFormat fmt : values()) { + if (fmt.isAvailable()) + avalable.add(fmt); + } + return avalable; + } + + } + + public static final String PROP_VIDEO_FORMAT_LIST = "videoFormatList"; + private final List _imgFmtList = VideoFormat.getAvailable(); + public List getVideoFormatList() { + return _imgFmtList; + } + + public static final String PROP_VIDEO_FORMAT = "imageFormat"; + private VideoFormat _videoFormat; + public VideoFormat getVideoFormat() { + return _videoFormat; + } + public void setVideoFormat(VideoFormat val) { + _videoFormat = val; + } + + public static final String PROP_JPG_COMPRESSION_LIST = "jpgCompressionList"; + private List _jpgList = JpgQualities.getList(); + public List getJpgCompressionList() { + return _jpgList; + } + + public static final String PROP_JPG_COMPRESSION_OPTION = "jpgCompressionOption"; + private JpgQualities _jpgCompressionOption = JpgQualities.GOOD_QUALITY; + public JpgQualities getJpgCompressionOption() { + return _jpgCompressionOption; + } + public void setJpgCompressionOption(JpgQualities val) { + if (_jpgList != null && _jpgList.contains(val)) { + _jpgCompressionOption = val; + } + } + + public static final String PROP_CROP = "noCrop"; + private boolean _blnCrop = true; + public boolean getCrop() { + return _blnCrop; + } + public void setCrop(boolean val) { + _blnCrop = val; + } + + public int getParallelAudioCount() { + return _sourceVidItem.getParallelAudioStreamCount(); + } + + private DiscItemAudioStream _parallelAudio = null; + + public DiscItemAudioStream getParallelAudio() { + return _parallelAudio; + } + + public boolean setParallelAudio(DiscItemAudioStream parallelAudio) { + if (_sourceVidItem.isAudioVideoAligned(_sourceVidItem)) { + _parallelAudio = parallelAudio; + return true; + } else { + return false; + } + } + + /** Returns the new setting, if valid, otherwise null. */ + public DiscItemAudioStream setParallelAudioBySizeOrder(int iSizeIndex) { + if (_sourceVidItem.hasAudio()) + return _parallelAudio = _sourceVidItem.getParallelAudioStream(iSizeIndex); + else + return null; + } + + /** Returns the new setting, if valid, otherwise null. */ + public DiscItemAudioStream setParallelAudioByIndexNumber(int iIndex) { + for (int i = 0; i < _sourceVidItem.getParallelAudioStreamCount(); i++) { + DiscItemAudioStream audStream = _sourceVidItem.getParallelAudioStream(i); + if (audStream.getIndex() == iIndex) + return _parallelAudio = audStream; + } + return null; + } + + public static enum DecodeQualities { + LOW("Fast (lower quality)", "low"), + HIGH("High quality (slower)", "high"), + PSX("Exact PSX quality", "psx"); + + public static String getCmdLineList() { + StringBuilder sb = new StringBuilder(); + for (DecodeQualities dq : DecodeQualities.values()) { + if (sb.length() > 0) + sb.append(", "); + sb.append(dq.getCmdLine()); + } + return sb.toString(); + } + + public static DecodeQualities fromCmdLine(String sCmdLine) { + for (DecodeQualities dq : DecodeQualities.values()) { + if (dq.getCmdLine().equals(sCmdLine)) + return dq; + } + return null; + } + + private final String _sGui; + private final String _sCmdLine; + + private DecodeQualities(String sDescription, String sCmdLine) { + _sGui = sDescription; + _sCmdLine = sCmdLine; + } + + public String getCmdLine() { return _sCmdLine; } + public String toString() { return _sGui; } + + public static List getList() { + return Arrays.asList(DecodeQualities.values()); + } + } + public List getDecodeQualities() { + return DecodeQualities.getList(); + } + + public static final String PROP_DECODE_QUALITY = "decodeQuality"; + private DecodeQualities _decodeQuality = DecodeQualities.LOW; + public DecodeQualities getDecodeQuality() { + return _decodeQuality; + } + public void setDecodeQuality(DecodeQualities val) { + _decodeQuality = val; + } + + public static final String PROP_PRECISE_FRAME_TIMING = "preciseFrameTiming"; + private boolean _blnPreciseFrameTiming = false; + public boolean getPreciseFrameTiming() { + return _blnPreciseFrameTiming; + } + public void setPreciseFrameTiming(boolean val) { + _blnPreciseFrameTiming = val; + } + + public static final String PROP_PRECISE_AUDIOVIDEO_SYNC = "preciseAVSync"; + private boolean _blnPreciseAVSync = false; + public boolean getPreciseAVSync() { + return _blnPreciseAVSync; + } + public void setPreciseAVSync(boolean val) { + _blnPreciseAVSync = val; + } + + public static final String PROP_SAVE_START_FRAME = "saveStartFrame"; + private int _iSaveStartFrame; + public int getSaveStartFrame() { + return _iSaveStartFrame; + } + public void setSaveStartFrame(int val) { + _iSaveStartFrame = val; + } + + public static final String PROP_SAVE_END_FRAME = "saveEndFrame"; + private int _iSaveEndFrame; + public int getSaveEndFrame() { + return _iSaveEndFrame; + } + public void setSaveEndFrame(int val) { + _iSaveEndFrame = val; + } + + //////////////////////////////////////////////////////////////////////////// + + public String[] commandLineOptions(String[] asArgs, FeedbackStream fbs) { + if (asArgs == null) return null; + + ArgParser parser = new ArgParser("", false); + + IntHolder discSpeed = new IntHolder(-10); + parser.addOption("-x %i {[1, 2]}", discSpeed); + + StringHolder vidfmt = new StringHolder(); + parser.addOption("-vidfmt,-vf %s", vidfmt); + + StringHolder jpg = null; + JavaImageFormat JPG = JavaImageFormat.JPG; + if (JavaImageFormat.JPG.isAvailable()) { + jpg = new StringHolder(); + String sParam = "-jpg %s"; + parser.addOption(sParam, jpg); + } + + StringHolder frames = new StringHolder(); + parser.addOption("-frame,-frames,-f %s", frames); + + BooleanHolder nocrop = new BooleanHolder(false); + parser.addOption("-nocrop %v", nocrop); // only non demux & mdec formats + + StringHolder quality = new StringHolder(); + parser.addOption("-quality,-q %s", quality); + + BooleanHolder noaud = new BooleanHolder(false); + parser.addOption("-noaud %v", noaud); // Only with AVI & audio + + BooleanHolder preciseav = new BooleanHolder(false); + parser.addOption("-preciseav %v", preciseav); // Only with AVI & audio + + BooleanHolder precisefps = new BooleanHolder(false); + parser.addOption("-precisefps %v", precisefps); // Mutually excusive with fps... + + // ------------------------- + String[] asRemain = null; + asRemain = parser.matchAllArgs(asArgs, 0, 0); + // ------------------------- + + if (frames.value != null) { + try { + int iFrame = Integer.parseInt(frames.value); + setSaveStartFrame(iFrame); + setSaveEndFrame(iFrame); + fbs.printlnNorm(String.format("Frames %d-%d", + getSaveStartFrame(), getSaveEndFrame())); + } catch (NumberFormatException ex) { + int[] aiRange = Misc.splitInt(frames.value, "-"); + if (aiRange != null && aiRange.length == 2) { + setSaveStartFrame(aiRange[0]); + setSaveEndFrame(aiRange[1]); + fbs.printlnNorm(String.format("Frames %d-%d", + getSaveStartFrame(), getSaveEndFrame())); + } else { + fbs.printlnWarn("Invalid frame(s) " + frames.value); + } + } + } + + if (vidfmt.value != null) { + VideoFormat vf = VideoFormat.fromCmdLine(vidfmt.value); + if (vf != null) { + setVideoFormat(vf); + fbs.printlnNorm("Format " + getVideoFormat()); + } else { + fbs.printlnWarn("Invalid video format " + vidfmt.value); + } + } + + if (quality.value != null) { + DecodeQualities dq = DecodeQualities.fromCmdLine(quality.value); + if (dq != null) { + setDecodeQuality(dq); + fbs.printlnNorm("Using decode quality " + getDecodeQuality()); + } else { + fbs.printlnWarn("Invalid decode quality " + quality.value); + } + } + + // make sure to process this after the video format is set + if (jpg != null && jpg.value != null) { + JpgQualities q = JpgQualities.fromCmdLine(jpg.value); + if (q != null) { + setJpgCompressionOption(q); + fbs.printlnNorm("Jpg compression " + getJpgCompressionOption()); + } else { + fbs.printlnWarn("Invalid jpg compression " + jpg.value); + } + } + + if (!nocrop.value != getCrop()) { + fbs.printlnNorm("Not cropping"); + } + setCrop(!nocrop.value); + + if (preciseav.value != getPreciseAVSync()) { + fbs.printlnNorm("Precise Audio/Video sync"); + } + setPreciseAVSync(preciseav.value); + + if (!noaud.value != getSaveAudio()) { + fbs.printlnNorm("Not saving audio"); + } + setSaveAudio(!noaud.value); + + if (discSpeed.value == 1) { + setSingleSpeed(true); + fbs.printlnNorm("Forcing single disc speed"); + } else if (discSpeed.value == 2) { + setSingleSpeed(true); + fbs.printlnNorm("Forcing double disc speed"); + } + + return asRemain; + } + + public void printHelp(FeedbackStream fbs) { + TabularFeedback tfb = new TabularFeedback(); + + tfb.setRowSpacing(1); + + tfb.print("-vidfmt,-vf ").tab().print("Output video format (default avi:mjpg). Options:"); + tfb.indent(); + for (VideoFormat fmt : VideoFormat.values()) { + if (fmt.isAvailable()) { + tfb.ln().print(fmt.getCmdLine()); + } + } + tfb.newRow(); + + if (_sourceVidItem.hasAudio()) { + tfb.print("-noaud").tab().print("Don't save audio."); + tfb.newRow(); + } + + tfb.print("-quality,-q ").tab().println("Decoding quality (default low). Options:") + .indent().print(DecodeQualities.getCmdLineList()); + tfb.newRow(); + + JavaImageFormat JPG = JavaImageFormat.JPG; + if (JPG.isAvailable()) { + tfb.print("-"+JPG.getId()+" ").tab() + .println("Quality when saving as jpg or avi:mjpg (default good). Options:") + .indent().print(JpgQualities.getCmdLineList()); + tfb.newRow(); + } + + tfb.print("-frame,-frames ").tab().print("One frame, or range of frames to save."); + if (_sourceVidItem.hasAudio()) { + tfb.ln().indent().print("(audio isn't available when using this option)"); + } + tfb.newRow(); + + tfb.print("-x ").tab().print("Force disc speed of 1 or 2."); + + if (_sourceVidItem.shouldBeCropped()) { + tfb.newRow(); + tfb.print("-nocrop").tab().print("Don't crop data around unused frame edges."); + } + + tfb.write(fbs); + } + + + public SectorMovieWriter openDemuxWriter() throws IOException { + final MdecDecoder vidDecoder; + switch (getDecodeQuality()) { + case HIGH: + vidDecoder = new MdecDecoder_double(new StephensIDCT(), _sourceVidItem.getWidth(), _sourceVidItem.getHeight()); + break; + case LOW: + vidDecoder = new MdecDecoder_int(new simple_idct(), _sourceVidItem.getWidth(), _sourceVidItem.getHeight()); + break; + case PSX: + vidDecoder = new MdecDecoder_int(new PsxMdecIDCT(), _sourceVidItem.getWidth(), _sourceVidItem.getHeight()); + break; + default: + throw new RuntimeException("Oops"); + } + + final String sBaseName = _sourceVidItem.getSuggestedBaseName(); + + // TODO: add api for selecting parallel audio + IAudioSectorDecoder audDecoder = null; + if (getSaveAudio()) { + boolean[] ablnSelectedAudio = new boolean[getParallelAudioCount()]; + Arrays.fill(ablnSelectedAudio, true); + List parallelAud = _sourceVidItem.getParallelAudio(ablnSelectedAudio); + audDecoder = new AudioStreamsCombiner(parallelAud, false, 1.0); + } + + switch (getVideoFormat()) { + case AVI_YUV: + return new DecodedAviWriter_YV12( + _sourceVidItem, sBaseName, + getSaveStartFrame(), getSaveEndFrame(), + getSingleSpeed(), + getCrop(), + getPreciseAVSync(), + audDecoder); + case AVI_MJPG: + return new DecodedAviWriter_MJPG( + _sourceVidItem, sBaseName, + getSaveStartFrame(), getSaveEndFrame(), + getSingleSpeed(), + vidDecoder, + getCrop(), + getJpgCompressionOption().getQuality(), + getPreciseAVSync(), + audDecoder); + case AVI_BMP: + return new DecodedAviWriter_DIB( + _sourceVidItem, sBaseName, + getSaveStartFrame(), getSaveEndFrame(), + getSingleSpeed(), + vidDecoder, + getCrop(), + getPreciseAVSync(), + audDecoder); + case IMGSEQ_DEMUX: + return new DemuxSequenceWriter(_sourceVidItem, sBaseName, + getSaveStartFrame(), getSaveEndFrame()); + case IMGSEQ_MDEC: + return new MdecSequenceWriter(_sourceVidItem, sBaseName, + getSaveStartFrame(), getSaveEndFrame()); + case IMGSEQ_JPG: + case IMGSEQ_BMP: + case IMGSEQ_PNG: + return new DecodedJavaImageSequenceWriter( + _sourceVidItem, sBaseName, + getSaveStartFrame(), getSaveEndFrame(), + vidDecoder, getCrop(), + getVideoFormat().getImgFmt()); + + case YUV4MPEG2_YUV: + return new DecodedYuv4mpeg2Writer( + _sourceVidItem, sBaseName, + getSaveStartFrame(), getSaveEndFrame(), + getSingleSpeed(), _blnCrop); + + + } // end case + throw new UnsupportedOperationException(getVideoFormat() + " not implemented yet."); + } + +} diff --git a/src/jpsxdec/plugins/psx/str/SectorMovieWriters.java b/src/jpsxdec/plugins/psx/str/SectorMovieWriters.java new file mode 100644 index 0000000..621b773 --- /dev/null +++ b/src/jpsxdec/plugins/psx/str/SectorMovieWriters.java @@ -0,0 +1,748 @@ +/* + * jPSXdec: PlayStation 1 Media Decoder/Converter in Java + * Copyright (C) 2007-2010 Michael Sabin + * All rights reserved. + * + * Redistribution and use of the jPSXdec code or any derivative works are + * permitted provided that the following conditions are met: + * + * * Redistributions may not be sold, nor may they be used in commercial + * or revenue-generating business activities. + * + * * Redistributions that are modified from the original source must + * include the complete source code, including the source code for all + * components used by a binary built from the modified sources. However, as + * a special exception, the source code distributed need not include + * anything that is normally distributed (in either source or binary form) + * with the major components (compiler, kernel, and so on) of the operating + * system on which the executable runs, unless that component itself + * accompanies the executable. + * + * * Redistributions must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package jpsxdec.plugins.psx.str; + +import jpsxdec.plugins.IdentifiedSector; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.imageio.ImageIO; +import javax.sound.sampled.AudioFormat; +import jpsxdec.formats.JavaImageFormat; +import jpsxdec.formats.RgbIntImage; +import jpsxdec.formats.Yuv4mpeg2; +import jpsxdec.formats.Yuv4mpeg2Writer; +import jpsxdec.plugins.JPSXPlugin; +import jpsxdec.plugins.ProgressListener; +import jpsxdec.util.NotThisTypeException; +import jpsxdec.util.aviwriter.AviWriter; +import jpsxdec.plugins.psx.video.decode.DemuxFrameUncompressor; +import jpsxdec.plugins.psx.video.decode.UncompressionException; +import jpsxdec.plugins.psx.video.mdec.MdecDecoder; +import jpsxdec.plugins.psx.video.mdec.MdecDecoder_double; +import jpsxdec.plugins.psx.video.mdec.MdecInputStream; +import jpsxdec.plugins.psx.video.mdec.MdecInputStream.MdecCode; +import jpsxdec.plugins.psx.video.mdec.idct.StephensIDCT; +import jpsxdec.plugins.xa.IAudioSectorDecoder; +import jpsxdec.plugins.xa.IAudioReceiver; +import jpsxdec.util.Fraction; +import jpsxdec.util.IO; +import jpsxdec.util.aviwriter.AviWriterDIB; +import jpsxdec.util.aviwriter.AviWriterMJPG; +import jpsxdec.util.aviwriter.AviWriterYV12; + + +class SectorMovieWriters { + + private static final Logger log = Logger.getLogger(SectorMovieWriters.class.getName()); + + //########################################################################## + //## The Writers ########################################################### + //########################################################################## + + public static class DemuxSequenceWriter implements SectorMovieWriter, IDemuxReceiver { + + protected final String _sBaseName; + protected final DiscItemSTRVideo _vidItem; + private final int _iStartFrame; + private final int _iEndFrame; + private ProgressListener _progress; + private final FrameDemuxer _demuxer; + + protected final int _iDigitCount; + + public DemuxSequenceWriter(DiscItemSTRVideo vidItem, String sBaseName, + int iStartFrame, int iEndFrame) + { + _sBaseName = sBaseName; + _iStartFrame = iStartFrame; + _iEndFrame = iEndFrame; + _vidItem = vidItem; + + _iDigitCount = String.valueOf(_iEndFrame).length(); + + _demuxer = new FrameDemuxer(this, vidItem.getStartSector(), vidItem.getEndSector()); + } + + public void close() throws IOException { /* nothing to do */ } + public int getMovieEndSector() { return _vidItem.getEndSector(); } + public int getMovieStartSector() { return _vidItem.getStartSector(); } + public int getStartFrame() { return _iStartFrame; } + public int getEndFrame() { return _iEndFrame; } + protected ProgressListener getListener() { return _progress; } + public void setListener(ProgressListener pl) { _progress = pl; } + public int getWidth() { return _vidItem.getWidth(); } + public int getHeight() { return _vidItem.getHeight(); } + + + protected String makeFileName(int iFrame) { + return String.format("%s_%dx%d[%0"+_iDigitCount+"d].demux", + _sBaseName, + _vidItem.getWidth(), _vidItem.getHeight(), + iFrame); + } + + public String getOutputFile() { + return makeFileName(_iStartFrame) + " to " + makeFileName(_iEndFrame); + } + + @Override + public void feedSectorForVideo(IVideoSector sector) throws IOException { + _demuxer.feedSector(sector); + } + + public void feedSectorForAudio(IdentifiedSector sector) throws IOException { + throw new UnsupportedOperationException("Cannot write audio with image sequence."); + } + + @Override + public void receive(byte[] abDemux, int iSize, int iFrameNumber, int iFrameEndSector) throws IOException { + if (iFrameNumber < _iStartFrame || iFrameNumber > _iEndFrame) + return; + + File f = new File(makeFileName(iFrameNumber)); + FileOutputStream fos = new FileOutputStream(f); + try { + fos.write(abDemux, 0, iSize); + } finally { + fos.close(); + } + } + + } + + //.......................................................................... + + public static class MdecSequenceWriter extends DemuxSequenceWriter { + + private DemuxFrameUncompressor _uncompressor; + + public MdecSequenceWriter(DiscItemSTRVideo vidItem, String sBaseName, + int iStartFrame, int iEndFrame) + { + super(vidItem, sBaseName, iStartFrame, iEndFrame); + + } + + private DemuxFrameUncompressor identify(byte[] abDemuxBuf, int iStart, int iFrame) + throws NotThisTypeException + { + DemuxFrameUncompressor uncompressor = JPSXPlugin.identifyUncompressor(abDemuxBuf, iStart, iFrame); + if (uncompressor == null) { + throw new NotThisTypeException("Error with frame " + iFrame + ": Unable to determine frame type."); + } else { + String s = "Using " + uncompressor.toString() + " uncompressor"; + log.info(s); + getListener().info(s); + } + return uncompressor; + } + protected DemuxFrameUncompressor resetUncompressor(byte[] abDemuxBuf, int iStart, int iFrame) throws NotThisTypeException { + if (_uncompressor == null) { + _uncompressor = identify(abDemuxBuf, iStart, iFrame); + _uncompressor.reset(abDemuxBuf, iStart); + } else { + try { + _uncompressor.reset(abDemuxBuf, iStart); + } catch (NotThisTypeException ex) { + _uncompressor = identify(abDemuxBuf, iStart, iFrame); + _uncompressor.reset(abDemuxBuf, iStart); + } + } + return _uncompressor; + } + + + @Override + public void receive(byte[] abDemux, int iSize, int iFrameNumber, int iFrameEndSector) throws IOException { + try { + DemuxFrameUncompressor uncompressor = resetUncompressor(abDemux, 0, iFrameNumber); + receiveUncompressor(uncompressor, iFrameNumber, iFrameEndSector); + } catch (NotThisTypeException ex) { + log.log(Level.WARNING, null, ex); + getListener().warning(ex); + } + } + + protected void receiveUncompressor(DemuxFrameUncompressor uncompressor, int iFrameNumber, int iFrameEndSector) throws IOException { + File f = new File(makeFileName(iFrameNumber)); + FileOutputStream fos = null; + try { + fos = new FileOutputStream(f); + try { + writeMdec(uncompressor, fos); + } catch (UncompressionException ex) { + log.log(Level.WARNING, "Error uncompressing frame " + iFrameNumber, ex); + getListener().warning("Error uncompressing frame " + iFrameNumber, ex); + } + } catch (IOException ex) { + log.log(Level.SEVERE, "Error writing frame " + iFrameNumber, ex); + getListener().error("Error writing frame " + iFrameNumber, ex); + } finally { + try { + fos.close(); + } catch (IOException ex) { + log.log(Level.SEVERE, "Error closing file for frame " + iFrameNumber, ex); + getListener().error("Error closing file for frame " + iFrameNumber, ex); + } + } + } + + protected String makeFileName(int iFrame) { + return String.format("%s_%dx%d[%0"+_iDigitCount+"d].mdec", + _sBaseName, + _vidItem.getWidth(), _vidItem.getHeight(), + iFrame); + } + + private void writeMdec(MdecInputStream mdecIn, OutputStream streamOut) + throws UncompressionException, IOException + { + MdecCode code = new MdecCode(); + final int TOTAL_BLOCKS = ((getHeight() + 15)) / 16 * ((getWidth() + 15) / 16) * 6; + int iBlock = 0; + while (iBlock < TOTAL_BLOCKS) { + if (mdecIn.readMdecCode(code)) { + iBlock++; + } + IO.writeInt16LE(streamOut, code.toMdecWord()); + } + } + + } + + //.......................................................................... + + + public abstract static class AbstractDecodedWriter extends MdecSequenceWriter { + + protected MdecDecoder _decoder; + private final int _iCroppedWidth, _iCroppedHeight; + + public AbstractDecodedWriter(DiscItemSTRVideo vidItem, + String sBaseName, + int iStartFrame, int iEndFrame, + MdecDecoder decoder, boolean blnCrop) + { + super(vidItem, sBaseName, iStartFrame, iEndFrame); + if (blnCrop) { + _iCroppedWidth = _vidItem.getWidth(); + _iCroppedHeight = _vidItem.getHeight(); + } else { + _iCroppedWidth = (_vidItem.getWidth() + 15) & ~15; + _iCroppedHeight = (_vidItem.getHeight() + 15) & ~15; + } + _decoder = decoder; // may be null for now, but set in subclass constructors + } + + @Override + final protected void receiveUncompressor(DemuxFrameUncompressor uncompressor, int iFrameNumber, int iFrameEndSector) throws IOException { + try { + _decoder.decode(uncompressor); + receiveDecoded(_decoder, iFrameNumber, iFrameEndSector); + } catch (UncompressionException ex) { + log.log(Level.SEVERE, "Error uncompressing frame " + iFrameNumber, ex); + getListener().error("Error uncompressing frame " + iFrameNumber, ex); + } + } + + protected int getCroppedHeight() { + return _iCroppedHeight; + } + + protected int getCroppedWidth() { + return _iCroppedWidth; + } + + abstract protected void receiveDecoded(MdecDecoder decoder, int iFrame, int iFrameEndSector) throws IOException; + abstract protected void receiveError(Throwable ex, int iFrame) throws IOException; + } + + //.......................................................................... + + public static class DecodedJavaImageSequenceWriter extends AbstractDecodedWriter { + + private final JavaImageFormat _eFmt; + private final RgbIntImage _rgbBuff; + + public DecodedJavaImageSequenceWriter(DiscItemSTRVideo vidItem, + String sBaseName, + int iStartFrame, int iEndFrame, + MdecDecoder decoder, boolean blnCrop, + JavaImageFormat eFormat) + { + super(vidItem, sBaseName, iStartFrame, iEndFrame, decoder, blnCrop); + + _rgbBuff = new RgbIntImage(getCroppedWidth(), getCroppedHeight()); + _eFmt = eFormat; + } + + @Override + protected void receiveDecoded(MdecDecoder decoder, int iFrame, int iFrameEndSector) { + decoder.readDecodedRGB(_rgbBuff); + BufferedImage bi = _rgbBuff.toBufferedImage(); + File f = new File(makeFileName(iFrame)); + try { + if (!ImageIO.write(bi, _eFmt.getId(), f)) { + log.log(Level.WARNING, "Unable to write frame file " + f); + getListener().warning("Unable to write frame file " + f); + } + } catch (IOException ex) { + log.log(Level.WARNING, "Error writing frame file " + f, ex); + getListener().error("Error writing frame file " + f, ex); + } + } + + @Override + protected void receiveError(Throwable thrown, int iFrame) { + log.log(Level.WARNING, "Error with frame " + iFrame, thrown); + BufferedImage bi = makeErrorImage(thrown, _rgbBuff.getWidth(), _rgbBuff.getHeight()); + File f = new File(makeFileName(iFrame)); + try { + if (!ImageIO.write(bi, _eFmt.getId(), f)) { + log.log(Level.WARNING, "Unable to write error frame file " + f); + getListener().warning("Unable to write error frame file " + f); + } + } catch (IOException ex) { + log.log(Level.WARNING, "Error writing error frame file " + f, ex); + getListener().error("Error writing error frame file " + f, ex); + } + } + + protected String makeFileName(int iFrame) { + return String.format("%s[%0"+_iDigitCount+"d].%s", + _sBaseName, iFrame, + _eFmt.getExtension()); + } + + } + + + + public static class DecodedYuv4mpeg2Writer extends AbstractDecodedWriter { + + private final Yuv4mpeg2 _yuvImgBuff; + private Yuv4mpeg2Writer _writer; + private final MdecDecoder_double _decoderDbl; + + public DecodedYuv4mpeg2Writer(DiscItemSTRVideo vidItem, + String sBaseName, + int iStartFrame, int iEndFrame, + boolean blnSingleSpeed, + boolean blnCrop) + throws IOException + { + super(vidItem, sBaseName, iStartFrame, iEndFrame, null, blnCrop); + + final Fraction frameRate; + if (blnSingleSpeed) { + frameRate = new Fraction(75).divide(_vidItem.getSectorsPerFrame()); + } else { + frameRate = new Fraction(150).divide(_vidItem.getSectorsPerFrame()); + } + + _decoder = _decoderDbl = new MdecDecoder_double(new StephensIDCT(), getCroppedWidth(), getCroppedHeight()); + _yuvImgBuff = new Yuv4mpeg2(getCroppedWidth(), getCroppedHeight()); + + File f = new File(_sBaseName + ".y4m"); + + _writer = new Yuv4mpeg2Writer(f, getCroppedWidth(), getCroppedHeight(), + (int)frameRate.getNumerator(), (int)frameRate.getDenominator(), + Yuv4mpeg2.SUB_SAMPLING); + } + + @Override + protected void receiveDecoded(MdecDecoder decoder, int iFrame, int iFrameEndSector) throws IOException { + _decoderDbl.readDecodedYuv4mpeg2(_yuvImgBuff); + _writer.writeFrame(_yuvImgBuff); + } + + @Override + protected void receiveError(Throwable ex, int iFrame) throws IOException { + BufferedImage bi = makeErrorImage(ex, _writer.getWidth(), _writer.getHeight()); + Yuv4mpeg2 yuv = new Yuv4mpeg2(bi); + _writer.writeFrame(yuv); + } + + + @Override + public String getOutputFile() { + return _sBaseName + ".y4m"; + } + + @Override + public void close() throws IOException { + _writer.close(); + } + + } + + + protected static BufferedImage makeErrorImage(Throwable ex, int iWidth, int iHeight) { + // draw the error onto a blank image + BufferedImage bi = new BufferedImage(iWidth, iHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D g = bi.createGraphics(); + g.drawString(ex.getMessage(), 5, 20); + g.dispose(); + return bi; + } + + //.......................................................................... + + + + public abstract static class AbstractDecodedAviWriter extends AbstractDecodedWriter { + + private IAudioSectorDecoder _audioSectorDecoder; + + private final int _iStartSector, _iEndSector; + + protected final VideoSync _vidSync; + + private double _volume = 1.0; + + protected AviWriter _aviWriter; + + public AbstractDecodedAviWriter(DiscItemSTRVideo vidItem, + String sBaseName, + int iStartFrame, int iEndFrame, + boolean blnSingleSpeed, + MdecDecoder decoder, + boolean blnCrop, + boolean blnPrecisesAV, + IAudioSectorDecoder audioDecoder) + throws IOException + { + super(vidItem, sBaseName, iStartFrame, iEndFrame, decoder, blnCrop); + + final int iSectorsPerSecond = blnSingleSpeed ? 75 : 150; + + _audioSectorDecoder = audioDecoder; + + if (_audioSectorDecoder != null) { + + AudioFormat fmt = _audioSectorDecoder.getOutputFormat(); + if (fmt.isBigEndian()) + throw new IllegalArgumentException("Audio format must be little endian for avi writing."); + + AudioVideoSync avSync = new AudioVideoSync( + _vidItem.getPresentationStartSector(), + iSectorsPerSecond, + _vidItem.getSectorsPerFrame(), + _audioSectorDecoder.getPresentationStartSector(), + fmt.getSampleRate(), + blnPrecisesAV); + + _audioSectorDecoder.open(new AviAudioWriter(avSync)); + + _vidSync = avSync; + + _iStartSector = Math.min(_vidItem.getStartSector(), + _audioSectorDecoder.getStartSector()); + _iEndSector = Math.max(_vidItem.getEndSector(), + _audioSectorDecoder.getEndSector()); + + } else { + _vidSync = new VideoSync(_vidItem.getPresentationStartSector(), + iSectorsPerSecond, + _vidItem.getSectorsPerFrame()); + + _iStartSector = _vidItem.getStartSector(); + _iEndSector = _vidItem.getEndSector(); + + + } + + } + + private class AviAudioWriter implements IAudioReceiver { + private final AudioVideoSync _avSync; + + public AviAudioWriter(AudioVideoSync avSync) { + _avSync = avSync; + } + + public void close() { /* do nothing */ } + + public void write(AudioFormat inFormat, byte[] abData, int iStart, int iLen, int iPresentationSector) throws IOException + { + if (_aviWriter.getAudioSamplesWritten() < 1 && + _avSync.getInitialAudio() > 0) + { + getListener().warning("Writing " + _avSync.getInitialAudio() + " samples of silence to align audio/video playback."); + _aviWriter.writeSilentSamples(_avSync.getInitialAudio()); + } + long lngNeededSilence = _avSync.calculateNeededSilence(iPresentationSector, iLen / inFormat.getFrameSize()); + if (lngNeededSilence > 0) { + getListener().warning("Adding " + lngNeededSilence + " samples to keep audio in sync."); + _aviWriter.writeSilentSamples(lngNeededSilence); + } + _aviWriter.writeAudio(abData, iStart, iLen); + } + } + @Override + public void feedSectorForAudio(IdentifiedSector sector) throws IOException { + if (_audioSectorDecoder == null) + return; + + _audioSectorDecoder.feedSector(sector); + } + + @Override + public int getMovieEndSector() { + return _iEndSector; + } + + @Override + public int getMovieStartSector() { + return _iStartSector; + } + + + @Override + public String getOutputFile() { + return _sBaseName + ".avi"; + } + + @Override + public void close() throws IOException { + if (_aviWriter != null) { + _aviWriter.close(); + _aviWriter = null; + _audioSectorDecoder = null; + } + } + + @Override + protected void receiveDecoded(MdecDecoder decoder, int iFrame, int iFrameEndSector) + throws IOException + { + + // if first frame + if (_aviWriter.getVideoFramesWritten() < 1 && _vidSync.getInitialVideo() > 0) { + + getListener().warning("Writing " + _vidSync.getInitialVideo() + " blank frame(s) to align audio/video playback."); + _aviWriter.writeBlankFrame(); + for (int i = _vidSync.getInitialVideo()-1; i > 0; i--) { + _aviWriter.repeatPreviousFrame(); + } + + } + + int iDupCount = _vidSync.calculateFramesToCatchup( + iFrameEndSector, + _aviWriter.getVideoFramesWritten()); + + if (iDupCount < 0) + // hopefully this will never happen because the frame rate + // calculated during indexing should prevent it + getListener().warning("Frame "+iFrame+" is ahead of reading by " + (-iDupCount) + " frame(s)."); + else while (iDupCount > 0) { // will never happen with first frame + _aviWriter.repeatPreviousFrame(); + iDupCount--; + } + + actuallyWrite(decoder, iFrame); + } + + + @Override + final protected void receiveError(Throwable ex, int iFrame) { + try { + writeError(ex); + } catch (IOException ex1) { + log.log(Level.WARNING, "Error writing error frame " + iFrame, ex); + getListener().error(ex); + } + } + + abstract protected void actuallyWrite(MdecDecoder decoder, int iFrame) throws IOException; + abstract protected void writeError(Throwable ex) throws IOException; + + } + + public static class DecodedAviWriter_MJPG extends AbstractDecodedAviWriter { + + private final float _fltJpgQuality; + private final AviWriterMJPG _writerMjpg; + private final RgbIntImage _rgbBuff; + + public DecodedAviWriter_MJPG(DiscItemSTRVideo vidItem, String sBaseName, + int iStartFrame, int iEndFrame, + boolean blnSingleSpeed, MdecDecoder decoder, + boolean blnCrop, float fltJpgQuality, + boolean blnPreciseAV, + IAudioSectorDecoder audioDecoder) + throws IOException + { + super(vidItem, sBaseName, iStartFrame, iEndFrame, + blnSingleSpeed, decoder, blnCrop, blnPreciseAV, audioDecoder); + + _fltJpgQuality = fltJpgQuality; + + _writerMjpg = new AviWriterMJPG(new File(getOutputFile()), + getCroppedWidth(), getCroppedHeight(), + _vidSync.getFpsNum(), + _vidSync.getFpsDenom(), + _fltJpgQuality, + audioDecoder == null ? null : audioDecoder.getOutputFormat()); + + super._aviWriter = _writerMjpg; + _rgbBuff = new RgbIntImage(getCroppedWidth(), getCroppedHeight()); + + } + + @Override + protected void actuallyWrite(MdecDecoder decoder, int iFrame) throws IOException { + decoder.readDecodedRGB(_rgbBuff); + _writerMjpg.writeFrame(_rgbBuff.toBufferedImage()); + } + + @Override + protected void writeError(Throwable ex) throws IOException { + _writerMjpg.writeFrame(makeErrorImage(ex, _aviWriter.getWidth(), _aviWriter.getHeight())); + } + } + + + public static class DecodedAviWriter_DIB extends AbstractDecodedAviWriter { + + private final AviWriterDIB _writerDib; + private final RgbIntImage _rgbBuff; + + public DecodedAviWriter_DIB(DiscItemSTRVideo vidItem, + String sBaseName, + int iStartFrame, int iEndFrame, + boolean blnSingleSpeed, + MdecDecoder decoder, + boolean blnCrop, + boolean blnPreciseAV, + IAudioSectorDecoder audioDecoder) + throws IOException + { + super(vidItem, + sBaseName, + iStartFrame, iEndFrame, + blnSingleSpeed, + decoder, + blnCrop, + blnPreciseAV, + audioDecoder); + + _writerDib = new AviWriterDIB(new File(getOutputFile()), + getCroppedWidth(), getCroppedHeight(), + _vidSync.getFpsNum(), + _vidSync.getFpsDenom(), + audioDecoder == null ? null : audioDecoder.getOutputFormat()); + + super._aviWriter = _writerDib; + _rgbBuff = new RgbIntImage(getCroppedWidth(), getCroppedHeight()); + } + + @Override + protected void actuallyWrite(MdecDecoder decoder, int iFrame) throws IOException { + decoder.readDecodedRGB(_rgbBuff); + _writerDib.writeFrameRGB(_rgbBuff.getData(), 0, _rgbBuff.getWidth()); + } + + @Override + protected void writeError(Throwable ex) throws IOException { + BufferedImage bi = makeErrorImage(ex, _writerDib.getWidth(), _writerDib.getHeight()); + RgbIntImage rgb = new RgbIntImage(bi); + _writerDib.writeFrameRGB(rgb.getData(), 0, rgb.getWidth()); + } + } + + + //.......................................................................... + + public static class DecodedAviWriter_YV12 extends AbstractDecodedAviWriter { + + private final Yuv4mpeg2 _yuvImgBuff; + private final AviWriterYV12 _writerYuv; + private final MdecDecoder_double _decoderDbl; + + public DecodedAviWriter_YV12(DiscItemSTRVideo vidItem, + String sBaseName, + int iStartFrame, int iEndFrame, + boolean blnSingleSpeed, + boolean blnCrop, + boolean blnPreciseAV, + IAudioSectorDecoder audioDecoder) + throws IOException + { + super(vidItem, sBaseName, + iStartFrame, iEndFrame, + blnSingleSpeed, null, blnCrop, + blnPreciseAV, + audioDecoder); + + _decoder = _decoderDbl = new MdecDecoder_double(new StephensIDCT(), getCroppedWidth(), getCroppedHeight()); + + _yuvImgBuff = new Yuv4mpeg2(getWidth(), getHeight()); + + _writerYuv = new AviWriterYV12(new File(getOutputFile()), + _vidItem.getWidth(), _vidItem.getHeight(), + _vidSync.getFpsNum(), + _vidSync.getFpsDenom(), + audioDecoder == null ? null : audioDecoder.getOutputFormat()); + + super._aviWriter = _writerYuv; + } + + @Override + protected void actuallyWrite(MdecDecoder decoder, int iFrame) throws IOException { + _decoderDbl.readDecodedYuv4mpeg2(_yuvImgBuff); + _writerYuv.write(_yuvImgBuff.getY(), _yuvImgBuff.getCr(), _yuvImgBuff.getCb()); + } + + @Override + protected void writeError(Throwable ex) throws IOException { + BufferedImage bi = makeErrorImage(ex, _writerYuv.getWidth(), _writerYuv.getHeight()); + Yuv4mpeg2 yuv = new Yuv4mpeg2(bi); + _writerYuv.write(yuv.getY(), yuv.getCr(), yuv.getCb()); + } + + } + +} \ No newline at end of file diff --git a/src/jpsxdec/plugins/psx/str/SectorSTR.java b/src/jpsxdec/plugins/psx/str/SectorSTR.java index 7a121b1..f7eed81 100644 --- a/src/jpsxdec/plugins/psx/str/SectorSTR.java +++ b/src/jpsxdec/plugins/psx/str/SectorSTR.java @@ -96,6 +96,7 @@ public SectorSTR(CDSector cdSector) throws NotThisTypeException { } } + /** Child classes override this method so most of this class can be reused. */ protected void readHeader(ByteArrayInputStream inStream) throws NotThisTypeException, IOException { @@ -135,6 +136,8 @@ protected void readHeader(ByteArrayInputStream inStream) if (_iQuantizationScale < 1) throw new NotThisTypeException(); _lngVersion = IO.readUInt16LE(inStream); + if (_lngVersion != 2 && _lngVersion != 3) + throw new NotThisTypeException(); _lngFourZeros = IO.readUInt32LE(inStream); } @@ -142,9 +145,9 @@ protected void readHeader(ByteArrayInputStream inStream) public String toString() { return String.format("%s %s frame:%d chunk:%d/%d %dx%d ver:%d " + - "{demux frame size?=%d rlc=%d 3800=%04x qscale=%d 4*00=%08x}", + "{demux frame size=%d rlc=%d 3800=%04x qscale=%d 4*00=%08x}", getTypeName(), - super.toString(), + super.cdToString(), _iFrameNumber, _iChunkNumber, _iChunksInThisFrame, @@ -203,50 +206,56 @@ public String getTypeName() { } public boolean matchesPrevious(IVideoSector prevSector) { - if (!(prevSector instanceof SectorSTR)) + if (!(prevSector.getClass().equals(prevSector.getClass()))) return false; - SectorSTR strSector = (SectorSTR)prevSector; + if (prevSector.getFrameNumber() == getFrameNumber() && + prevSector.getChunksInFrame() != getChunksInFrame()) + return false; - if (getWidth() != strSector.getWidth() || - getHeight() != strSector.getHeight()) + if (getWidth() != prevSector.getWidth() || + getHeight() != prevSector.getHeight()) return false; - long iNextChunk = strSector.getChunkNumber() + 1; - long iNextFrame = strSector.getFrameNumber(); - if (iNextChunk >= strSector.getChunksInFrame()) { + /* This logic is accurate, but not forgiving at all + long iNextChunk = prevSector.getChunkNumber() + 1; + long iNextFrame = prevSector.getFrameNumber(); + if (iNextChunk >= prevSector.getChunksInFrame()) { iNextChunk = 0; iNextFrame++; } if (iNextChunk != getChunkNumber() || iNextFrame != getFrameNumber()) return false; - - if (strSector.getFrameNumber() == getFrameNumber() && - strSector.getChunksInFrame() != getChunksInFrame()) + */ + + // softer logic + if (prevSector.getFrameNumber() == getFrameNumber()) + return true; + else if (prevSector.getFrameNumber() == getFrameNumber() - 1) + return true; + else return false; - - return true; } - public DiscItem createMedia(int iStartSector, int iStartFrame, int iFrame1End) + public DiscItem createMedia(int iStartSector, int iStartFrame, int iFrame1LastSector) { - int iSectors = getSectorNumber() - iStartSector; - int iFrames = getFrameNumber() - iStartFrame; + int iSectors = getSectorNumber() - iStartSector + 1; + int iFrames = getFrameNumber() - iStartFrame + 1; return createMedia(iStartSector, iStartFrame, - iFrame1End, + iFrame1LastSector, iSectors, iFrames); } public DiscItem createMedia(int iStartSector, int iStartFrame, - int iFrame1End, + int iFrame1LastSector, int iSectors, int iPerFrame) { return new DiscItemSTRVideo(iStartSector, getSectorNumber(), iStartFrame, getFrameNumber(), getWidth(), getHeight(), iSectors, iPerFrame, - iFrame1End); + iFrame1LastSector); } public JPSXPlugin getSourcePlugin() { diff --git a/src/jpsxdec/plugins/psx/str/VideoSync.java b/src/jpsxdec/plugins/psx/str/VideoSync.java new file mode 100644 index 0000000..38b1c9c --- /dev/null +++ b/src/jpsxdec/plugins/psx/str/VideoSync.java @@ -0,0 +1,72 @@ +package jpsxdec.plugins.psx.str; + +import java.util.logging.Logger; +import jpsxdec.util.Fraction; + +public class VideoSync { + + private static final Logger log = Logger.getLogger(VideoSync.class.getName()); + + private final int _iSectorsPerSecond; + private final Fraction _sectorsPerFrame; + private final Fraction _secondsPerFrame; + private final int _iFirstPresentationSector; + + public VideoSync(int iFirstPresentationSector, + int iSectorsPerSecond, + Fraction sectorsPerFrame) + { + _iFirstPresentationSector = iFirstPresentationSector; + _iSectorsPerSecond = iSectorsPerSecond; + _sectorsPerFrame = sectorsPerFrame; + + _secondsPerFrame = _sectorsPerFrame.divide(_iSectorsPerSecond); + } + + public Fraction getSecondsPerFrame() { + return _secondsPerFrame; + } + + public Fraction getSectorsPerFrame() { + return _sectorsPerFrame; + } + + public int getSectorsPerSecond() { + return _iSectorsPerSecond; + } + + private static Fraction Point5 = new Fraction(1, 2); + private static Fraction NegPoint5 = Point5.negative(); + + public int calculateFramesToCatchup(int iSector, long lngFramesWritten) { + + Fraction presentationTime = new Fraction(iSector - _iFirstPresentationSector, _iSectorsPerSecond); + Fraction movieTime = _secondsPerFrame.multiply(lngFramesWritten); + Fraction timeDiff = presentationTime.subtract(movieTime); + Fraction framesDiff = timeDiff.divide(_secondsPerFrame); + + int iFrameCatchupNeeded = 0; + + if (framesDiff.compareTo(NegPoint5) > 0) { // presentation time is equal, or ahead of movie time + iFrameCatchupNeeded = (int)Math.round(framesDiff.asDouble()); + } else { // movie time is ahead of disc time + log.warning(String.format("Frame is written %1.3f seconds ahead.", -timeDiff.asDouble())); + // return the negative number + iFrameCatchupNeeded = (int)Math.round(framesDiff.asDouble()); + } + + return iFrameCatchupNeeded; + } + + public int getInitialVideo() { + return 0; + } + + public long getFpsNum() { + return getSecondsPerFrame().getDenominator(); + } + + public long getFpsDenom() { + return getSecondsPerFrame().getNumerator(); + } +} diff --git a/src/jpsxdec/plugins/psx/video/DemuxImage.java b/src/jpsxdec/plugins/psx/video/DemuxImage.java index 798eed3..56b5931 100644 --- a/src/jpsxdec/plugins/psx/video/DemuxImage.java +++ b/src/jpsxdec/plugins/psx/video/DemuxImage.java @@ -46,72 +46,70 @@ public class DemuxImage { - private final int m_iWidth, m_iHeight, m_iSourceFrame; - private final byte[] m_abData; + private final int _iWidth, _iHeight, _iSourceFrame; + private final byte[] _abData; public DemuxImage(int iWidth, int iHeight, int iFrame, File oSource) throws IOException { - m_iWidth = iWidth; - m_iHeight= iHeight; - m_iSourceFrame = iFrame; - m_abData = IO.readFile(oSource); + _iWidth = iWidth; + _iHeight= iHeight; + _iSourceFrame = iFrame; + _abData = IO.readFile(oSource); } public DemuxImage(int iWidth, int iHeight, File oSource) throws IOException { this(iWidth, iHeight, -1, oSource); } - public DemuxImage(int iWidth, int iHeight, int iFrame, byte[] oSource) { - this(iWidth, iHeight, iFrame, oSource, 0, oSource.length); - } + public DemuxImage(int iWidth, int iHeight, int iFrame, byte[] abDemuxBuffer) { + _iWidth = iWidth; + _iHeight= iHeight; + _iSourceFrame = iFrame; - public DemuxImage(int iWidth, int iHeight, int iFrame, byte[] oSource, int iStart, int iLen) { - m_iWidth = iWidth; - m_iHeight= iHeight; - m_iSourceFrame = iFrame; - m_abData = Misc.copyOfRange(oSource, iStart, iLen); + _abData = abDemuxBuffer; } + public DemuxImage(int iWidth, int iHeight, int iFrame, InputStream oIS, int iExpectedDataSize) throws IOException { - m_iWidth = iWidth; - m_iHeight= iHeight; - m_iSourceFrame = iFrame; + _iWidth = iWidth; + _iHeight= iHeight; + _iSourceFrame = iFrame; // copy as much from the stream as possible - m_abData = new byte[iExpectedDataSize]; - int pos = oIS.read(m_abData); + _abData = new byte[iExpectedDataSize]; + int pos = oIS.read(_abData); while (pos < iExpectedDataSize) { - int i = oIS.read(m_abData, pos, iExpectedDataSize - pos); + int i = oIS.read(_abData, pos, iExpectedDataSize - pos); if (i < 0) break; pos += i; } } public int getWidth() { - return m_iWidth; + return _iWidth; } public int getHeight() { - return m_iHeight; + return _iHeight; } public int getActualWidth() { - return (m_iWidth + 15) & (~15); + return (_iWidth + 15) & (~15); } public int getActualHeight() { - return (m_iHeight + 15) & (~15); + return (_iHeight + 15) & (~15); } /** Size of the underlying demux data (currently equal to the array size). */ public int getBufferSize() { - return m_abData.length; + return _abData.length; } public int getFrameNumber() { - return m_iSourceFrame; + return _iSourceFrame; } public byte[] getData() { - return m_abData; + return _abData; } } diff --git a/src/jpsxdec/plugins/psx/video/decode/ArrayBitReader.java b/src/jpsxdec/plugins/psx/video/decode/ArrayBitReader.java index 3f64de2..1972fe6 100644 --- a/src/jpsxdec/plugins/psx/video/decode/ArrayBitReader.java +++ b/src/jpsxdec/plugins/psx/video/decode/ArrayBitReader.java @@ -60,7 +60,11 @@ public class ArrayBitReader { 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF }; - + + public ArrayBitReader() { + + } + /** Start reading from the start of the array as little-endian. */ public ArrayBitReader(byte[] abData) { this(abData, true, 0); diff --git a/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor.java b/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor.java index 5f479a1..0bc05b7 100644 --- a/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor.java +++ b/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor.java @@ -44,6 +44,6 @@ public abstract class DemuxFrameUncompressor extends MdecInputStream { /** Resets this instance as if a new object was created. */ - public abstract void reset(byte[] abDemuxData) throws NotThisTypeException; + public abstract void reset(byte[] abDemuxData, int iStart) throws NotThisTypeException; } diff --git a/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_FF7.java b/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_FF7.java index 7de405a..3ea8089 100644 --- a/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_FF7.java +++ b/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_FF7.java @@ -56,63 +56,31 @@ public class DemuxFrameUncompressor_FF7 extends DemuxFrameUncompressor_STRv2 { public DemuxFrameUncompressor_FF7() { super(); } - public DemuxFrameUncompressor_FF7(byte[] abDemuxData) throws NotThisTypeException { - super(abDemuxData); + public DemuxFrameUncompressor_FF7(byte[] abDemuxData, int iStart) throws NotThisTypeException { + super(abDemuxData, iStart); } - private boolean _blnHasCameraData; - - public byte[] getCameraData() { - if (_blnHasCameraData) { - return Misc.copyOfRange(_bitReader.getArray(), 0, 40); - } else { - return null; - } - } - - public long getMagic3800() { - return _lngMagic3800; - } - - public int getHalfVlcCountCeil32() { - return _iHalfVlcCountCeil32; - } - - @Override - protected ArrayBitReader readHeader(byte[] abFrameData) throws NotThisTypeException { - int iStartOffset = 0; - _lngMagic3800 = IO.readUInt16LE(abFrameData, 2); - - _blnHasCameraData = _lngMagic3800 != 0x3800; - if (_blnHasCameraData) { - iStartOffset = 40; - _lngMagic3800 = IO.readUInt16LE(abFrameData, iStartOffset + 2); - } + protected void readHeader(byte[] abFrameData, int iStart, ArrayBitReader bitReader) throws NotThisTypeException { - _iHalfVlcCountCeil32 = IO.readSInt16LE(abFrameData, iStartOffset + 0); - _iQscale = IO.readSInt16LE(abFrameData, iStartOffset + 4); - int iVersion = IO.readSInt16LE(abFrameData, iStartOffset + 6); + _iHalfVlcCountCeil32 = IO.readSInt16LE(abFrameData, iStart+0); + _lngMagic3800 = IO.readUInt16LE(abFrameData, iStart+2); + _iQscale = IO.readSInt16LE(abFrameData, iStart+4); + int iVersion = IO.readSInt16LE(abFrameData, iStart+6); if (_lngMagic3800 != 0x3800 || _iQscale < 1 || iVersion != 1 || _iHalfVlcCountCeil32 < 0) throw new NotThisTypeException(); - return new ArrayBitReader(abFrameData, true, iStartOffset + 8); + bitReader.reset(abFrameData, true, iStart+8); } public static boolean checkHeader(byte[] abFrameData) { - int iStartOffset = 0; - long lngMagic3800 = IO.readUInt16LE(abFrameData, 2); - - if (lngMagic3800 != 0x3800) { - iStartOffset = 40; - lngMagic3800 = IO.readUInt16LE(abFrameData, iStartOffset + 2); - } - int iHalfVlcCountCeil32 = IO.readSInt16LE(abFrameData, iStartOffset + 0); - int iQscale = IO.readSInt16LE(abFrameData, iStartOffset + 4); - int iVersion = IO.readSInt16LE(abFrameData, iStartOffset + 6); + int iHalfVlcCountCeil32 = IO.readSInt16LE(abFrameData, 0); + long lngMagic3800 = IO.readUInt16LE(abFrameData, 2); + int iQscale = IO.readSInt16LE(abFrameData, 4); + int iVersion = IO.readSInt16LE(abFrameData, 6); return !(lngMagic3800 != 0x3800 || iQscale < 1 || iVersion != 1 || iHalfVlcCountCeil32 < 0); @@ -126,11 +94,7 @@ public String toString() { public static class Recompressor_FF7 extends DemuxFrameUncompressor_STRv2.FrameRecompressor_STRv2 { - public void compressToDemuxFF7(BitStreamWriter oBitStream, int iQscale, int iVlcCount) throws IOException { - compressToDemuxFF7(oBitStream, iQscale, iVlcCount, null); - } - - public void compressToDemuxFF7(BitStreamWriter bitStream, int iQscale, int iVlcCount, byte[] abCameraData) + public void compressToDemuxFF7(BitStreamWriter bitStream, int iQscale, int iVlcCount) throws IOException { if (iQscale < 1 || iVlcCount < 0) throw new IllegalArgumentException(); @@ -138,10 +102,6 @@ public void compressToDemuxFF7(BitStreamWriter bitStream, int iQscale, int iVlcC _iVlcCount = iVlcCount; _bitStream = bitStream; - if (abCameraData != null) { - _bitStream.setLittleEndian(false); - _bitStream.write(abCameraData); - } _bitStream.setLittleEndian(true); _bitStream.writeInt16LE( (((iVlcCount+1) / 2) + 31) & ~31 ); _bitStream.writeInt16LE(0x3800); diff --git a/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_Lain.java b/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_Lain.java index d203018..52df2e4 100644 --- a/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_Lain.java +++ b/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_Lain.java @@ -181,18 +181,18 @@ public DemuxFrameUncompressor_Lain() { super(); _aoVarLenCodes = AC_VARIABLE_LENGTH_CODES_LAIN; } - public DemuxFrameUncompressor_Lain(byte[] abDemuxData) throws NotThisTypeException { - super(abDemuxData); + public DemuxFrameUncompressor_Lain(byte[] abDemuxData, int iStart) throws NotThisTypeException { + super(abDemuxData, iStart); _aoVarLenCodes = AC_VARIABLE_LENGTH_CODES_LAIN; } @Override - protected ArrayBitReader readHeader(byte[] abFrameData) throws NotThisTypeException { - _iQscaleLumin = abFrameData[0]; - _iQscaleChrom = abFrameData[1]; - _lngMagic3800 = IO.readUInt16LE(abFrameData, 2); - _iVlcCount = IO.readSInt16LE(abFrameData, 4); - int iVersion = IO.readSInt16LE(abFrameData, 6); + protected void readHeader(byte[] abFrameData, int iStart, ArrayBitReader bitReader) throws NotThisTypeException { + _iQscaleLumin = abFrameData[iStart+0]; + _iQscaleChrom = abFrameData[iStart+1]; + _lngMagic3800 = IO.readUInt16LE(abFrameData, iStart+2); + _iVlcCount = IO.readSInt16LE(abFrameData, iStart+4); + int iVersion = IO.readSInt16LE(abFrameData, iStart+6); if (_iQscaleChrom < 1 || _iQscaleLumin < 1 || iVersion != 0 || _iVlcCount < 1) @@ -202,7 +202,7 @@ protected ArrayBitReader readHeader(byte[] abFrameData) throws NotThisTypeExcept if (_lngMagic3800 != 0x3800 && (_lngMagic3800 < 0 || _lngMagic3800 > 4765)) throw new NotThisTypeException(); - return new ArrayBitReader(abFrameData, false, 8); + bitReader.reset(abFrameData, false, iStart+8); } public static boolean checkHeader(byte[] abFrameData) { diff --git a/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_STRv2.java b/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_STRv2.java index 1e2f8a9..ae02155 100644 --- a/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_STRv2.java +++ b/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_STRv2.java @@ -260,7 +260,7 @@ public void codeRead(long lngNextCodePos, String s, int bits6, int bits10) { protected long _lngMagic3800; protected int _iHalfVlcCountCeil32; - protected ArrayBitReader _bitReader; + protected ArrayBitReader _bitReader = new ArrayBitReader(); protected MdecDebugger _debug; @@ -275,9 +275,9 @@ public DemuxFrameUncompressor_STRv2() { //_debug = new MdecDebugger(); } - public DemuxFrameUncompressor_STRv2(byte[] abDemuxData) throws NotThisTypeException { + public DemuxFrameUncompressor_STRv2(byte[] abDemuxData, int iStart) throws NotThisTypeException { this(); - reset(abDemuxData); + reset(abDemuxData, iStart); } /* ---------------------------------------------------------------------- */ @@ -308,9 +308,9 @@ public int getMacroBlock() { /* ---------------------------------------------------------------------- */ - public void reset(byte[] abDemuxData) throws NotThisTypeException + public void reset(byte[] abDemuxData, int iStart) throws NotThisTypeException { - _bitReader = readHeader(abDemuxData); + readHeader(abDemuxData, iStart, _bitReader); _iCurrentMacroBlock = 0; _iCurrentBlock = 0; _blnStartOfBlock = true; @@ -318,17 +318,19 @@ public void reset(byte[] abDemuxData) throws NotThisTypeException } - protected ArrayBitReader readHeader(byte[] abFrameData) throws NotThisTypeException { - _iHalfVlcCountCeil32 = IO.readSInt16LE(abFrameData, 0); - _lngMagic3800 = IO.readUInt16LE(abFrameData, 2); - _iQscale = IO.readSInt16LE(abFrameData, 4); - int iVersion = IO.readSInt16LE(abFrameData, 6); + protected void readHeader(byte[] abFrameData, int iStart, ArrayBitReader bitReader) throws NotThisTypeException { + if (iStart != 0) + throw new RuntimeException(); + _iHalfVlcCountCeil32 = IO.readSInt16LE(abFrameData, iStart+0); + _lngMagic3800 = IO.readUInt16LE(abFrameData, iStart+2); + _iQscale = IO.readSInt16LE(abFrameData, iStart+4); + int iVersion = IO.readSInt16LE(abFrameData, iStart+6); if (_lngMagic3800 != 0x3800 || _iQscale < 1 || iVersion != 2 || _iHalfVlcCountCeil32 < 0) throw new NotThisTypeException(); - return new ArrayBitReader(abFrameData, true, 8); + bitReader.reset(abFrameData, true, iStart+8); } public static boolean checkHeader(byte[] abFrameData) { diff --git a/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_STRv3.java b/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_STRv3.java index d1d331b..69257fc 100644 --- a/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_STRv3.java +++ b/src/jpsxdec/plugins/psx/video/decode/DemuxFrameUncompressor_STRv3.java @@ -115,8 +115,8 @@ public DemuxFrameUncompressor_STRv3() { super(); } - public DemuxFrameUncompressor_STRv3(byte[] abDemuxData) throws NotThisTypeException { - super(abDemuxData); + public DemuxFrameUncompressor_STRv3(byte[] abDemuxData, int iStart) throws NotThisTypeException { + super(abDemuxData, iStart); } // Holds the previous DC values for ver 3 frames. @@ -125,22 +125,22 @@ public DemuxFrameUncompressor_STRv3(byte[] abDemuxData) throws NotThisTypeExcept private int _iPreviousY_DC; @Override - public void reset(byte[] abDemuxData) throws NotThisTypeException { + public void reset(byte[] abDemuxData, int iStart) throws NotThisTypeException { _iPreviousCr_DC = _iPreviousCb_DC = _iPreviousY_DC = 0; - super.reset(abDemuxData); + super.reset(abDemuxData, iStart); } @Override - protected ArrayBitReader readHeader(byte[] abFrameData) throws NotThisTypeException { - _iHalfVlcCountCeil32 = IO.readSInt16LE(abFrameData, 0); - _lngMagic3800 = IO.readUInt16LE(abFrameData, 2); - _iQscale = IO.readSInt16LE(abFrameData, 4); - int iVersion = IO.readSInt16LE(abFrameData, 6); + protected void readHeader(byte[] abFrameData, int iStart, ArrayBitReader bitReader) throws NotThisTypeException { + _iHalfVlcCountCeil32 = IO.readSInt16LE(abFrameData, iStart+0); + _lngMagic3800 = IO.readUInt16LE(abFrameData, iStart+2); + _iQscale = IO.readSInt16LE(abFrameData, iStart+4); + int iVersion = IO.readSInt16LE(abFrameData, iStart+6); if (_lngMagic3800 != 0x3800 || _iQscale < 1 || iVersion != 3) throw new NotThisTypeException(); - return new ArrayBitReader(abFrameData, true, 8); + bitReader.reset(abFrameData, true, iStart+8); } public static boolean checkHeader(byte[] abFrameData) { diff --git a/src/jpsxdec/plugins/psx/video/encode/DemuxQualityEditor.java b/src/jpsxdec/plugins/psx/video/encode/DemuxQualityEditor.java index 84899ef..8245183 100644 --- a/src/jpsxdec/plugins/psx/video/encode/DemuxQualityEditor.java +++ b/src/jpsxdec/plugins/psx/video/encode/DemuxQualityEditor.java @@ -71,7 +71,6 @@ public class DemuxQualityEditor extends javax.swing.JFrame { private int m_iWidth; private int m_iHeight; private MdecDecoder_double m_oMacBlockDecoder = new MdecDecoder_double(new StephensIDCT(), 16, 16); - private byte[] m_abCameraData; private int m_iOriginalDemuxSize; private int m_iOriginalMdecCount; @@ -351,15 +350,13 @@ private void m_guiOpenActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIR m_oUncompressedImage = new ParsedMdecImage(m_iWidth, m_iHeight); if (FRAME_TYPE == FF7_FRAME) { - DemuxFrameUncompressor_FF7 oUncompressor = new DemuxFrameUncompressor_FF7(oDemux.getData()); + DemuxFrameUncompressor_FF7 oUncompressor = new DemuxFrameUncompressor_FF7(oDemux.getData(), 0); m_oUncompressedImage.readFrom(oUncompressor); int iQscale = m_oUncompressedImage.getMacroBlock(0, 0).Y1.getQscale(); m_guiQscaleSpin.setValue(iQscale); - - m_abCameraData = oUncompressor.getCameraData(); } else if (FRAME_TYPE == STRV2_FRAME) { - DemuxFrameUncompressor_STRv2 oUncompressor = new DemuxFrameUncompressor_STRv2(oDemux.getData()); + DemuxFrameUncompressor_STRv2 oUncompressor = new DemuxFrameUncompressor_STRv2(oDemux.getData(), 0); m_oUncompressedImage.readFrom(oUncompressor); int iQscale = m_oUncompressedImage.getMacroBlock(0, 0).Y1.getQscale(); @@ -410,7 +407,7 @@ private int getDemuxSize() throws IOException, UncompressionException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); BitStreamWriter oBitWriter = new BitStreamWriter(baos); - oRecompressor.compressToDemuxFF7(oBitWriter, iQscale, m_oUncompressedImage.getRunLengthCodeCount(), m_abCameraData); + oRecompressor.compressToDemuxFF7(oBitWriter, iQscale, m_oUncompressedImage.getRunLengthCodeCount()); oRecompressor.write(m_oUncompressedImage.getStream()); oBitWriter.close(); return baos.size(); @@ -419,7 +416,7 @@ private int getDemuxSize() throws IOException, UncompressionException { private void updateBlocks() { if (m_guiImage.getHighlightX() < 0) return; MdecInputStream oMBlk = m_oUncompressedImage.getStream(m_guiImage.getHighlightX(), - m_guiImage.getHighlightY()); + m_guiImage.getHighlightY()); try { m_oMacBlockDecoder.decode(oMBlk); } catch (UncompressionException ex) { diff --git a/src/jpsxdec/plugins/psx/video/encode/MdecEncoder.java b/src/jpsxdec/plugins/psx/video/encode/MdecEncoder.java index 29f1005..7887739 100644 --- a/src/jpsxdec/plugins/psx/video/encode/MdecEncoder.java +++ b/src/jpsxdec/plugins/psx/video/encode/MdecEncoder.java @@ -280,7 +280,7 @@ public static void main(String args[]) throws Throwable { bsw.close(); IO.writeFile(String.format("newsteve%dx%d-v2.demux", oYuv.getLuminWidth(), oYuv.getLuminHeight()), baos.toByteArray()); - DemuxFrameUncompressor uncompressor = new DemuxFrameUncompressor_STRv2(baos.toByteArray()); + DemuxFrameUncompressor uncompressor = new DemuxFrameUncompressor_STRv2(baos.toByteArray(), 0); MdecDecoder_double decoder = new MdecDecoder_double(new StephensIDCT(), bi.getWidth(), bi.getHeight()); decoder.decode(uncompressor); RgbIntImage oRgb = new RgbIntImage(bi.getWidth(), bi.getHeight()); @@ -291,7 +291,7 @@ public static void main(String args[]) throws Throwable { private static PsxYuvImage convert() throws Throwable { DemuxImage oDemux = new DemuxImage(320, 160, new File("abc000[0]0300.demux")); - DemuxFrameUncompressor_STRv3 oUncompress = new DemuxFrameUncompressor_STRv3(oDemux.getData()); + DemuxFrameUncompressor_STRv3 oUncompress = new DemuxFrameUncompressor_STRv3(oDemux.getData(), 0); MdecDecoder_double decoder = new MdecDecoder_double(new StephensIDCT(), oDemux.getWidth(), oDemux.getHeight()); decoder.decode(oUncompress); RgbIntImage rgb = new RgbIntImage(oDemux.getWidth(), oDemux.getHeight()); diff --git a/src/jpsxdec/plugins/psx/video/encode/STRFrameReplacer.java b/src/jpsxdec/plugins/psx/video/encode/STRFrameReplacer.java index 069586d..4f8c58c 100644 --- a/src/jpsxdec/plugins/psx/video/encode/STRFrameReplacer.java +++ b/src/jpsxdec/plugins/psx/video/encode/STRFrameReplacer.java @@ -58,8 +58,7 @@ public class STRFrameReplacer { public STRFrameReplacer(String sDemuxFilesFolder, String sOutputCD) throws Throwable { CD = new CDFileSectorReader(sOutputCD, true); - DiscIndex index = new DiscIndex(CD); - index.indexDisc(new ConsoleProgressListener(new FeedbackStream(System.out, FeedbackStream.NORM))); + DiscIndex index = new DiscIndex(CD, new ConsoleProgressListener(new FeedbackStream(System.out, FeedbackStream.NORM))); DiscItem item = index.getByIndex(0); DiscItemSTRVideo oVidItem = (DiscItemSTRVideo) item; diff --git a/src/jpsxdec/plugins/psx/video/encode/STRRecompressor.java b/src/jpsxdec/plugins/psx/video/encode/STRRecompressor.java index c043ec7..9361c6b 100644 --- a/src/jpsxdec/plugins/psx/video/encode/STRRecompressor.java +++ b/src/jpsxdec/plugins/psx/video/encode/STRRecompressor.java @@ -117,9 +117,8 @@ private void doFrame( ParsedMdecImage oSrcUncompress = new ParsedMdecImage(WIDTH, HEIGHT); ParsedMdecImage oModUncompress = new ParsedMdecImage(WIDTH, HEIGHT); - UNCOMPRESSOR_FF7.reset(oSrcDemux.getData()); + UNCOMPRESSOR_FF7.reset(oSrcDemux.getData(), 0); oSrcUncompress.readFrom(UNCOMPRESSOR_FF7); - byte[] abCameraData = UNCOMPRESSOR_FF7.getCameraData(); int iSrcDemuxSize = UNCOMPRESSOR_FF7.getPosition(); System.out.println(" Original file MAX demux size: " + oSrcDemux.getBufferSize()); @@ -142,7 +141,7 @@ private void doFrame( copyMacroBlock(oSrcMacBlk, oModMacBlk); } - int iMergedDemuxSize = writeMerged(oSrcUncompress, iFile, abCameraData); + int iMergedDemuxSize = writeMerged(oSrcUncompress, iFile); if (iMergedDemuxSize <= oSrcDemux.getBufferSize()) { System.out.format(" Merged file %d demux size %d <= max source %d ", iFile, iMergedDemuxSize, oSrcDemux.getBufferSize()); System.out.println(); @@ -170,7 +169,7 @@ private void copyMacroBlock(MacroBlock oSrcMacBlk, MacroBlock oModMacBlk) { } } - private int writeMerged(ParsedMdecImage oMerged, int iFile, byte[] abCameraData) throws Throwable { + private int writeMerged(ParsedMdecImage oMerged, int iFile) throws Throwable { System.out.println(" Merged frame MDEC count: " + oMerged.getRunLengthCodeCount()); String sFile = String.format("merged%04d.demux", iFile); System.out.println(" Writing merged file " + sFile); @@ -182,7 +181,7 @@ private int writeMerged(ParsedMdecImage oMerged, int iFile, byte[] abCameraData) int iQscale = oMerged.getMacroBlock(0, 0).getBlock(0).getQscale(); DemuxFrameUncompressor_FF7.Recompressor_FF7 oRecompressor = new DemuxFrameUncompressor_FF7.Recompressor_FF7(); - oRecompressor.compressToDemuxFF7(bsw, iQscale, oMerged.getRunLengthCodeCount(), abCameraData); + oRecompressor.compressToDemuxFF7(bsw, iQscale, oMerged.getRunLengthCodeCount()); oRecompressor.write(oMerged.getStream()); bsw.close(); return (int)oOutputFile.length(); diff --git a/src/jpsxdec/plugins/psx/video/mdec/MdecDecoder_double.java b/src/jpsxdec/plugins/psx/video/mdec/MdecDecoder_double.java index 2e81843..d437dbd 100644 --- a/src/jpsxdec/plugins/psx/video/mdec/MdecDecoder_double.java +++ b/src/jpsxdec/plugins/psx/video/mdec/MdecDecoder_double.java @@ -93,6 +93,7 @@ public class MdecDecoder_double extends MdecDecoder { protected final double[] _LuminBuffer; protected final double[] _CurrentBlock = new double[64]; + protected final MdecInputStream.MdecCode _code = new MdecInputStream.MdecCode(); protected int _iMacBlockWidth; protected int _iMacBlockHeight; @@ -120,8 +121,6 @@ public void decode(MdecInputStream mdecInStream) int iCurrentBlockNonZeroCount; int iCurrentBlockLastNonZeroPosition; - MdecInputStream.MdecCode code = new MdecInputStream.MdecCode(); - int iMacBlk = 0, iBlock = 0; try { @@ -139,36 +138,36 @@ public void decode(MdecInputStream mdecInStream) for (iBlock = 0; iBlock < 6; iBlock++) { Arrays.fill(_CurrentBlock, 0); - mdecInStream.readMdecCode(code); + mdecInStream.readMdecCode(_code); if (log().isLoggable(Level.FINEST)) - log().finest("Qscale & DC " + code); + log().finest("Qscale & DC " + _code); - if (code.Bottom10Bits != 0) { + if (_code.Bottom10Bits != 0) { _CurrentBlock[0] = - code.Bottom10Bits * PSX_DEFAULT_INTRA_QUANTIZATION_MATRIX[0]; + _code.Bottom10Bits * PSX_DEFAULT_INTRA_QUANTIZATION_MATRIX[0]; iCurrentBlockNonZeroCount = 1; iCurrentBlockLastNonZeroPosition = 0; } else { iCurrentBlockNonZeroCount = 0; iCurrentBlockLastNonZeroPosition = -1; } - iCurrentBlockQscale = code.Top6Bits; + iCurrentBlockQscale = _code.Top6Bits; iCurrentBlockVectorPosition = 0; - while (!mdecInStream.readMdecCode(code)) { + while (!mdecInStream.readMdecCode(_code)) { if (log().isLoggable(Level.FINEST)) - log().finest(code.toString()); + log().finest(_code.toString()); //////////////////////////////////////////////////////// - iCurrentBlockVectorPosition += code.Top6Bits + 1; + iCurrentBlockVectorPosition += _code.Top6Bits + 1; try { // Reverse Zig-Zag and Dequantize all at the same time int iRevZigZagPos = MdecInputStream.REVERSE_ZIG_ZAG_SCAN_MATRIX[iCurrentBlockVectorPosition]; _CurrentBlock[iRevZigZagPos] = - (code.Bottom10Bits + (_code.Bottom10Bits * PSX_DEFAULT_INTRA_QUANTIZATION_MATRIX[iRevZigZagPos] * iCurrentBlockQscale) / 8.0; iCurrentBlockNonZeroCount++; @@ -183,7 +182,7 @@ public void decode(MdecInputStream mdecInStream) } if (log().isLoggable(Level.FINEST)) - log().finest(code.toString()); + log().finest(_code.toString()); writeEndOfBlock(iMacBlk, iBlock, iCurrentBlockNonZeroCount, @@ -275,9 +274,9 @@ public void readDecodedRGB(RgbIntImage rgb) { if ((WIDTH % 16) != 0) throw new IllegalArgumentException("Image width must be multiple of 16."); + // TODO: add handling for widths not divisible by 16 if ((HEIGHT % 2) != 0) throw new IllegalArgumentException("Image height must be multiple of 2."); - // TODO: add handling for heights not divisible by 16 double Cr, Cb, ChromRed, ChromGreen, ChromBlue; @@ -375,17 +374,17 @@ public void readDecodedYuv4mpeg2(Yuv4mpeg2 yuv) { dblCr = _CrBuffer[iChromOfs], dblY; - yuv.setCb( (iX+iCx)/2 , (iY+iCy)/2 , psxChrom_to_y4mCb(dblCb, dblCr)); - yuv.setCr( (iX+iCx)/2 , (iY+iCy)/2 , psxChrom_to_y4mCr(dblCb, dblCr)); + yuv.setCb( (iX+iCx)/2 , (iY+iCy)/2 , shiftClamp(psxChrom_to_y4mCb(dblCb, dblCr))); + yuv.setCr( (iX+iCx)/2 , (iY+iCy)/2 , shiftClamp(psxChrom_to_y4mCr(dblCb, dblCr))); dblY = _LuminBuffer[iLuminOfs + aiLuminIdxs.TL]; - yuv.setY( iX+iCx+0 , iY+iCy+0 , psxYuv_to_y4mLumin(dblY, dblCb, dblCr) ); + yuv.setY( iX+iCx+0 , iY+iCy+0 , shiftClamp(psxYuv_to_y4mLumin(dblY, dblCb, dblCr) )); dblY = _LuminBuffer[iLuminOfs + aiLuminIdxs.TR]; - yuv.setY( iX+iCx+1 , iY+iCy+0 , psxYuv_to_y4mLumin(dblY, dblCb, dblCr) ); - dblY = _LuminBuffer[iLuminOfs + aiLuminIdxs.BR]; - yuv.setY( iX+iCx+0 , iY+iCy+1 , psxYuv_to_y4mLumin(dblY, dblCb, dblCr) ); + yuv.setY( iX+iCx+1 , iY+iCy+0 , shiftClamp(psxYuv_to_y4mLumin(dblY, dblCb, dblCr) )); dblY = _LuminBuffer[iLuminOfs + aiLuminIdxs.BL]; - yuv.setY( iX+iCx+1 , iY+iCy+1 , psxYuv_to_y4mLumin(dblY, dblCb, dblCr) ); + yuv.setY( iX+iCx+0 , iY+iCy+1 , shiftClamp(psxYuv_to_y4mLumin(dblY, dblCb, dblCr) )); + dblY = _LuminBuffer[iLuminOfs + aiLuminIdxs.BR]; + yuv.setY( iX+iCx+1 , iY+iCy+1 , shiftClamp(psxYuv_to_y4mLumin(dblY, dblCb, dblCr) )); iChromOfs++; } @@ -396,5 +395,15 @@ public void readDecodedYuv4mpeg2(Yuv4mpeg2 yuv) { } } + private static byte shiftClamp(double dbl) { + long lng = Math.round(dbl + 128); + if (lng < 0) + return (byte)0; + else if (lng > 255) + return (byte)255; + else + return (byte)(lng); + } + } diff --git a/src/jpsxdec/plugins/psx/video/mdec/MdecDecoder_int.java b/src/jpsxdec/plugins/psx/video/mdec/MdecDecoder_int.java index 561a5f6..b9588d2 100644 --- a/src/jpsxdec/plugins/psx/video/mdec/MdecDecoder_int.java +++ b/src/jpsxdec/plugins/psx/video/mdec/MdecDecoder_int.java @@ -64,6 +64,7 @@ public class MdecDecoder_int extends MdecDecoder { protected final int[] _LuminBuffer; protected final int[] _CurrentBlock = new int[64]; + protected final MdecInputStream.MdecCode _code = new MdecInputStream.MdecCode(); protected int _iMacBlockWidth; protected int _iMacBlockHeight; @@ -91,8 +92,6 @@ public void decode(MdecInputStream mdecInStream) int iCurrentBlockNonZeroCount; int iCurrentBlockLastNonZeroPosition; - MdecInputStream.MdecCode code = new MdecInputStream.MdecCode(); - int iMacBlk = 0, iBlock = 0; try { @@ -110,36 +109,36 @@ public void decode(MdecInputStream mdecInStream) for (iBlock = 0; iBlock < 6; iBlock++) { Arrays.fill(_CurrentBlock, 0); - mdecInStream.readMdecCode(code); + mdecInStream.readMdecCode(_code); if (log().isLoggable(Level.FINEST)) - log().finest("Qscale & DC " + code); + log().finest("Qscale & DC " + _code); - if (code.Bottom10Bits != 0) { + if (_code.Bottom10Bits != 0) { _CurrentBlock[0] = - code.Bottom10Bits * PSX_DEFAULT_INTRA_QUANTIZATION_MATRIX[0]; + _code.Bottom10Bits * PSX_DEFAULT_INTRA_QUANTIZATION_MATRIX[0]; iCurrentBlockNonZeroCount = 1; iCurrentBlockLastNonZeroPosition = 0; } else { iCurrentBlockNonZeroCount = 0; iCurrentBlockLastNonZeroPosition = -1; } - iCurrentBlockQscale = code.Top6Bits; + iCurrentBlockQscale = _code.Top6Bits; iCurrentBlockVectorPosition = 0; - while (!mdecInStream.readMdecCode(code)) { + while (!mdecInStream.readMdecCode(_code)) { if (log().isLoggable(Level.FINEST)) - log().finest(code.toString()); + log().finest(_code.toString()); //////////////////////////////////////////////////////// - iCurrentBlockVectorPosition += code.Top6Bits + 1; + iCurrentBlockVectorPosition += _code.Top6Bits + 1; try { // Reverse Zig-Zag and Dequantize all at the same time int iRevZigZagPos = MdecInputStream.REVERSE_ZIG_ZAG_SCAN_MATRIX[iCurrentBlockVectorPosition]; _CurrentBlock[iRevZigZagPos] = - (code.Bottom10Bits + (_code.Bottom10Bits * PSX_DEFAULT_INTRA_QUANTIZATION_MATRIX[iRevZigZagPos] * iCurrentBlockQscale + 4) >> 3; // i >> 3 == (int)Math.floor(i / 8.0) @@ -156,7 +155,7 @@ public void decode(MdecInputStream mdecInStream) } if (log().isLoggable(Level.FINEST)) - log().finest(code.toString()); + log().finest(_code.toString()); writeEndOfBlock(iMacBlk, iBlock, iCurrentBlockNonZeroCount, diff --git a/src/jpsxdec/plugins/xa/AudioStreamsCombiner.java b/src/jpsxdec/plugins/xa/AudioStreamsCombiner.java new file mode 100644 index 0000000..cd19004 --- /dev/null +++ b/src/jpsxdec/plugins/xa/AudioStreamsCombiner.java @@ -0,0 +1,230 @@ +/* + * jPSXdec: PlayStation 1 Media Decoder/Converter in Java + * Copyright (C) 2007-2010 Michael Sabin + * All rights reserved. + * + * Redistribution and use of the jPSXdec code or any derivative works are + * permitted provided that the following conditions are met: + * + * * Redistributions may not be sold, nor may they be used in commercial + * or revenue-generating business activities. + * + * * Redistributions that are modified from the original source must + * include the complete source code, including the source code for all + * components used by a binary built from the modified sources. However, as + * a special exception, the source code distributed need not include + * anything that is normally distributed (in either source or binary form) + * with the major components (compiler, kernel, and so on) of the operating + * system on which the executable runs, unless that component itself + * accompanies the executable. + * + * * Redistributions must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package jpsxdec.plugins.xa; + +import java.io.IOException; +import java.util.List; +import javax.sound.sampled.AudioFormat; +import jpsxdec.plugins.IdentifiedSector; +import jpsxdec.util.Maths; + + +public class AudioStreamsCombiner implements IAudioSectorDecoder { + private boolean _blnIsStereo; + private int _iSampleRate; + + private final AudioFormat _outFormat; + private int _iStartSector; + private int _iEndSector; + private int _iPresStartSector; + + private byte[] _abNormalizedBuffer; + + private final IAudioSectorDecoder[] _aoDecoders; + + private NormalizingOutputStream _normal; + private IAudioReceiver _feedOut; + + public AudioStreamsCombiner(List audStreams, + boolean blnBigEndian, double dblVolume) + { + + ensureNotOverlap(audStreams); + + _aoDecoders = new IAudioSectorDecoder[audStreams.size()]; + + _blnIsStereo = audStreams.get(0).isStereo(); + _iSampleRate = audStreams.get(0).getSampleRate(); + _iStartSector = audStreams.get(0).getStartSector(); + _iEndSector = audStreams.get(0).getEndSector(); + _iPresStartSector = audStreams.get(0).getPresentationStartSector(); + for (int i = 0; i < _aoDecoders.length; i++) { + DiscItemAudioStream aud = audStreams.get(i); + _aoDecoders[i] = aud.makeDecoder(blnBigEndian, dblVolume); + _blnIsStereo = _blnIsStereo || aud.isStereo(); + _iSampleRate = Maths.gcd(_iSampleRate, aud.getSampleRate()); + _iStartSector = Math.min(_iStartSector, aud.getStartSector()); + _iEndSector = Math.max(_iEndSector, aud.getEndSector()); + _iPresStartSector = Math.min(_iPresStartSector, aud.getPresentationStartSector()); + } + + _outFormat = new AudioFormat(_iSampleRate, 16, _blnIsStereo ? 2 : 1, + true, blnBigEndian); + } + + private static void ensureNotOverlap(List audStreams) { + for (int i = 0; i < audStreams.size(); i++) { + for (int j = i+1; j < audStreams.size(); j++) { + if (audStreams.get(i).overlaps(audStreams.get(j))) + throw new IllegalArgumentException("Streams are not mutually exclusive."); + } + } + } + + @Override + public void open(IAudioReceiver audioOut) { + + _feedOut = audioOut; + _normal = new NormalizingOutputStream(); + + for (IAudioSectorDecoder decoder : _aoDecoders) { + if (decoder.getOutputFormat().matches(_outFormat)) + decoder.open(_feedOut); + else + decoder.open(_normal); + } + } + + @Override + public double getVolume() { + // assume the volume is the same for all decoders + return _aoDecoders[0].getVolume(); + } + + @Override + public void setVolume(double dblVolume) { + for (IAudioSectorDecoder decoder : _aoDecoders) { + decoder.setVolume(dblVolume); + } + } + + @Override + public AudioFormat getOutputFormat() { + return _outFormat; + } + + @Override + public void reset() { + for (IAudioSectorDecoder decoder : _aoDecoders) { + decoder.reset(); + } + } + + @Override + public int getStartSector() { + return _iStartSector; + } + + @Override + public int getEndSector() { + return _iEndSector; + } + + @Override + public int getPresentationStartSector() { + return _iPresStartSector; + } + + private IAudioSectorDecoder pickDecoder(int iSector) { + for (IAudioSectorDecoder decoder : _aoDecoders) { + if (decoder.getStartSector() <= iSector && + iSector <= decoder.getEndSector()) + return decoder; + } + return null; + } + + @Override + public void feedSector(IdentifiedSector sector) throws IOException { + IAudioSectorDecoder decoder = pickDecoder(sector.getSectorNumber()); + if (decoder == null) + return; + + if (decoder.getOutputFormat().matches(_outFormat)) + decoder.feedSector(sector); + else { + if (_normal == null) + _normal = new NormalizingOutputStream(); + + decoder.feedSector(sector); + } + } + + + private class NormalizingOutputStream implements IAudioReceiver { + + @Override + public void close() throws IOException { + throw new UnsupportedOperationException("I hope this never happens."); + } + + @Override + public void write(AudioFormat inFormat, byte[] abIn, int iInOfs, int iInLen, int iPresentationSector) throws IOException { + + boolean blnInIsStereo = inFormat.getChannels() == 2 ? true : false; + int iInSampleRate = (int)inFormat.getSampleRate(); + + if (_iSampleRate < iInSampleRate) + throw new IllegalArgumentException("Unable to downsample."); + if (_iSampleRate % iInSampleRate != 0) + throw new IllegalArgumentException("Unable to upsample non multiple rate."); + if (_blnIsStereo != blnInIsStereo && blnInIsStereo) + throw new IllegalArgumentException("Unable to downsample to mono."); + + boolean blnStereoize = _blnIsStereo != blnInIsStereo; + + int iSizeMultiple = _iSampleRate / iInSampleRate; + if (blnStereoize) + iSizeMultiple *= 2; + + int iOutLen = iInLen * iSizeMultiple; + if (_abNormalizedBuffer == null || _abNormalizedBuffer.length < iOutLen) + _abNormalizedBuffer = new byte[iOutLen]; + + int iInSampleSize = blnInIsStereo ? 4 : 2; + if (iInLen % iInSampleSize != 0) + throw new IllegalArgumentException("Input size is not a multiple of sample size."); + int iSampleCount = iInLen / iInSampleSize; + + int iOutOfs = 0; + for (int iSampleIdx = 0; iSampleIdx < iSampleCount; iSampleIdx++) { + for (int iScaleIdx = 0; iScaleIdx < iSizeMultiple; iScaleIdx++) { + for (int iCopyIdx = 0; iCopyIdx < iInSampleSize; iCopyIdx++) { + _abNormalizedBuffer[iOutOfs] = abIn[iInOfs+iCopyIdx]; + iOutOfs++; + } + } + iInOfs += iInSampleSize; + } + + _feedOut.write(_outFormat, _abNormalizedBuffer, 0, iOutLen, iPresentationSector); + } + + } + +} diff --git a/src/jpsxdec/plugins/xa/AudioSync.java b/src/jpsxdec/plugins/xa/AudioSync.java new file mode 100644 index 0000000..0618935 --- /dev/null +++ b/src/jpsxdec/plugins/xa/AudioSync.java @@ -0,0 +1,54 @@ +package jpsxdec.plugins.xa; + +import jpsxdec.util.Fraction; + +public class AudioSync { + + private final Fraction _sectorsPerSecond; + private final Fraction _samplesPerSecond; + private final Fraction _samplesPerSector; + + private int _iPreviousAudioSector; + private long _lngSamplesFromPreviousAudioSector; + + public AudioSync(int iMovieStartSector, + int iSectorsPerSecond, + float fltSamplesPerSecond) + { + + _sectorsPerSecond = new Fraction(iSectorsPerSecond, 1); + _samplesPerSecond = new Fraction(Math.round(fltSamplesPerSecond * 1000), 1000); + // samples/sector = samples/second / sectors/second + _samplesPerSector = _samplesPerSecond.divide(_sectorsPerSecond); + + _iPreviousAudioSector = iMovieStartSector; + } + + public Fraction getSamplesPerSecond() { + return _samplesPerSecond; + } + + public Fraction getSamplesPerSector() { + return _samplesPerSector; + } + + public Fraction getSectorsPerSecond() { + return _sectorsPerSecond; + } + + public long calculateSamples(int iSectorSpan) { + return Math.round(_samplesPerSector.multiply(iSectorSpan).asDouble()); + } + + public long calculateNeededSilence(int iSector, long lngSampleCount) { + + long lngSamplesNeededForSectorSpan = calculateSamples(iSector - _iPreviousAudioSector); + long lngSampleDifference = lngSamplesNeededForSectorSpan - _lngSamplesFromPreviousAudioSector; + + _iPreviousAudioSector = iSector; + _lngSamplesFromPreviousAudioSector = lngSampleCount; + + return lngSampleDifference; + } + +} diff --git a/src/jpsxdec/plugins/xa/IDiscItemAudioStream.java b/src/jpsxdec/plugins/xa/DiscItemAudioStream.java similarity index 65% rename from src/jpsxdec/plugins/xa/IDiscItemAudioStream.java rename to src/jpsxdec/plugins/xa/DiscItemAudioStream.java index 0165018..30b5018 100644 --- a/src/jpsxdec/plugins/xa/IDiscItemAudioStream.java +++ b/src/jpsxdec/plugins/xa/DiscItemAudioStream.java @@ -38,27 +38,44 @@ package jpsxdec.plugins.xa; import javax.sound.sampled.AudioFormat; -import jpsxdec.util.AudioOutputStream; +import jpsxdec.plugins.DiscItemSerialization; +import jpsxdec.plugins.DiscItemStreaming; +import jpsxdec.util.NotThisTypeException; /** Interface for all DiscItems that represent an audio stream. * This is necessary for the video plugin to utilize any audio stream that * runs parallel to the video. */ -public interface IDiscItemAudioStream { +public abstract class DiscItemAudioStream extends DiscItemStreaming { - int getSampleRate(); + public DiscItemAudioStream(DiscItemSerialization fields) throws NotThisTypeException { + super(fields); + } - boolean isStereo(); + public DiscItemAudioStream(int iStartSector, int iEndSector) { + super(iStartSector, iEndSector); + } - int getStartSector(); - int getEndSector(); + public boolean overlaps(DiscItemAudioStream other) { + // [ this ] < [ other ] + if (getEndSector() + getSectorsPastEnd() < other.getStartSector()) + return false; + // [ other ] < [ this ] + if (other.getEndSector() + other.getSectorsPastEnd() < getStartSector()) + return false; + return true; + } + + abstract public int getSampleRate(); + + abstract public boolean isStereo(); /** Creates a decoder capable of converting IdentifiedSectors into audio - * data that is then fed into the AudioOutputStream. */ - IDiscItemAudioSectorDecoder makeDecoder(AudioOutputStream outStream, - boolean blnBigEndian, double dblVolume); + * data which will then be fed into an AudioOutputStream. + * @see IAudioSectorDecoder#open(jpsxdec.util.AudioOutputStream) */ + abstract public IAudioSectorDecoder makeDecoder(boolean blnBigEndian, double dblVolume); - AudioFormat getAudioFormat(boolean blnBigEndian); + abstract public AudioFormat getAudioFormat(boolean blnBigEndian); - int getIndex(); + abstract public int getSectorsPastEnd(); } diff --git a/src/jpsxdec/plugins/xa/DiscItemXAAudioStream.java b/src/jpsxdec/plugins/xa/DiscItemXAAudioStream.java index dea3fff..2508714 100644 --- a/src/jpsxdec/plugins/xa/DiscItemXAAudioStream.java +++ b/src/jpsxdec/plugins/xa/DiscItemXAAudioStream.java @@ -38,16 +38,14 @@ package jpsxdec.plugins.xa; import javax.sound.sampled.AudioFormat; -import jpsxdec.plugins.DiscItemStreaming; import java.io.IOException; import jpsxdec.plugins.DiscItemSerialization; import jpsxdec.plugins.DiscItemSaver; import jpsxdec.plugins.IdentifiedSector; -import jpsxdec.util.AudioOutputStream; import jpsxdec.util.NotThisTypeException; /** Represents a series of XA ADPCM sectors that combine to make an audio stream */ -public class DiscItemXAAudioStream extends DiscItemStreaming implements IDiscItemAudioStream { +public class DiscItemXAAudioStream extends DiscItemAudioStream { /** Type identifier for this disc item. */ public static final String TYPE_ID = "XA"; @@ -209,25 +207,35 @@ public int getADPCMBitsPerSample() { return _iBitsPerSample; } + @Override + public int getPresentationStartSector() { + return getStartSector(); + } + public AudioFormat getAudioFormat(boolean blnBigEndian) { return new AudioFormat(_iSamplesPerSecond, 16, _blnIsStereo ? 2 : 1, true, blnBigEndian); } - public IDiscItemAudioSectorDecoder makeDecoder(AudioOutputStream outStream, boolean blnBigEndian, double dblVolume) { - return new XAConverter(outStream, blnBigEndian, dblVolume); + public IAudioSectorDecoder makeDecoder(boolean blnBigEndian, double dblVolume) { + return new XAConverter(blnBigEndian, dblVolume); } - private class XAConverter implements IDiscItemAudioSectorDecoder { + private class XAConverter implements IAudioSectorDecoder { - private AudioOutputStream __outStream; - private XAADPCMDecoder __decoder; - private byte[] __abTempBuffer = new byte[XAADPCMDecoder.BYTES_GENERATED_FROM_XAADPCM_SECTOR]; + private final XAADPCMDecoder __decoder; + private IAudioReceiver __outFeed; + private final byte[] __abTempBuffer = new byte[XAADPCMDecoder.BYTES_GENERATED_FROM_XAADPCM_SECTOR]; + private AudioFormat __format; - public XAConverter(AudioOutputStream outStream, boolean blnBigEndian, double dblVolume) { - __outStream = outStream; + public XAConverter(boolean blnBigEndian, double dblVolume) { __decoder = XAADPCMDecoder.create(getADPCMBitsPerSample(), isStereo(), blnBigEndian, dblVolume); } + @Override + public void open(IAudioReceiver audioFeed) { + __outFeed = audioFeed; + } + public void feedSector(IdentifiedSector sector) throws IOException { if (sector == null) return; @@ -248,8 +256,11 @@ public void feedSector(IdentifiedSector sector) throws IOException { } __decoder.decode(xaSector.getIdentifiedUserDataStream(), __abTempBuffer); - __outStream.write(__decoder.getOutputFormat(xaSector.getSamplesPerSecond()), - __abTempBuffer, 0, __abTempBuffer.length); + + if (__format == null) + __format = __decoder.getOutputFormat(xaSector.getSamplesPerSecond()); + + __outFeed.write(__format, __abTempBuffer, 0, __abTempBuffer.length, xaSector.getSectorNumber()); } public double getVolume() { @@ -275,6 +286,10 @@ public int getEndSector() { public int getStartSector() { return DiscItemXAAudioStream.this.getStartSector(); } + + public int getPresentationStartSector() { + return DiscItemXAAudioStream.this.getPresentationStartSector(); + } } public boolean isPartOfStream(SectorXA xaSector) { @@ -283,13 +298,6 @@ public boolean isPartOfStream(SectorXA xaSector) { xaSector.getBitsPerSample() == _iBitsPerSample; } - @Override - public long calclateTime(int iSect) { - if (iSect < getStartSector() || iSect > getEndSector()) - throw new IllegalArgumentException("Sector number is out of media item bounds."); - return ( iSect - getStartSector() ) * getDiscSpeed() * 75; - } - @Override public DiscItemSaver getSaver() { return new XAAudioItemSaver(this); diff --git a/src/jpsxdec/plugins/xa/IAudioReceiver.java b/src/jpsxdec/plugins/xa/IAudioReceiver.java new file mode 100644 index 0000000..8264cfb --- /dev/null +++ b/src/jpsxdec/plugins/xa/IAudioReceiver.java @@ -0,0 +1,15 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package jpsxdec.plugins.xa; + +import java.io.IOException; +import javax.sound.sampled.AudioFormat; + + +public interface IAudioReceiver { + void write(AudioFormat inFormat, byte[] abData, int iStart, int iLen, int iEndSector) throws IOException; + void close() throws IOException; +} diff --git a/src/jpsxdec/plugins/xa/IDiscItemAudioSectorDecoder.java b/src/jpsxdec/plugins/xa/IAudioSectorDecoder.java similarity index 86% rename from src/jpsxdec/plugins/xa/IDiscItemAudioSectorDecoder.java rename to src/jpsxdec/plugins/xa/IAudioSectorDecoder.java index 1aa02aa..cc92deb 100644 --- a/src/jpsxdec/plugins/xa/IDiscItemAudioSectorDecoder.java +++ b/src/jpsxdec/plugins/xa/IAudioSectorDecoder.java @@ -40,10 +40,19 @@ import java.io.IOException; import javax.sound.sampled.AudioFormat; import jpsxdec.plugins.IdentifiedSector; +import jpsxdec.util.AudioOutputStream; /** Interface for decoders generated by an IDiscItemAudioStream. * Hides the decoder implementation, but also allows it to be passed around. */ -public interface IDiscItemAudioSectorDecoder { +public interface IAudioSectorDecoder { + + /** Opens the decoder for writing to the supplied AudioOutputStream. */ + void open(IAudioReceiver audioFeeder); + + /** The format that the AudioOutputStream supplied in + * {@link #open(jpsxdec.util.AudioOutputStream)} + * must have. */ + AudioFormat getOutputFormat(); /** If it likes the sector, writes audio data to the AudioOutputStream * it got when created. */ @@ -52,10 +61,11 @@ public interface IDiscItemAudioSectorDecoder { double getVolume(); void setVolume(double dblVolume); - AudioFormat getOutputFormat(); - + /** Resets the decoding context. */ void reset(); + int getPresentationStartSector(); + int getStartSector(); int getEndSector(); } diff --git a/src/jpsxdec/plugins/xa/PCM16bitAudioWriter.java b/src/jpsxdec/plugins/xa/SectorAudioWriter.java similarity index 89% rename from src/jpsxdec/plugins/xa/PCM16bitAudioWriter.java rename to src/jpsxdec/plugins/xa/SectorAudioWriter.java index 60a73a6..f9f8e85 100644 --- a/src/jpsxdec/plugins/xa/PCM16bitAudioWriter.java +++ b/src/jpsxdec/plugins/xa/SectorAudioWriter.java @@ -37,21 +37,15 @@ package jpsxdec.plugins.xa; -import jpsxdec.util.AudioOutputStream; import java.io.IOException; -import javax.sound.sampled.AudioFormat; +import jpsxdec.plugins.IdentifiedSector; -public interface PCM16bitAudioWriter extends AudioOutputStream { +public interface SectorAudioWriter { - void close() throws IOException; - - AudioFormat getFormat(); - - void open() throws IOException; + void feedSector(IdentifiedSector sector) throws IOException; - double getVolume(); + void close() throws IOException; String getOutputFile(); - } diff --git a/src/jpsxdec/plugins/xa/PCM16bitAudioWriterBuilder.java b/src/jpsxdec/plugins/xa/SectorAudioWriterBuilder.java similarity index 76% rename from src/jpsxdec/plugins/xa/PCM16bitAudioWriterBuilder.java rename to src/jpsxdec/plugins/xa/SectorAudioWriterBuilder.java index 94cdce7..b98a414 100644 --- a/src/jpsxdec/plugins/xa/PCM16bitAudioWriterBuilder.java +++ b/src/jpsxdec/plugins/xa/SectorAudioWriterBuilder.java @@ -48,25 +48,22 @@ import javax.sound.sampled.AudioFormat; import javax.swing.JPanel; import jpsxdec.formats.JavaAudioFormat; +import jpsxdec.plugins.IdentifiedSector; import jpsxdec.util.AudioOutputFileWriter; import jpsxdec.util.FeedbackStream; import jpsxdec.util.TabularFeedback; -/** Manages possible options for creating a PCM16bitAudioWriter, and +/** Manages possible options for creating a SectorAudioWriter, and * produces independent instances using the current options. */ -public class PCM16bitAudioWriterBuilder { +public class SectorAudioWriterBuilder { - private final boolean _blnIsStereo; - private final String _sSuggestedBaseName; - private final int _iSampleRate; + private final DiscItemAudioStream _audItem; - public PCM16bitAudioWriterBuilder(boolean blnIsStereo, int iSampleRate, String sSuggestedBaseName) + public SectorAudioWriterBuilder(DiscItemAudioStream audItem) { - _blnIsStereo = blnIsStereo; - _sSuggestedBaseName = sSuggestedBaseName; - _iSampleRate = iSampleRate; + _audItem = audItem; setPossibleFormats(JavaAudioFormat.getAudioFormats()); - setFilename(sSuggestedBaseName); + setFilename(_audItem.getSuggestedBaseName()); } private JavaAudioFormat[] _aoPossibleContainerFormats; @@ -157,46 +154,48 @@ public void printHelp(FeedbackStream fbs) { tfb.write(fbs); } + private class JavaFormatWriter implements SectorAudioWriter, IAudioReceiver { - public PCM16bitAudioWriter getAudioWriter() { - return new PCM16bitAudioWriter() { + private final AudioFormat __audioFmt; + private final String __sOutputFile = _sFilename + "." + _containerFormat.getExtension(); + private final AudioOutputFileWriter __audioWriter; + private IAudioSectorDecoder __decoder; - private AudioOutputFileWriter __audioWriter; - private AudioFormat __audioFmt = new AudioFormat( - _iSampleRate, - 16, _blnIsStereo ? 2 : 1, - true, true); - private String __sOutputFile = _sFilename + "." + _containerFormat.getExtension(); + public JavaFormatWriter(double dblVolume) throws IOException { - public void close() throws IOException { - __audioWriter.close(); - } + __audioFmt = new AudioFormat( + _audItem.getSampleRate(), + 16, _audItem.isStereo() ? 2 : 1, + true, true); + __audioWriter = new AudioOutputFileWriter( + new File(__sOutputFile), + __audioFmt, _containerFormat.getJavaType()); + __decoder = _audItem.makeDecoder(__audioFmt.isBigEndian(), dblVolume); + __decoder.open(this); + } - public AudioFormat getFormat() { - return __audioFmt; - } + @Override + public void write(AudioFormat inFormat, byte[] abData, int iStart, int iLen, int iPresentationSector) throws IOException { + __audioWriter.write(inFormat, abData, iStart, iLen); + } - public void open() throws IOException { - __audioWriter = new AudioOutputFileWriter( - new File(__sOutputFile), - __audioFmt, _containerFormat.getJavaType()); - } + public void close() throws IOException { + __audioWriter.close(); + } - public void write(AudioFormat inFormat, byte[] abData, int iOffset, int iLength) - throws IOException - { - __audioWriter.write(inFormat, abData, iOffset, iLength); - } + public String getOutputFile() { + return __sOutputFile; + } - public double getVolume() { - return 1.0; - } + @Override + public void feedSector(IdentifiedSector sector) throws IOException { + __decoder.feedSector(sector); + } - public String getOutputFile() { - return __sOutputFile; - } + }; - }; + public SectorAudioWriter getAudioWriter() throws IOException { + return new JavaFormatWriter(getVolume()); } diff --git a/src/jpsxdec/plugins/xa/SectorXA.java b/src/jpsxdec/plugins/xa/SectorXA.java index 5f752cb..1baa384 100644 --- a/src/jpsxdec/plugins/xa/SectorXA.java +++ b/src/jpsxdec/plugins/xa/SectorXA.java @@ -74,37 +74,36 @@ public SectorXA(CDSector cdSector) throws NotThisTypeException { throw new NotThisTypeException(); int iErrors = 0; - byte[] abSectorData = cdSector.getCdUserDataCopy(); _iBitsPerSample = cdSector.getCodingInfo().getBitsPerSample(); if (_iBitsPerSample == 4) { for (int iOfs = 0; - iOfs < abSectorData.length - XAADPCMDecoder.SIZE_OF_SOUND_GROUP; + iOfs < cdSector.getCdUserDataSize() - XAADPCMDecoder.SIZE_OF_SOUND_GROUP; iOfs+=XAADPCMDecoder.SIZE_OF_SOUND_GROUP) { // the 8 sound parameters (one for each sound unit) // are repeated twice, and are ordered like this: // 0,1,2,3, 0,1,2,3, 4,5,6,7, 4,5,6,7 for (int i = 0; i < 4; i++) { - if (abSectorData[iOfs + i] != abSectorData[iOfs + 4 + i]) + if (cdSector.readUserDataByte(iOfs + i) != cdSector.readUserDataByte(iOfs + 4 + i)) iErrors++; - if (abSectorData[iOfs + 8 + i] != abSectorData[iOfs + 12 + i]) + if (cdSector.readUserDataByte(iOfs + 8 + i) != cdSector.readUserDataByte(iOfs + 12 + i)) iErrors++; } } } else { for (int iOfs = 0; - iOfs < abSectorData.length - XAADPCMDecoder.SIZE_OF_SOUND_GROUP; + iOfs < cdSector.getCdUserDataSize() - XAADPCMDecoder.SIZE_OF_SOUND_GROUP; iOfs+=XAADPCMDecoder.SIZE_OF_SOUND_GROUP) { // the 4 sound parameters (one for each sound unit) // are repeated four times and are ordered like this: // 0,1,2,3, 0,1,2,3, 0,1,2,3, 0,1,2,3 for (int i = 0; i < 4; i++) { - if (abSectorData[iOfs + i] != abSectorData[iOfs + 4 + i]) + if (cdSector.readUserDataByte(iOfs + i) != cdSector.readUserDataByte(iOfs + 4 + i)) iErrors++; - if (abSectorData[iOfs + i] != abSectorData[iOfs + 8 + i]) + if (cdSector.readUserDataByte(iOfs + i) != cdSector.readUserDataByte(iOfs + 8 + i)) iErrors++; - if (abSectorData[iOfs + i] != abSectorData[iOfs + 12 + i]) + if (cdSector.readUserDataByte(iOfs + i) != cdSector.readUserDataByte(iOfs + 12 + i)) iErrors++; } } diff --git a/src/jpsxdec/plugins/xa/XAAudioItemSaver.java b/src/jpsxdec/plugins/xa/XAAudioItemSaver.java index d8db9df..4b8fdc0 100644 --- a/src/jpsxdec/plugins/xa/XAAudioItemSaver.java +++ b/src/jpsxdec/plugins/xa/XAAudioItemSaver.java @@ -38,7 +38,6 @@ package jpsxdec.plugins.xa; import java.io.IOException; -import java.io.PrintStream; import java.util.logging.Logger; import javax.swing.JPanel; import jpsxdec.cdreaders.CDSector; @@ -51,11 +50,11 @@ public class XAAudioItemSaver extends DiscItemSaver { private static final Logger log = Logger.getLogger(XAAudioItemSaver.class.getName()); - private PCM16bitAudioWriterBuilder _audWriterBuilder; + private SectorAudioWriterBuilder _audWriterBuilder; private DiscItemXAAudioStream _xaItem; public XAAudioItemSaver(DiscItemXAAudioStream xaItem) { - _audWriterBuilder = new PCM16bitAudioWriterBuilder(xaItem.isStereo(), xaItem.getSampleRate(), xaItem.getSuggestedBaseName()); + _audWriterBuilder = new SectorAudioWriterBuilder(xaItem); _xaItem = xaItem; } @@ -76,17 +75,15 @@ public JPanel getOptionPane() { @Override public void startSave(ProgressListener pl) throws IOException { - PCM16bitAudioWriter audioWriter = _audWriterBuilder.getAudioWriter(); + SectorAudioWriter audioWriter = _audWriterBuilder.getAudioWriter(); int iSector = _xaItem.getStartSector(); - IDiscItemAudioSectorDecoder decoder = _xaItem.makeDecoder(audioWriter, audioWriter.getFormat().isBigEndian(), audioWriter.getVolume()); try { final double SECTOR_LENGTH = _xaItem.getSectorLength(); pl.progressStart("Writing " + audioWriter.getOutputFile()); - audioWriter.open(); for (; iSector <= _xaItem.getEndSector(); iSector++) { CDSector cdSector = _xaItem.getSourceCD().getSector(iSector); IdentifiedSector identifiedSect = _xaItem.identifySector(cdSector); - decoder.feedSector(identifiedSect); + audioWriter.feedSector(identifiedSect); pl.progressUpdate((iSector - _xaItem.getStartSector()) / SECTOR_LENGTH); } pl.progressEnd(); diff --git a/src/jpsxdec/util/Fraction.java b/src/jpsxdec/util/Fraction.java index 9defe49..305497f 100644 --- a/src/jpsxdec/util/Fraction.java +++ b/src/jpsxdec/util/Fraction.java @@ -66,6 +66,10 @@ public double asDouble() { return ((double)(getNumerator())) / ((double)(getDenominator())); } + public int asInt() { + return (int) (getNumerator() / getDenominator()); + } + /** * Compute the nonnegative greatest common divisor of a and b. * (This is needed for normalizing Fractions, but can be @@ -89,6 +93,14 @@ public static long gcd(long a, long b) { return x; } + public static Fraction divide(long a, Fraction b) { + long an = a; + long ad = 1; + long bn = b.getNumerator(); + long bd = b.getDenominator(); + return new Fraction(an*bd, ad*bn); + } + /** return a Fraction representing the negated value of this Fraction **/ public Fraction negative() { long an = getNumerator(); @@ -159,6 +171,10 @@ public Fraction multiply(long n) { return new Fraction(an*bn, ad*bd); } + public float multiply(float f) { + return getNumerator() * f / getDenominator(); + } + /** return a Fraction representing this Fraction divided by b **/ public Fraction divide(Fraction b) { long an = getNumerator(); diff --git a/src/jpsxdec/util/IO.java b/src/jpsxdec/util/IO.java index 9b1e0b8..87be3fa 100644 --- a/src/jpsxdec/util/IO.java +++ b/src/jpsxdec/util/IO.java @@ -78,6 +78,10 @@ public static int readUInt8(byte[] ab, int i) { return ab[i] & 0xFF; } + public static byte readSInt8(InputStream is) throws IOException { + return (byte)(readUInt8(is)); + } + //== 16-bit signed ========================================================= public static short readSInt16LE(InputStream oIS) throws IOException { @@ -269,6 +273,21 @@ public static int readSInt32LE(InputStream oIS) throws IOException { return total; } + //== 64-bit ================================================================ + + public static long readSInt64BE(InputStream is) throws IOException { + long lngRet = 0; + for (int i = 0; i < 8; i++) { + int iByte = is.read(); + if (iByte < 0) + throw new EOFException("Unexpected end of file in readSInt64BE"); + lngRet <<= 8; + lngRet |= iByte; + } + return lngRet; + } + + //== Other IO ============================================================== /** Because the read(byte[]) method won't always return the entire diff --git a/src/jpsxdec/util/aviwriter/AviWriter.java b/src/jpsxdec/util/aviwriter/AviWriter.java index 0cb9d41..1c6cf6f 100644 --- a/src/jpsxdec/util/aviwriter/AviWriter.java +++ b/src/jpsxdec/util/aviwriter/AviWriter.java @@ -37,29 +37,20 @@ package jpsxdec.util.aviwriter; -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; -import java.awt.image.ColorModel; -import javax.imageio.ImageIO; import java.io.IOException; import java.io.RandomAccessFile; import java.io.File; +import java.io.InputStream; import java.util.ArrayList; -import java.util.Iterator; -import javax.imageio.IIOImage; -import javax.imageio.ImageWriteParam; -import javax.imageio.ImageWriter; -import javax.imageio.stream.MemoryCacheImageOutputStream; +import java.util.Arrays; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; -import jpsxdec.util.ExposedBAOS; import jpsxdec.util.aviwriter.AVIOLDINDEX.AVIOLDINDEXENTRY; /** - * AVI encoder to write uncompressed, RGB DIB video, or compressed MJPG video, - * along with uncompressed PCM audio. The resulting MJPG AVI seems playable on - * vanilla Windows XP systems, and of course VLC. + * Creates AVI files with audio and video without the need for JMF. + * Subclasses should take care of codec handling. *

* This code is originally based on (but now hardly resembles) the ImageJ * package at http://rsb.info.nih.gov/ij @@ -77,9 +68,6 @@ * MIPAV program, available from http://mipav.cit.nih.gov/, which also * appears to be in the public domain. *

- * I owe my MJPG understanding to the jpegtoavi program. - * http://sourceforge.net/projects/jpegtoavi/ - *

* Random list of codecs * http://www.oltenia.ro/download/pub/windows/media/video/tools/GSpot/gspot22/GSpot22.dat *

@@ -90,22 +78,8 @@ *

* Works with Java 1.5 or higher. */ -public class AviWriter { - - /** Hold's true if system can write "jpeg" images. */ - private final static boolean CAN_ENCODE_JPEG; - static { - // check if the system can write "jpeg" images - boolean bln = false; - for (String s : ImageIO.getReaderFormatNames()) { - if (s.equals("jpeg")) { - bln = true; - break; - } - } - CAN_ENCODE_JPEG = bln; - } - +public abstract class AviWriter { + // ------------------------------------------------------------------------- // -- Fields --------------------------------------------------------------- // ------------------------------------------------------------------------- @@ -119,14 +93,9 @@ public class AviWriter { private final long _lngFrames; /** Denominator of the frames/second fraction. */ private final long _lngPerSecond; - - /** Size of the frame data in bytes. Only applicable to DIB AVI. - * Each DIB frame submitted is compared to this value to ensure - * proper data. */ - private int _iFrameByteSize = -1; /** Temporary buffer available to store data before writing. */ - private byte[] _abWriteBuffer; + protected byte[] _abWriteBuffer; /** Number of frames written. */ private long _lngFrameCount = 0; @@ -134,14 +103,11 @@ public class AviWriter { private final AudioFormat _audioFormat; /** Number of audio samples written. */ - private double _dblSampleCount = 0; - - /** The image writer used to convert the BufferedImages to BMP or JPEG. */ - private final ImageWriter _imgWriter; - /** Only used for MJPG when not using default quality level. */ - private final ImageWriteParam _writeParams; - /** True if writing avi with MJPG codec, false if writing DIB codec. */ - private final boolean _blnMJPG; + private long _lngSampleCount = 0; + + private final boolean _blnCompressedVideo; + private final String _sFourCCcodec; + private final int _iCompression; // ------------------------------------------------------------------------- // -- Properties ----------------------------------------------------------- @@ -168,28 +134,38 @@ public long getVideoFramesWritten() { } public long getAudioSamplesWritten() { - return (long)_dblSampleCount; + return _lngSampleCount; + } + + /** Returns approximately how many samples per frame (may be a fraction off) */ + public int getAudioSamplesPerFrame() { + if (_audioFormat == null) + return 0; + + // samples/second / frames/second = samples/frame + + return Math.round(_audioFormat.getSampleRate() * _lngPerSecond / _lngFrames); } // ------------------------------------------------------------------------- // -- AVI Structure Fields ------------------------------------------------- // ------------------------------------------------------------------------- - private RandomAccessFile raFile; + private RandomAccessFile _aviFile; - private Chunk RIFF_chunk; - private Chunk LIST_hdr1; - private AVIMAINHEADER avih; - private Chunk LIST_strl_vid; - private Chunk strf_vid; - private AVISTREAMHEADER strh_vid; - private BITMAPINFOHEADER bif; + private Chunk _RIFF_chunk; + private Chunk _LIST_hdr1; + private AVIMAINHEADER _avih; + private Chunk _LIST_strl_vid; + private Chunk _strf_vid; + private AVISTREAMHEADER _strh_vid; + private BITMAPINFOHEADER _bif; //strf_vid //LIST_strl_vid - private Chunk LIST_strl_aud; - private Chunk strf_aud; - private AVISTREAMHEADER strh_aud; - private WAVEFORMATEX wavfmt; + private Chunk _LIST_strl_aud; + private Chunk _strf_aud; + private AVISTREAMHEADER _strh_aud; + private WAVEFORMATEX _wavfmt; //strf_aud //LIST_strl_aud //LIST_hdr1 @@ -207,107 +183,20 @@ public long getAudioSamplesWritten() { // -- Constructors --------------------------------------------------------- // ------------------------------------------------------------------------- - public AviWriter(final File oOutputfile, - final int iWidth, final int iHeight, - final long lngFrames, final long lngPerSecond) - throws IOException - { - this(oOutputfile, - iWidth, iHeight, - lngFrames, lngPerSecond, - false, true, -0.1f, null); - } - public AviWriter(final File outFile, - final int iWidth, final int iHeight, - final long lngFrames, final long lngPerSecond, - float fltQuality) - throws IOException - { - this(outFile, - iWidth, iHeight, - lngFrames, lngPerSecond, - true, false, fltQuality, - null); - } - /** Audio data must be signed 16-bit PCM in little-endian order. */ - public AviWriter(final File outFile, - final int iWidth, final int iHeight, - final long lngFrames, final long lngPerSecond, - boolean blnDefaultLossyQuality, - final AudioFormat audioFormat) - throws IOException - { - this(outFile, - iWidth, iHeight, - lngFrames, lngPerSecond, - true, true, -0.1f, audioFormat); - } /** Audio data must be signed 16-bit PCM in little-endian order. */ - public AviWriter(final File outFile, + protected AviWriter(final File oOutputfile, final int iWidth, final int iHeight, final long lngFrames, final long lngPerSecond, - final AudioFormat audioFormat) + final AudioFormat oAudioFormat, + final boolean blnCompressedVideo, + final String sFourCCcodec, + final int iBytes) throws IOException { - this(outFile, - iWidth, iHeight, - lngFrames, lngPerSecond, - false, true, -0.1f, audioFormat); - } - /** Write MJPG encoded frames with specified quality. - * @param iAudChannels 0, 1 or 2 audio channels. - * @param fltMjpgQuality quality from 0 (lowest) to 1 (highest). */ - public AviWriter(final File oOutputfile, - final int iWidth, final int iHeight, - final long lngFrames, final long lngPerSecond, - final float fltLossyQuality, - final AudioFormat oAudioFormat) - throws IOException - { - this(oOutputfile, - iWidth, iHeight, - lngFrames, lngPerSecond, - true, false, fltLossyQuality, - oAudioFormat); - } - - /** Audio data must be signed 16-bit PCM in little-endian order. */ - private AviWriter(final File oOutputfile, - final int iWidth, final int iHeight, - final long lngFrames, final long lngPerSecond, - final boolean blnIsLossy, - final boolean blnDefaultLossyQuality, - final float fltLossyQuality, - final AudioFormat oAudioFormat) - throws IOException - { - Iterator oIter; - - if (blnIsLossy) { - if (!CAN_ENCODE_JPEG) - throw new UnsupportedOperationException("Unable to create 'jpeg' images on this platform."); - - oIter = ImageIO.getImageWritersByFormatName("jpeg"); - _imgWriter = oIter.next(); - - if (blnDefaultLossyQuality) { - _writeParams = null; - } else { - // TODO: Make sure thumbnails are not being created in the jpegs - _writeParams = _imgWriter.getDefaultWriteParam(); - - _writeParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - // 0 for lowest qulaity, 1 for highest - _writeParams.setCompressionQuality(fltLossyQuality); - } - _blnMJPG = true; - } else { - oIter = ImageIO.getImageWritersByFormatName("bmp"); - _imgWriter = (ImageWriter)oIter.next(); - _writeParams = null; - _blnMJPG = false; - } + _blnCompressedVideo = blnCompressedVideo; + _sFourCCcodec = sFourCCcodec; + _iCompression = iBytes; _iWidth = iWidth; _iHeight = iHeight; @@ -333,57 +222,59 @@ private AviWriter(final File oOutputfile, throw new IllegalArgumentException("Audio sample size cannot be NOT_SPECIFIED."); if (oAudioFormat.isBigEndian()) throw new IllegalArgumentException("Audio must be little-endian."); + if (oAudioFormat.getEncoding() != AudioFormat.Encoding.PCM_SIGNED) + throw new IllegalArgumentException("Audio encoding needs to be PCM_SIGNED."); // TODO: Are there any more checks to perform? } _audioFormat = oAudioFormat; - raFile = new RandomAccessFile(oOutputfile, "rw"); - raFile.setLength(0); // trim the file to 0 + _aviFile = new RandomAccessFile(oOutputfile, "rw"); + _aviFile.setLength(0); // trim the file to 0 //---------------------------------------------------------------------- // Setup the header structure. // Actual values will be filled in when avi is closed. - RIFF_chunk = new Chunk(raFile, "RIFF", "AVI "); + _RIFF_chunk = new Chunk(_aviFile, "RIFF", "AVI "); - LIST_hdr1 = new Chunk(raFile, "LIST", "hdrl"); + _LIST_hdr1 = new Chunk(_aviFile, "LIST", "hdrl"); - avih = new AVIMAINHEADER(); - avih.makePlaceholder(raFile); + _avih = new AVIMAINHEADER(); + _avih.makePlaceholder(_aviFile); - LIST_strl_vid = new Chunk(raFile, "LIST", "strl"); + _LIST_strl_vid = new Chunk(_aviFile, "LIST", "strl"); - strh_vid = new AVISTREAMHEADER(); - strh_vid.makePlaceholder(raFile); + _strh_vid = new AVISTREAMHEADER(); + _strh_vid.makePlaceholder(_aviFile); - strf_vid = new Chunk(raFile, "strf"); + _strf_vid = new Chunk(_aviFile, "strf"); - bif = new BITMAPINFOHEADER(); - bif.makePlaceholder(raFile); + _bif = new BITMAPINFOHEADER(); + _bif.makePlaceholder(_aviFile); - strf_vid.endChunk(raFile); + _strf_vid.endChunk(_aviFile); - LIST_strl_vid.endChunk(raFile); + _LIST_strl_vid.endChunk(_aviFile); if (_audioFormat != null) { // if there is audio - LIST_strl_aud = new Chunk(raFile, "LIST", "strl"); + _LIST_strl_aud = new Chunk(_aviFile, "LIST", "strl"); - strh_aud = new AVISTREAMHEADER(); - strh_aud.makePlaceholder(raFile); + _strh_aud = new AVISTREAMHEADER(); + _strh_aud.makePlaceholder(_aviFile); - strf_aud = new Chunk(raFile, "strf"); + _strf_aud = new Chunk(_aviFile, "strf"); - wavfmt = new WAVEFORMATEX(); - wavfmt.makePlaceholder(raFile); + _wavfmt = new WAVEFORMATEX(); + _wavfmt.makePlaceholder(_aviFile); - strf_aud.endChunk(raFile); + _strf_aud.endChunk(_aviFile); - LIST_strl_aud.endChunk(raFile); + _LIST_strl_aud.endChunk(_aviFile); } - LIST_hdr1.endChunk(raFile); + _LIST_hdr1.endChunk(_aviFile); - LIST_movi = new Chunk(raFile, "LIST", "movi"); + LIST_movi = new Chunk(_aviFile, "LIST", "movi"); // now we're ready to start accepting video/audio data @@ -392,236 +283,184 @@ private AviWriter(final File oOutputfile, } // ------------------------------------------------------------------------- - // -- Public functions ----------------------------------------------------- + // -- Writing functions ---------------------------------------------------- // ------------------------------------------------------------------------- - - /** Assumes width/height is correct. - * abData should either be DIB data - * (BGR rows inverted and widths padded to 4 byte boundaries), - * or JPEG data. */ - private void writeFrameFormatted(byte[] abData) throws IOException { - if (raFile == null) throw new IOException("Avi file is closed"); - // only keep track of frame data size if it's DIB. They should all be the same - if (!_blnMJPG) { - if (_iFrameByteSize < 0) - _iFrameByteSize = abData.length; - else if (_iFrameByteSize != abData.length) - throw new IllegalArgumentException("Frame data size is not consistent"); - } - - writeStreamDataChunk(abData, 0, abData.length, true); - } - /** @param abData RGB image data stored at 24 bits/pixel (3 bytes/pixel) */ - public void writeFrameRGB(byte[] abData, int iStart, int iLineStride) { - int iLinePadding = (_iWidth * 3) & 3; - if (iLinePadding != 0) - iLinePadding = 4 - iLinePadding; - int iNeededBuffSize = (_iWidth * 3 + iLinePadding) * _iHeight ; - if (_abWriteBuffer == null || _abWriteBuffer.length < iNeededBuffSize) - _abWriteBuffer = new byte[iNeededBuffSize]; - - int iSrcLine = iStart; - int iDestPos = 0; - for (int y = _iHeight-1; y >= 0; y--) { - int iSrcPos = iSrcLine; - for (int x = 0; x < _iWidth; x++) { - _abWriteBuffer[iDestPos] = abData[iSrcPos+2]; - iDestPos++; - _abWriteBuffer[iDestPos] = abData[iSrcPos+1]; - iDestPos++; - _abWriteBuffer[iDestPos] = abData[iSrcPos+0]; - iDestPos++; - iSrcPos+=3; - } - iSrcLine += iLineStride; - for (int i = 0; i < iLinePadding; i++) { - _abWriteBuffer[iDestPos] = 0; - iDestPos++; - } - } - } - - /** @param abData RGB image data stored at RGB in the lower bytes of an int. */ - public void writeFrameRGB(int[] aiData, int iStart, int iLineStride) { - int iLinePadding = (_iWidth * 3) & 3; - if (iLinePadding != 0) - iLinePadding = 4 - iLinePadding; - int iNeededBuffSize = (_iWidth * 3 + iLinePadding) * _iHeight ; - if (_abWriteBuffer == null || _abWriteBuffer.length < iNeededBuffSize) - _abWriteBuffer = new byte[iNeededBuffSize]; - - int iSrcLine = iStart; - int iDestPos = 0; - for (int y = _iHeight-1; y >= 0; y--) { - int iSrcPos = iSrcLine; - for (int x = 0; x < _iWidth; x++) { - int c = aiData[iSrcPos]; - _abWriteBuffer[iDestPos] = (byte)(c >> 16); - iDestPos++; - _abWriteBuffer[iDestPos] = (byte)(c >> 8); - iDestPos++; - _abWriteBuffer[iDestPos] = (byte)(c ); - iDestPos++; - iSrcPos++; - } - iSrcLine += iLineStride; - for (int i = 0; i < iLinePadding; i++) { - _abWriteBuffer[iDestPos] = 0; - iDestPos++; - } - } - } - - /** Converts a BufferedImage to proper avi format and writes it. */ - public void writeFrame(BufferedImage bi) throws IOException { - if (raFile == null) throw new IOException("Avi file is closed"); - if (_iWidth != bi.getWidth()) - throw new IllegalArgumentException("AviWriter: Frame width doesn't match" + - " (was " + _iWidth + ", now " + bi.getWidth() + ")."); - - if (_iHeight != bi.getHeight()) - throw new IllegalArgumentException("AviWriter: Frame height doesn't match" + - " (was " + _iHeight + ", now " + bi.getHeight() + ")."); - - if (_blnMJPG) { - writeFrameFormatted(image2MJPEG(bi).getBuffer()); - } else { - writeFrameFormatted(image2DIB(bi, _iFrameByteSize)); - } - } + //abstract public void writeFrame(BufferedImage bi) throws IOException; public void repeatPreviousFrame() { if (_lngFrameCount < 1) throw new IllegalStateException("Unable to repeat a previous frame that doesn't exist."); int iIndex = _indexList.size() - 1; - + + // find the previous chunk that is a frame final int VID_CHUNK_ID = AVIstruct.string2int("00d_") & 0x00FFFFFF; while (true) { int iChunkId = _indexList.get(iIndex).dwChunkId; + // does it start with '00d'? if ((iChunkId & 0x00FFFFFF) == VID_CHUNK_ID) break; else iIndex--; } + // add the same reference in the list _indexList.add(_indexList.get(iIndex)); _lngFrameCount++; } - /** Audio data must be signed 16-bit PCM in little-endian order. */ - public void writeAudio(byte[] abData) throws IOException { - writeAudio(abData, 0, abData.length); - } - - public void writeAudio(byte[] abData, int iOffset, int iLength) throws IOException { - if (raFile == null) throw new IOException("Avi file is closed"); - if (_audioFormat == null) - throw new IllegalStateException("Unable to write audio to video-only avi."); - writeStreamDataChunk(abData, iOffset, iLength, false); - } - - /** Audio data must be signed 16-bit PCM in little-endian order. */ - public void writeAudio(AudioInputStream oData) throws IOException { - if (raFile == null) throw new IOException("Avi file is closed"); + public void writeAudio(AudioInputStream audStream) throws IOException { + if (_aviFile == null) throw new IOException("Avi file is closed"); if (_audioFormat == null) throw new IllegalStateException("Unable to write audio to video-only avi."); - AudioFormat fmt = oData.getFormat(); + AudioFormat fmt = audStream.getFormat(); if (!fmt.matches(_audioFormat)) throw new IllegalArgumentException("Audio stream format does not match."); Chunk data_size; AVIOLDINDEXENTRY idxentry = new AVIOLDINDEXENTRY(); - idxentry.dwOffset = (int)(raFile.getFilePointer() - (LIST_movi.getStart() + 4)); + idxentry.dwOffset = (int)(_aviFile.getFilePointer() - (LIST_movi.getStart() + 4)); idxentry.dwChunkId = AVIstruct.string2int("01wb"); idxentry.dwFlags = 0; - data_size = new Chunk(raFile, "01wb"); + data_size = new Chunk(_aviFile, "01wb"); - if (_abWriteBuffer == null || _abWriteBuffer.length < 1024) - _abWriteBuffer = new byte[1024]; + if (_abWriteBuffer == null || _abWriteBuffer.length < _audioFormat.getFrameSize() * 1024) + _abWriteBuffer = new byte[_audioFormat.getFrameSize() * 1024]; - // write the data, padded to 4 byte boundary - int i; - while ((i = oData.read(_abWriteBuffer, 0, 1024)) > 0) { - _dblSampleCount += i / (double)_audioFormat.getFrameSize(); - raFile.write(_abWriteBuffer, 0, i); + // write the data then pad the data to 4 byte boundary + int i, iTotal = 0; + while ((i = audStream.read(_abWriteBuffer)) > 0) { + iTotal += i; + _lngSampleCount += i / _audioFormat.getFrameSize(); + _aviFile.write(_abWriteBuffer, 0, i); } - - int remaint = (int)(4 - ( (raFile.getFilePointer() - (data_size.getStart()+4) ) % 4)) % 4; - while (remaint > 0) { raFile.write(0); remaint--; } - + if (iTotal % _audioFormat.getFrameSize() != 0) + throw new RuntimeException("Read and wrote partial sample."); + + padTo4ByteBoundary((int)(_aviFile.getFilePointer() - (data_size.getStart()+4))); + // end the chunk - data_size.endChunk(raFile); + data_size.endChunk(_aviFile); // add this item to the index idxentry.dwSize = (int)data_size.getSize(); _indexList.add(idxentry); } - - private void writeStreamDataChunk(byte[] abData, int iOfs, int iLen, boolean blnIsVideo) throws IOException { - Chunk data_size; + /** Audio data must be signed 16-bit PCM in little-endian order. */ + public void writeAudio(byte[] abData) throws IOException { + writeAudio(abData, 0, abData.length); + } + + /** Audio data must be signed 16-bit PCM in little-endian order. */ + public void writeAudio(byte[] abData, int iOfs, int iLen) throws IOException { + if (_aviFile == null) throw new IOException("Avi file is closed"); + if (_audioFormat == null) + throw new IllegalStateException("Unable to write audio to video-only avi."); + + // TODO: Maybe have better handling if half a sample is provided + if (iLen % _audioFormat.getFrameSize() != 0) + throw new IllegalArgumentException("Half an audio sample can't be processed."); AVIOLDINDEXENTRY idxentry = new AVIOLDINDEXENTRY(); - idxentry.dwOffset = (int)(raFile.getFilePointer() - (LIST_movi.getStart() + 4)); + idxentry.dwOffset = (int)(_aviFile.getFilePointer() - (LIST_movi.getStart() + 4)); + idxentry.dwChunkId = AVIstruct.string2int("01wb"); + idxentry.dwFlags = 0; - if (blnIsVideo) { // if video + Chunk data_size = new Chunk(_aviFile, "01wb"); - String sChunkId; - if (_blnMJPG) - sChunkId = "00dc"; // dc for compressed frame - else - sChunkId = "00db"; // db for uncompressed frame - idxentry.dwChunkId = AVIstruct.string2int(sChunkId); - idxentry.dwFlags = AVIOLDINDEX.AVIIF_KEYFRAME; // Write the flags - select AVIIF_KEYFRAME - // AVIIF_KEYFRAME 0x00000010L - // The flag indicates key frames in the video sequence. - _lngFrameCount++; - - data_size = new Chunk(raFile, sChunkId); - } else { // if audio - // TODO: Maybe have better handling if half a sample is provided - if (iLen % _audioFormat.getFrameSize() != 0) - throw new IllegalArgumentException("Half an audio sample can't be processed."); - - idxentry.dwChunkId = AVIstruct.string2int("01wb"); - idxentry.dwFlags = 0; + // write the data, then pad to 4 byte boundary + _aviFile.write(abData, iOfs, iLen); + padTo4ByteBoundary(iLen); + // end the chunk + data_size.endChunk(_aviFile); - _dblSampleCount += iLen / (double)_audioFormat.getFrameSize(); - - data_size = new Chunk(raFile, "01wb"); - } + _lngSampleCount += iLen / _audioFormat.getFrameSize(); + + // add the index to the list + idxentry.dwSize = (int)data_size.getSize(); + _indexList.add(idxentry); + } + + public void writeSilentSamples(long lngSampleCount) throws IOException { + writeAudio(new AudioInputStream(new InputStream() { + @Override + public int read() throws IOException { + return 0; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + Arrays.fill(b, off, len, (byte)0); + return len; + } + + @Override + public long skip(long n) throws IOException { + return n; + } + }, _audioFormat, lngSampleCount)); + } + + protected void writeFrameChunk(byte[] abData, int iOfs, int iLen) throws IOException { + if (_aviFile == null) throw new IOException("Avi file is closed"); + + AVIOLDINDEXENTRY idxentry = new AVIOLDINDEXENTRY(); + idxentry.dwOffset = (int)(_aviFile.getFilePointer() - (LIST_movi.getStart() + 4)); + String sChunkId; + if (_blnCompressedVideo) + sChunkId = "00dc"; // dc for compressed frame + else + sChunkId = "00db"; // db for uncompressed frame + idxentry.dwChunkId = AVIstruct.string2int(sChunkId); + idxentry.dwFlags = AVIOLDINDEX.AVIIF_KEYFRAME; // Write the flags - select AVIIF_KEYFRAME + // AVIIF_KEYFRAME 0x00000010L + // The flag indicates key frames in the video sequence. + Chunk data_size = new Chunk(_aviFile, sChunkId); + + // write the data, then pad to 4 byte boundary + _aviFile.write(abData, iOfs, iLen); + padTo4ByteBoundary(iLen); - // write the data, padded to 4 byte boundary - int remaint = (4 - (iLen % 4)) % 4; - raFile.write(abData, iOfs, iLen); - while (remaint > 0) { raFile.write(0); remaint--; } // end the chunk - data_size.endChunk(raFile); + data_size.endChunk(_aviFile); + _lngFrameCount++; + // add the index to the list idxentry.dwSize = (int)data_size.getSize(); _indexList.add(idxentry); } - + + abstract public void writeBlankFrame() throws IOException; + + private void padTo4ByteBoundary(int iBytesWritten) throws IOException { + int remaint = (4 - (iBytesWritten % 4)) % 4; + while (remaint > 0) { _aviFile.write(0); remaint--; } + } + + // ------------------------------------------------------------------------- + // -- Close ---------------------------------------------------------------- + // ------------------------------------------------------------------------- + /** I'm tempted to remove the IOException throw, but Java's * RandomAccessFile can also throw an IOException on close(). */ public void close() throws IOException { - if (raFile == null) throw new IOException("Avi file is closed"); + if (_aviFile == null) throw new IOException("Avi file is closed"); - LIST_movi.endChunk(raFile); + LIST_movi.endChunk(_aviFile); // write idx avioldidx = new AVIOLDINDEX(_indexList.toArray(new AVIOLDINDEXENTRY[_indexList.size()])); - avioldidx.write(raFile); + avioldidx.write(_aviFile); // /write idx - RIFF_chunk.endChunk(raFile); + _RIFF_chunk.endChunk(_aviFile); //###################################################################### //## Fill the headers fields ########################################### @@ -630,28 +469,28 @@ public void close() throws IOException { //avih.fcc = 'avih'; // the avih sub-CHUNK //avih.cb = 0x38; // the length of the avih sub-CHUNK (38H) not including the // the first 8 bytes for avihSignature and the length - avih.dwMicroSecPerFrame = (int)((_lngPerSecond/(double)_lngFrames)*1.0e6); - avih.dwMaxBytesPerSec = 0; // (maximum data rate of the file in bytes per second) - avih.dwPaddingGranularity = 0; - avih.dwFlags = AVIMAINHEADER.AVIF_HASINDEX | + _avih.dwMicroSecPerFrame = (int)((_lngPerSecond/(double)_lngFrames)*1.0e6); + _avih.dwMaxBytesPerSec = 0; // (maximum data rate of the file in bytes per second) + _avih.dwPaddingGranularity = 0; + _avih.dwFlags = AVIMAINHEADER.AVIF_HASINDEX | AVIMAINHEADER.AVIF_ISINTERLEAVED; // just set the bit for AVIF_HASINDEX // 10H AVIF_HASINDEX: The AVI file has an idx1 chunk containing // an index at the end of the file. For good performance, all // AVI files should contain an index. - avih.dwTotalFrames = _lngFrameCount; // total frame number - avih.dwInitialFrames = 0; // Initial frame for interleaved files. + _avih.dwTotalFrames = _lngFrameCount; // total frame number + _avih.dwInitialFrames = 0; // Initial frame for interleaved files. // Noninterleaved files should specify 0. if (_audioFormat == null) - avih.dwStreams = 1; // number of streams in the file - here 1 video and zero audio. + _avih.dwStreams = 1; // number of streams in the file - here 1 video and zero audio. else - avih.dwStreams = 2; // number of streams in the file - here 1 video and zero audio. - avih.dwSuggestedBufferSize = 0; // Suggested buffer size for reading the file. + _avih.dwStreams = 2; // number of streams in the file - here 1 video and zero audio. + _avih.dwSuggestedBufferSize = 0; // Suggested buffer size for reading the file. // Generally, this size should be large enough to contain the largest // chunk in the file. // dwSuggestedBufferSize - Suggested buffer size for reading the file. - avih.dwWidth = _iWidth; // image width in pixels - avih.dwHeight = _iHeight; // image height in pixels + _avih.dwWidth = _iWidth; // image width in pixels + _avih.dwHeight = _iHeight; // image height in pixels //avih.dwReserved1 = 0; // Microsoft says to set the following 4 values to 0. //avih.dwReserved2 = 0; // //avih.dwReserved3 = 0; // @@ -663,35 +502,32 @@ public void close() throws IOException { //strh_vid.fcc = 'strh'; // strh sub-CHUNK //strh_vid.cb = 56; // the length of the strh sub-CHUNK - strh_vid.fccType = AVIstruct.string2int("vids"); // the type of data stream - here vids for video stream + _strh_vid.fccType = AVIstruct.string2int("vids"); // the type of data stream - here vids for video stream // Write DIB for Microsoft Device Independent Bitmap. Note: Unfortunately, // at least 3 other four character codes are sometimes used for uncompressed // AVI videos: 'RGB ', 'RAW ', 0x00000000 - if (_blnMJPG) - strh_vid.fccHandler = AVIstruct.string2int("MJPG"); - else - strh_vid.fccHandler = AVIstruct.string2int("DIB "); - strh_vid.dwFlags = 0; - strh_vid.wPriority = 0; - strh_vid.wLanguage = 0; - strh_vid.dwInitialFrames = 0; - strh_vid.dwScale = _lngPerSecond; - strh_vid.dwRate = _lngFrames; // frame rate for video streams - strh_vid.dwStart = 0; // this field is usually set to zero - strh_vid.dwLength = _lngFrameCount; // playing time of AVI file as defined by scale and rate + _strh_vid.fccHandler = AVIstruct.string2int(_sFourCCcodec); + _strh_vid.dwFlags = 0; + _strh_vid.wPriority = 0; + _strh_vid.wLanguage = 0; + _strh_vid.dwInitialFrames = 0; + _strh_vid.dwScale = _lngPerSecond; + _strh_vid.dwRate = _lngFrames; // frame rate for video streams + _strh_vid.dwStart = 0; // this field is usually set to zero + _strh_vid.dwLength = _lngFrameCount; // playing time of AVI file as defined by scale and rate // Set equal to the number of frames // TODO: Add a sugested buffer size - strh_vid.dwSuggestedBufferSize = 0; // Suggested buffer size for reading the stream. + _strh_vid.dwSuggestedBufferSize = 0; // Suggested buffer size for reading the stream. // Typically, this contains a value corresponding to the largest chunk // in a stream. - strh_vid.dwQuality = -1; // encoding quality given by an integer between + _strh_vid.dwQuality = -1; // encoding quality given by an integer between // 0 and 10,000. If set to -1, drivers use the default // quality value. - strh_vid.dwSampleSize = 0; - strh_vid.left = 0; - strh_vid.top = 0; - strh_vid.right = (short)_iWidth; // virtualdub uses width - strh_vid.bottom = (short)_iHeight; // virtualdub uses height + _strh_vid.dwSampleSize = 0; + _strh_vid.left = 0; + _strh_vid.top = 0; + _strh_vid.right = (short)_iWidth; // virtualdub uses width + _strh_vid.bottom = (short)_iHeight; // virtualdub uses height //###################################################################### // BITMAPINFOHEADER @@ -699,25 +535,26 @@ public void close() throws IOException { //bif.biSize = 40; // Write header size of BITMAPINFO header structure // Applications should use this size to determine which BITMAPINFO header structure is // being used. This size includes this biSize field. - bif.biWidth = _iWidth; // BITMAP width in pixels - bif.biHeight = _iHeight; // image height in pixels. If height is positive, + _bif.biWidth = _iWidth; // BITMAP width in pixels + _bif.biHeight = _iHeight; // image height in pixels. If height is positive, // the bitmap is a bottom up DIB and its origin is in the lower left corner. If // height is negative, the bitmap is a top-down DIB and its origin is the upper // left corner. This negative sign feature is supported by the Windows Media Player, but it is not // supported by PowerPoint. //bif.biPlanes = 1; // biPlanes - number of color planes in which the data is stored // This must be set to 1. - bif.biBitCount = 24; // biBitCount - number of bits per pixel # - if (_blnMJPG) // 0L for BI_RGB, uncompressed data as bitmap - bif.biCompression = AVIstruct.string2int("MJPG"); - else // type of compression used - bif.biCompression = BITMAPINFOHEADER.BI_RGB; - bif.biSizeImage = 0; - bif.biXPelsPerMeter = 0; // horizontal resolution in pixels - bif.biYPelsPerMeter = 0; // vertical resolution in pixels + _bif.biBitCount = 24; // biBitCount - number of bits per pixel # + + // 0L for BI_RGB, uncompressed data as bitmap + // or type of compression used + _bif.biCompression = _iCompression; + + _bif.biSizeImage = 0; + _bif.biXPelsPerMeter = 0; // horizontal resolution in pixels + _bif.biYPelsPerMeter = 0; // vertical resolution in pixels // per meter - bif.biClrUsed = 0; // - bif.biClrImportant = 0; // biClrImportant - specifies that the first x colors of the color table + _bif.biClrUsed = 0; // + _bif.biClrImportant = 0; // biClrImportant - specifies that the first x colors of the color table // are important to the DIB. If the rest of the colors are not available, // the image still retains its meaning in an acceptable manner. When this // field is set to zero, all the colors are important, or, rather, their @@ -729,40 +566,40 @@ public void close() throws IOException { if (_audioFormat != null) { //strh.fcc = 'strh'; // strh sub-CHUNK //strh.cb = 56; // length of the strh sub-CHUNK - strh_aud.fccType = AVIstruct.string2int("auds"); // Write the type of data stream - here auds for audio stream - strh_aud.fccHandler = 0; // no fccHandler for wav - strh_aud.dwFlags = 0; - strh_aud.wPriority = 0; - strh_aud.wLanguage = 0; - strh_aud.dwInitialFrames = 1; // virtualdub uses 1 - strh_aud.dwScale = 1; - strh_aud.dwRate = (int)_audioFormat.getSampleRate(); // sample rate for audio streams - strh_aud.dwStart = 0; // this field is usually set to zero + _strh_aud.fccType = AVIstruct.string2int("auds"); // Write the type of data stream - here auds for audio stream + _strh_aud.fccHandler = 0; // no fccHandler for wav + _strh_aud.dwFlags = 0; + _strh_aud.wPriority = 0; + _strh_aud.wLanguage = 0; + _strh_aud.dwInitialFrames = 1; // virtualdub uses 1 + _strh_aud.dwScale = 1; + _strh_aud.dwRate = (int)_audioFormat.getSampleRate(); // sample rate for audio streams + _strh_aud.dwStart = 0; // this field is usually set to zero // FIXME: for some reason virtualdub has a different dwLength value - strh_aud.dwLength = (int)_dblSampleCount; // playing time of AVI file as defined by scale and rate + _strh_aud.dwLength = (int)_lngSampleCount; // playing time of AVI file as defined by scale and rate // Set equal to the number of audio samples in file? // TODO: Add suggested audio buffer size - strh_aud.dwSuggestedBufferSize = 0; // Suggested buffer size for reading the stream. + _strh_aud.dwSuggestedBufferSize = 0; // Suggested buffer size for reading the stream. // Typically, this contains a value corresponding to the largest chunk // in a stream. - strh_aud.dwQuality = -1; // encoding quality given by an integer between + _strh_aud.dwQuality = -1; // encoding quality given by an integer between // 0 and 10,000. If set to -1, drivers use the default // quality value. - strh_aud.dwSampleSize = _audioFormat.getFrameSize(); - strh_aud.left = 0; - strh_aud.top = 0; - strh_aud.right = 0; - strh_aud.bottom = 0; + _strh_aud.dwSampleSize = _audioFormat.getFrameSize(); + _strh_aud.left = 0; + _strh_aud.top = 0; + _strh_aud.right = 0; + _strh_aud.bottom = 0; //###################################################################### // WAVEFORMATEX - wavfmt.wFormatTag = WAVEFORMATEX.WAVE_FORMAT_PCM; - wavfmt.nChannels = (short)_audioFormat.getChannels(); - wavfmt.nSamplesPerSec = (int)_audioFormat.getFrameRate(); - wavfmt.nAvgBytesPerSec = _audioFormat.getFrameSize() * wavfmt.nSamplesPerSec; - wavfmt.nBlockAlign = (short)_audioFormat.getFrameSize(); - wavfmt.wBitsPerSample = (short) _audioFormat.getSampleSizeInBits(); + _wavfmt.wFormatTag = WAVEFORMATEX.WAVE_FORMAT_PCM; + _wavfmt.nChannels = (short)_audioFormat.getChannels(); + _wavfmt.nSamplesPerSec = (int)_audioFormat.getFrameRate(); + _wavfmt.nAvgBytesPerSec = _audioFormat.getFrameSize() * _wavfmt.nSamplesPerSec; + _wavfmt.nBlockAlign = (short)_audioFormat.getFrameSize(); + _wavfmt.wBitsPerSample = (short) _audioFormat.getSampleSizeInBits(); //wavfmt.cbSize = 0; // not written } @@ -772,123 +609,38 @@ public void close() throws IOException { //###################################################################### // go back and write the headers - avih.goBackAndWrite(raFile); - strh_vid.goBackAndWrite(raFile); - bif.goBackAndWrite(raFile); + _avih.goBackAndWrite(_aviFile); + _strh_vid.goBackAndWrite(_aviFile); + _bif.goBackAndWrite(_aviFile); if (_audioFormat != null) { - strh_aud.goBackAndWrite(raFile); - wavfmt.goBackAndWrite(raFile); + _strh_aud.goBackAndWrite(_aviFile); + _wavfmt.goBackAndWrite(_aviFile); } // and we're done - raFile.close(); - raFile = null; - - RIFF_chunk = null; - LIST_hdr1 = null; - avih = null; - LIST_strl_vid = null; - strf_vid = null; - strh_vid = null; - bif = null; - LIST_strl_aud = null; - strf_aud = null; - strh_aud = null; - wavfmt = null; + _aviFile.close(); + _aviFile = null; + + _RIFF_chunk = null; + _LIST_hdr1 = null; + _avih = null; + _LIST_strl_vid = null; + _strf_vid = null; + _strh_vid = null; + _bif = null; + _LIST_strl_aud = null; + _strf_aud = null; + _strh_aud = null; + _wavfmt = null; LIST_movi = null; avioldidx = null; } - + // ------------------------------------------------------------------------- // -- Private functions ---------------------------------------------------- // ------------------------------------------------------------------------- - private byte[] image2DIB(BufferedImage bmp, int iSize) throws IOException { - // first make sure this is a 24 bit RGB image - ColorModel cm = bmp.getColorModel(); - if (bmp.getType() != BufferedImage.TYPE_3BYTE_BGR) { - // if not, convert it - BufferedImage buffer = new BufferedImage( bmp.getWidth(), bmp.getHeight(), BufferedImage.TYPE_3BYTE_BGR); - Graphics2D g = buffer.createGraphics(); - g.drawImage(bmp, 0, 0, null); - g.dispose(); - bmp = buffer; - } - - ExposedBAOS oDIBstream; - if (iSize <= 32) - oDIBstream = writeImageToBytes(bmp, new ExposedBAOS()); - else - // use a ByteArrayOutputStream with the same - // initial size as the last frame (saves time and memory re-allocation) - oDIBstream = writeImageToBytes(bmp, new ExposedBAOS(iSize + 54)); - // get the 'bfOffBits' value, which says where the - // image data actually starts (should be 54) - int iDataStart = read32LE(oDIBstream.getBuffer(), 10); - // return the data from that byte onward - int iBufferSize = oDIBstream.size() - iDataStart; - if (_abWriteBuffer == null || _abWriteBuffer.length < iBufferSize) - _abWriteBuffer = new byte[iBufferSize]; - System.arraycopy(oDIBstream.getBuffer(), iDataStart, _abWriteBuffer, 0, iBufferSize); - return _abWriteBuffer; - } - - /** Read a 32 little-endian value from a position in an array. */ - private static int read32LE(byte[] ab, int iPos) { - return ( (ab[iPos+0] & 0xFF) ) | - ( (ab[iPos+1] & 0xFF) << 8 ) | - ( (ab[iPos+2] & 0xFF) << 16) | - ( (ab[iPos+3] & 0xFF) << 24); - } - - /** Converts a BufferedImage into a frame to be written into a MJPG avi. */ - private ExposedBAOS image2MJPEG(BufferedImage img) throws IOException { - ExposedBAOS oJpgStream = writeImageToBytes(img, new ExposedBAOS()); - //IO.writeFile("test.bin", abJpg); // debug - JPEG2MJPEG(oJpgStream.getBuffer()); - return oJpgStream; - } - - private ExposedBAOS writeImageToBytes(BufferedImage img, ExposedBAOS oOut) throws IOException { - // wrap the ByteArrayOutputStream with a MemoryCacheImageOutputStream - MemoryCacheImageOutputStream oMemOut = new MemoryCacheImageOutputStream(oOut); - // set our image writer's output stream - _imgWriter.setOutput(oMemOut); - - // wrap the BufferedImage with a IIOImage - IIOImage oImgIO = new IIOImage(img, null, null); - // finally write the buffered image to the output stream - // using our parameters (if any) - _imgWriter.write(null, oImgIO, _writeParams); - // don't forget to flush - oMemOut.flush(); - oMemOut.close(); - - // clear image writer's output stream - _imgWriter.setOutput(null); - - // return the result - return oOut; - } - - - /** Converts JPEG file data to be used in an MJPG AVI. */ - private static void JPEG2MJPEG(byte [] ab) throws IOException { - if (ab[6] != 'J' || ab[7] != 'F' || ab[8] != 'I' || ab[9] != 'F') - throw new IOException("JFIF header not found in jpeg data, unable to write frame to AVI."); - // http://cekirdek.pardus.org.tr/~ismail/ffmpeg-docs/mjpegdec_8c-source.html#l00869 - // ffmpeg treats the JFIF and AVI1 header differently. It's probably - // safer to stick with standard JFIF header since that's what JPEG uses. - /* - ab[6] = 'A'; - ab[7] = 'V'; - ab[8] = 'I'; - ab[9] = '1'; - */ - } - - /** Represents an AVI 'chunk'. When created, it saves the current * position in the AVI RandomAccessFile. When endChunk() is called, * it temporarily jumps back to the start of the chunk and records how diff --git a/src/jpsxdec/util/aviwriter/AviWriterDIB.java b/src/jpsxdec/util/aviwriter/AviWriterDIB.java new file mode 100644 index 0000000..85fe3ee --- /dev/null +++ b/src/jpsxdec/util/aviwriter/AviWriterDIB.java @@ -0,0 +1,166 @@ +/* + * jPSXdec: PlayStation 1 Media Decoder/Converter in Java + * Copyright (C) 2007-2010 Michael Sabin + * All rights reserved. + * + * Redistribution and use of the jPSXdec code or any derivative works are + * permitted provided that the following conditions are met: + * + * * Redistributions may not be sold, nor may they be used in commercial + * or revenue-generating business activities. + * + * * Redistributions that are modified from the original source must + * include the complete source code, including the source code for all + * components used by a binary built from the modified sources. However, as + * a special exception, the source code distributed need not include + * anything that is normally distributed (in either source or binary form) + * with the major components (compiler, kernel, and so on) of the operating + * system on which the executable runs, unless that component itself + * accompanies the executable. + * + * * Redistributions must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package jpsxdec.util.aviwriter; + +import java.io.IOException; +import java.io.File; +import java.util.Arrays; +import javax.sound.sampled.AudioFormat; + +/** + * Uncompressed Device Independent Bitmap (DIB) implementation of AVI writer. + * Writing frames via BufferedImages is removed due to the often unpredictable + * and mysterious ColorSpace conversions that can happen depending on how + * the data is accessed. + */ +public class AviWriterDIB extends AviWriter { + + /** Size of the frame data in bytes. Only applicable to DIB AVI. + * Each DIB frame submitted is compared to this value to ensure + * proper data. */ + private final int _iFrameByteSize; + private final int _iLinePadding; + + // ------------------------------------------------------------------------- + // -- Constructors --------------------------------------------------------- + // ------------------------------------------------------------------------- + + public AviWriterDIB(final File oOutputfile, + final int iWidth, final int iHeight, + final long lngFrames, final long lngPerSecond) + throws IOException + { + this(oOutputfile, + iWidth, iHeight, + lngFrames, lngPerSecond, + null); + } + + /** Audio data must be signed 16-bit PCM in little-endian order. */ + public AviWriterDIB(final File oOutputfile, + final int iWidth, final int iHeight, + final long lngFrames, final long lngPerSecond, + final AudioFormat oAudioFormat) + throws IOException + { + super(oOutputfile, iWidth, iHeight, lngFrames, lngPerSecond, oAudioFormat, true, "DIB ", BITMAPINFOHEADER.BI_RGB); + + int iLinePadding = (getWidth() * 3) & 3; + if (iLinePadding != 0) + iLinePadding = 4 - iLinePadding; + _iLinePadding = iLinePadding; + + _iFrameByteSize = (getWidth() * 3 + iLinePadding) * getHeight() ; + } + + // ------------------------------------------------------------------------- + // -- Writing functions ---------------------------------------------------- + // ------------------------------------------------------------------------- + + /** @param abData RGB image data stored at 24 bits/pixel (3 bytes/pixel) */ + public void writeFrameRGB(byte[] abData, int iStart, int iLineStride) throws IOException { + final int WIDTH = getWidth(), HEIGHT = getHeight(); + + if (_abWriteBuffer == null || _abWriteBuffer.length < _iFrameByteSize) + _abWriteBuffer = new byte[_iFrameByteSize]; + + int iSrcLine = iStart + (HEIGHT-1) * iLineStride; + int iDestPos = 0; + for (int y = HEIGHT-1; y >= 0; y--) { + int iSrcPos = iSrcLine; + for (int x = 0; x < WIDTH; x++) { + _abWriteBuffer[iDestPos] = abData[iSrcPos+2]; + iDestPos++; + _abWriteBuffer[iDestPos] = abData[iSrcPos+1]; + iDestPos++; + _abWriteBuffer[iDestPos] = abData[iSrcPos+0]; + iDestPos++; + iSrcPos+=3; + } + iSrcLine -= iLineStride; + for (int i = 0; i < _iLinePadding; i++) { + _abWriteBuffer[iDestPos] = 0; + iDestPos++; + } + } + + writeFrameChunk(_abWriteBuffer, 0, _iFrameByteSize); + } + + /** @param abData RGB image data stored at RGB in the lower bytes of an int. */ + public void writeFrameRGB(int[] aiData, int iStart, int iLineStride) throws IOException { + final int WIDTH = getWidth(), HEIGHT = getHeight(); + + if (_abWriteBuffer == null || _abWriteBuffer.length < _iFrameByteSize) + _abWriteBuffer = new byte[_iFrameByteSize]; + + int iSrcLine = iStart + (HEIGHT-1) * iLineStride; + int iDestPos = 0; + for (int y = HEIGHT-1; y >= 0; y--) { + int iSrcPos = iSrcLine; + for (int x = 0; x < WIDTH; x++) { + int c = aiData[iSrcPos]; + _abWriteBuffer[iDestPos] = (byte)(c ); + iDestPos++; + _abWriteBuffer[iDestPos] = (byte)(c >> 8); + iDestPos++; + _abWriteBuffer[iDestPos] = (byte)(c >> 16); + iDestPos++; + iSrcPos++; + } + iSrcLine -= iLineStride; + for (int i = 0; i < _iLinePadding; i++) { + _abWriteBuffer[iDestPos] = 0; + iDestPos++; + } + } + + writeFrameChunk(_abWriteBuffer, 0, _iFrameByteSize); + } + + @Override + public void writeBlankFrame() throws IOException { + if (_abWriteBuffer == null || _abWriteBuffer.length < _iFrameByteSize) + _abWriteBuffer = new byte[_iFrameByteSize]; + + Arrays.fill(_abWriteBuffer, 0, _iFrameByteSize, (byte)0); + + writeFrameChunk(_abWriteBuffer, 0, _iFrameByteSize); + } + +} diff --git a/src/jpsxdec/util/aviwriter/AviWriterMJPG.java b/src/jpsxdec/util/aviwriter/AviWriterMJPG.java new file mode 100644 index 0000000..f2f7278 --- /dev/null +++ b/src/jpsxdec/util/aviwriter/AviWriterMJPG.java @@ -0,0 +1,232 @@ +/* + * jPSXdec: PlayStation 1 Media Decoder/Converter in Java + * Copyright (C) 2007-2010 Michael Sabin + * All rights reserved. + * + * Redistribution and use of the jPSXdec code or any derivative works are + * permitted provided that the following conditions are met: + * + * * Redistributions may not be sold, nor may they be used in commercial + * or revenue-generating business activities. + * + * * Redistributions that are modified from the original source must + * include the complete source code, including the source code for all + * components used by a binary built from the modified sources. However, as + * a special exception, the source code distributed need not include + * anything that is normally distributed (in either source or binary form) + * with the major components (compiler, kernel, and so on) of the operating + * system on which the executable runs, unless that component itself + * accompanies the executable. + * + * * Redistributions must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package jpsxdec.util.aviwriter; + +import java.awt.image.BufferedImage; +import javax.imageio.ImageIO; +import java.io.IOException; +import java.io.File; +import java.util.Iterator; +import javax.imageio.IIOImage; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.MemoryCacheImageOutputStream; +import javax.sound.sampled.AudioFormat; +import jpsxdec.util.ExposedBAOS; + +/** + * MJPG implementation of AVI writer. It's really just a JPEG file stuffed + * into each frame. The output seems playable on vanilla Windows XP systems, + * and of course VLC. + *

+ * However, according to Wikipedia: + *

+ * "there is no document that defines a single exact format that is + * universally recognized as a complete specification of “Motion JPEG” for + * use in all contexts." + *
+ *

+ * I owe my MJPG understanding to the jpegtoavi program. + * http://sourceforge.net/projects/jpegtoavi/ + *

+ * Random list of codecs + * http://www.oltenia.ro/download/pub/windows/media/video/tools/GSpot/gspot22/GSpot22.dat + *

+ * http://www.alexander-noe.com/video/documentation/avi.pdf + *

+ * According to the Multimedia Guru, AVI can support variable frame rates. + * http://guru.multimedia.cx/the-avi-container-file-format/ + */ +public class AviWriterMJPG extends AviWriter { + + /** Hold's true if system can write "jpeg" images. */ + private final static boolean CAN_ENCODE_JPEG; + static { + // check if the system can write "jpeg" images + boolean bln = false; + for (String s : ImageIO.getReaderFormatNames()) { + if (s.equals("jpeg")) { + bln = true; + break; + } + } + CAN_ENCODE_JPEG = bln; + } + + // ------------------------------------------------------------------------- + // -- Fields --------------------------------------------------------------- + // ------------------------------------------------------------------------- + + + /** The image writer used to convert the BufferedImages to BMP or JPEG. */ + private final ImageWriter _imgWriter; + /** Only used for MJPG when not using default quality level. */ + private final ImageWriteParam _writeParams; + + + // ------------------------------------------------------------------------- + // -- Constructors --------------------------------------------------------- + // ------------------------------------------------------------------------- + + /** Audio data must be signed 16-bit PCM in little-endian order. */ + public AviWriterMJPG(final File oOutputfile, + final int iWidth, final int iHeight, + final long lngFrames, final long lngPerSecond) + throws IOException + { + this(oOutputfile, iWidth, iHeight, lngFrames, lngPerSecond, -1, null); + } + /** Audio data must be signed 16-bit PCM in little-endian order. */ + public AviWriterMJPG(final File oOutputfile, + final int iWidth, final int iHeight, + final long lngFrames, final long lngPerSecond, + final AudioFormat oAudioFormat) + throws IOException + { + this(oOutputfile, iWidth, iHeight, lngFrames, lngPerSecond, -1, oAudioFormat); + } + public AviWriterMJPG(final File oOutputfile, + final int iWidth, final int iHeight, + final long lngFrames, final long lngPerSecond, + final float fltLossyQuality) + throws IOException + { + this(oOutputfile, iWidth, iHeight, lngFrames, lngPerSecond, fltLossyQuality, null); + } + public AviWriterMJPG(final File oOutputfile, + final int iWidth, final int iHeight, + final long lngFrames, final long lngPerSecond, + final float fltLossyQuality, + final AudioFormat oAudioFormat) + throws IOException + { + super(oOutputfile, iWidth, iHeight, lngFrames, lngPerSecond, oAudioFormat, true, "MJPG", AVIstruct.string2int("MJPG")); + + if (!CAN_ENCODE_JPEG) + throw new UnsupportedOperationException("Unable to create 'jpeg' images on this platform."); + + Iterator oIter = ImageIO.getImageWritersByFormatName("jpeg"); + _imgWriter = oIter.next(); + + if (fltLossyQuality < 0 || fltLossyQuality > 1) { + _writeParams = null; + } else { + // TODO: Make sure thumbnails are not being created in the jpegs + _writeParams = _imgWriter.getDefaultWriteParam(); + + _writeParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + // 0 for lowest qulaity, 1 for highest + _writeParams.setCompressionQuality(fltLossyQuality); + } + + } + + // ------------------------------------------------------------------------- + // -- Writing functions ---------------------------------------------------- + // ------------------------------------------------------------------------- + + /** Converts a BufferedImage to proper avi format and writes it. */ + public void writeFrame(BufferedImage bi) throws IOException { + if (getWidth() != bi.getWidth()) + throw new IllegalArgumentException("AviWriter: Frame width doesn't match" + + " (was " + getWidth() + ", now " + bi.getWidth() + ")."); + + if (getHeight() != bi.getHeight()) + throw new IllegalArgumentException("AviWriter: Frame height doesn't match" + + " (was " + getHeight() + ", now " + bi.getHeight() + ")."); + + byte[] abData = image2MJPEG(bi).getBuffer(); + + writeFrameChunk(abData, 0, abData.length); + } + + // ------------------------------------------------------------------------- + // -- Private functions ---------------------------------------------------- + // ------------------------------------------------------------------------- + + private ExposedBAOS writeImageToBytes(BufferedImage img, ExposedBAOS out) throws IOException { + // have to wrap the ByteArrayOutputStream with a MemoryCacheImageOutputStream + MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(out); + // set our image writer's output stream + _imgWriter.setOutput(imgOut); + + // wrap the BufferedImage with a IIOImage + IIOImage imgIO = new IIOImage(img, null, null); + // finally write the buffered image to the output stream + // using our parameters (if any) + _imgWriter.write(null, imgIO, _writeParams); + // don't forget to flush + imgOut.flush(); + imgOut.close(); + + // clear image writer's output stream + _imgWriter.setOutput(null); + + // return the result + return out; + } + + /** Converts a BufferedImage into a frame to be written into a MJPG avi. */ + private ExposedBAOS image2MJPEG(BufferedImage img) throws IOException { + ExposedBAOS oJpgStream = writeImageToBytes(img, new ExposedBAOS()); + //IO.writeFile("test.bin", abJpg); // debug + JPEG2MJPEG(oJpgStream.getBuffer()); + return oJpgStream; + } + + /** Converts JPEG file data to be used in an MJPG AVI. */ + private static void JPEG2MJPEG(byte [] ab) throws IOException { + if (ab[6] != 'J' || ab[7] != 'F' || ab[8] != 'I' || ab[9] != 'F') + throw new IOException("JFIF header not found in jpeg data, unable to write frame to AVI."); + // http://cekirdek.pardus.org.tr/~ismail/ffmpeg-docs/mjpegdec_8c-source.html#l00869 + // ffmpeg treats the JFIF and AVI1 header differently. It's probably + // safer to stick with standard JFIF header since that's what JPEG uses. + /* + ab[6] = 'A'; + ab[7] = 'V'; + ab[8] = 'I'; + ab[9] = '1'; + */ + } + + @Override + public void writeBlankFrame() throws IOException { + BufferedImage bi = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); + writeFrame(bi); + } +} diff --git a/src/jpsxdec/util/aviwriter/AviWriterYV12.java b/src/jpsxdec/util/aviwriter/AviWriterYV12.java new file mode 100644 index 0000000..6b037a8 --- /dev/null +++ b/src/jpsxdec/util/aviwriter/AviWriterYV12.java @@ -0,0 +1,77 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package jpsxdec.util.aviwriter; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import javax.sound.sampled.AudioFormat; + + +public class AviWriterYV12 extends AviWriter { + + private final int _iFrameSize, _iFrameYSize, _iFrameCSize; + + public AviWriterYV12(final File outFile, + final int iWidth, final int iHeight, + final long lngFrames, final long lngPerSecond) + throws IOException + { + this(outFile, + iWidth, iHeight, + lngFrames, lngPerSecond, + null); + } + + /** Audio data must be signed 16-bit PCM in little-endian order. */ + public AviWriterYV12(final File outFile, + final int iWidth, final int iHeight, + final long lngFrames, final long lngPerSecond, + final AudioFormat oAudioFormat) + throws IOException + { + super(outFile, iWidth, iHeight, lngFrames, lngPerSecond, oAudioFormat, + false, "YV12", AVIstruct.string2int("YV12")); + + if (((iWidth | iHeight) & 1) != 0) + throw new IllegalArgumentException("Dimensions must be divisible by 2"); + + _iFrameYSize = iWidth * iHeight; + _iFrameCSize = iWidth * iHeight / 4; + + _iFrameSize = _iFrameYSize + _iFrameCSize * 2; + } + + public void write(byte[] abY, byte[] abCr, byte[] abCb) throws IOException { + if (_abWriteBuffer == null || _abWriteBuffer.length < _iFrameSize) + _abWriteBuffer = new byte[_iFrameSize]; + + if (abY.length != _iFrameYSize) + throw new IllegalArgumentException("Y data wrong size."); + if (abCb.length != _iFrameCSize) + throw new IllegalArgumentException("Cb data wrong size."); + if (abCr.length != _iFrameCSize) + throw new IllegalArgumentException("Cr data wrong size."); + + System.arraycopy(abY, 0, _abWriteBuffer, 0, _iFrameYSize); + System.arraycopy(abCr, 0, _abWriteBuffer, _iFrameYSize, _iFrameCSize); + System.arraycopy(abCb, 0, _abWriteBuffer, _iFrameYSize+_iFrameCSize, _iFrameCSize); + + writeFrameChunk(_abWriteBuffer, 0, _iFrameSize); + } + + @Override + public void writeBlankFrame() throws IOException { + if (_abWriteBuffer == null || _abWriteBuffer.length < _iFrameSize) + _abWriteBuffer = new byte[_iFrameSize]; + + Arrays.fill(_abWriteBuffer, 0, _iFrameYSize, (byte)0); + Arrays.fill(_abWriteBuffer, _iFrameYSize, _iFrameYSize + _iFrameCSize*2, (byte)128); + + writeFrameChunk(_abWriteBuffer, 0, _iFrameSize); + } + +}