mirror of
https://github.com/0ldsk00l/nestopia.git
synced 2025-04-02 10:31:51 -04:00
458 lines
12 KiB
C++
458 lines
12 KiB
C++
////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Nestopia - NES/Famicom emulator written in C++
|
|
//
|
|
// Copyright (C) 2003-2008 Martin Freij
|
|
//
|
|
// This file is part of Nestopia.
|
|
//
|
|
// Nestopia is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation; either version 2 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Nestopia is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Nestopia; if not, write to the Free Software
|
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "NstIoFile.hpp"
|
|
#include "NstIoStream.hpp"
|
|
#include "NstObjectPod.hpp"
|
|
#include "NstWindowUser.hpp"
|
|
#include "NstManager.hpp"
|
|
#include "NstManagerAviConverter.hpp"
|
|
#include "NstManagerMovie.hpp"
|
|
|
|
#if NST_MSVC
|
|
#pragma comment(lib,"vfw32")
|
|
#endif
|
|
|
|
#if NST_MSVC >= 1200
|
|
#pragma warning( push )
|
|
#pragma warning( disable : 4201 )
|
|
#endif
|
|
|
|
#include <vfw.h>
|
|
|
|
#if NST_MSVC >= 1200
|
|
#pragma warning( pop )
|
|
#endif
|
|
|
|
namespace Nestopia
|
|
{
|
|
namespace Managers
|
|
{
|
|
AviConverter::AviConverter(Emulator& e)
|
|
: emulator(e), on(e.IsOn())
|
|
{
|
|
Nes::Video::Output::lockCallback.Get( nesVideoLockFunc, nesVideoLockData );
|
|
Nes::Video::Output::unlockCallback.Get( nesVideoUnlockFunc, nesVideoUnlockData );
|
|
Nes::Sound::Output::lockCallback.Get( nesSoundLockFunc, nesSoundLockData );
|
|
Nes::Sound::Output::unlockCallback.Get( nesSoundUnlockFunc, nesSoundUnlockData );
|
|
Nes::Movie::eventCallback.Get( nesMovieEventFunc, nesMovieEventData );
|
|
|
|
Nes::Video::Output::lockCallback.Unset();
|
|
Nes::Video::Output::unlockCallback.Unset();
|
|
Nes::Sound::Output::lockCallback.Unset();
|
|
Nes::Sound::Output::unlockCallback.Unset();
|
|
Nes::Movie::eventCallback.Unset();
|
|
|
|
Nes::Video(emulator).GetRenderState( renderState );
|
|
|
|
if (!on)
|
|
emulator.Power( true );
|
|
|
|
if (emulator.IsOn())
|
|
emulator.SaveState( saveState, false, Emulator::QUIETLY );
|
|
}
|
|
|
|
#ifdef NST_MSVC_OPTIMIZE
|
|
#pragma optimize("t", on)
|
|
#endif
|
|
|
|
uint AviConverter::Record(const Path& moviePath,const Path& aviPath) const
|
|
{
|
|
if (moviePath.Empty() || aviPath.Empty() || !emulator.IsOn())
|
|
return 0;
|
|
|
|
try
|
|
{
|
|
class Movie
|
|
{
|
|
Emulator& emulator;
|
|
Io::Stream::In stream;
|
|
|
|
public:
|
|
|
|
explicit Movie(Emulator& e,const Path& path)
|
|
: emulator(e), stream(path) {}
|
|
|
|
uint Play()
|
|
{
|
|
const Nes::Result result = Nes::Movie(emulator).Play( stream );
|
|
return NES_FAILED(result) ? Emulator::ResultToString( result ) : 0;
|
|
}
|
|
|
|
~Movie()
|
|
{
|
|
Nes::Movie(emulator).Stop();
|
|
}
|
|
};
|
|
|
|
struct BitmapInfo : Object::Pod<BITMAPINFOHEADER>
|
|
{
|
|
BitmapInfo(const uint width,const uint height)
|
|
{
|
|
NST_ASSERT( width && height );
|
|
|
|
biSize = sizeof(BITMAPINFOHEADER);
|
|
biWidth = width;
|
|
biHeight = height << uint(width / height >= 2);
|
|
biPlanes = 1;
|
|
biBitCount = VIDEO_BPP;
|
|
biCompression = BI_RGB;
|
|
biSizeImage = VIDEO_BPP/8 * biWidth * biHeight;
|
|
}
|
|
};
|
|
|
|
struct CompressVars : Object::Pod<COMPVARS>
|
|
{
|
|
CompressVars()
|
|
{
|
|
cbSize = sizeof(COMPVARS);
|
|
}
|
|
|
|
~CompressVars()
|
|
{
|
|
::ICCompressorFree( this );
|
|
}
|
|
|
|
BOOL Choose(BitmapInfo& bitmapInfo)
|
|
{
|
|
char title[] = "Choose Video Codec";
|
|
|
|
return ::ICCompressorChoose
|
|
(
|
|
Application::Instance::GetMainWindow(),
|
|
ICMF_CHOOSE_DATARATE|ICMF_CHOOSE_KEYFRAME,
|
|
&bitmapInfo,
|
|
NULL,
|
|
this,
|
|
title
|
|
);
|
|
}
|
|
};
|
|
|
|
class File
|
|
{
|
|
PAVIFILE file;
|
|
mutable wcstring name;
|
|
|
|
public:
|
|
|
|
explicit File(wcstring n)
|
|
: name(n)
|
|
{
|
|
NST_ASSERT( name && *name );
|
|
|
|
::AVIFileInit();
|
|
|
|
if (::AVIFileOpen( &file, name, OF_WRITE|OF_CREATE, NULL ) != AVIERR_OK)
|
|
file = NULL;
|
|
}
|
|
|
|
operator PAVIFILE() const
|
|
{
|
|
return file;
|
|
}
|
|
|
|
void SetSuccess() const
|
|
{
|
|
name = NULL;
|
|
}
|
|
|
|
~File()
|
|
{
|
|
if (file)
|
|
{
|
|
::AVIFileRelease( file );
|
|
|
|
if (name)
|
|
::DeleteFile( name );
|
|
}
|
|
|
|
::AVIFileExit();
|
|
}
|
|
};
|
|
|
|
class Stream
|
|
{
|
|
PAVISTREAM stream;
|
|
|
|
public:
|
|
|
|
Stream()
|
|
: stream(NULL) {}
|
|
|
|
Stream(const File* avi,AVISTREAMINFO& info)
|
|
{
|
|
NST_ASSERT( !avi || bool(*avi) );
|
|
|
|
if (!avi || ::AVIFileCreateStream( *avi, &stream, &info ) != AVIERR_OK)
|
|
stream = NULL;
|
|
}
|
|
|
|
Stream(const File& avi,AVISTREAMINFO& info)
|
|
{
|
|
NST_ASSERT( bool(avi) );
|
|
|
|
if (::AVIFileCreateStream( avi, &stream, &info ) != AVIERR_OK)
|
|
stream = NULL;
|
|
}
|
|
|
|
Stream(const Stream& base,AVICOMPRESSOPTIONS& options)
|
|
{
|
|
if (::AVIMakeCompressedStream( &stream, base, &options, NULL ) != AVIERR_OK)
|
|
stream = NULL;
|
|
}
|
|
|
|
operator PAVISTREAM() const
|
|
{
|
|
return stream;
|
|
}
|
|
|
|
bool SetFormat(void* info,uint size) const
|
|
{
|
|
NST_ASSERT( stream && info && size );
|
|
|
|
return ::AVIStreamSetFormat( stream, 0, info, size ) == AVIERR_OK;
|
|
}
|
|
|
|
~Stream()
|
|
{
|
|
if (stream)
|
|
::AVIStreamRelease( stream );
|
|
}
|
|
};
|
|
|
|
struct CompressOptions : Object::Pod<AVICOMPRESSOPTIONS>
|
|
{
|
|
CompressOptions(const CompressVars& compressVars)
|
|
{
|
|
fccType = streamtypeVIDEO;
|
|
fccHandler = compressVars.fccHandler;
|
|
dwKeyFrameEvery = compressVars.lKey;
|
|
dwQuality = compressVars.lQ;
|
|
dwBytesPerSecond = compressVars.lDataRate;
|
|
|
|
if (compressVars.lDataRate > 0)
|
|
dwFlags |= AVICOMPRESSF_DATARATE;
|
|
|
|
if (compressVars.lKey > 0)
|
|
dwFlags |= AVICOMPRESSF_KEYFRAMES;
|
|
}
|
|
};
|
|
|
|
struct VideoInfo : Object::Pod<AVISTREAMINFO>
|
|
{
|
|
VideoInfo(Emulator& emulator,const BitmapInfo& bitmapInfo,const CompressVars& compressVars)
|
|
{
|
|
fccType = streamtypeVIDEO;
|
|
fccHandler = compressVars.fccHandler;
|
|
dwScale = 1;
|
|
dwRate = emulator.GetDefaultSpeed();
|
|
dwQuality = compressVars.lQ;
|
|
dwSuggestedBufferSize = bitmapInfo.biSizeImage;
|
|
rcFrame.right = bitmapInfo.biWidth;
|
|
rcFrame.bottom = bitmapInfo.biHeight;
|
|
}
|
|
};
|
|
|
|
struct WaveFormat : Object::Pod<WAVEFORMATEX>
|
|
{
|
|
WaveFormat(const Nes::Sound nes)
|
|
{
|
|
wFormatTag = WAVE_FORMAT_PCM;
|
|
nSamplesPerSec = nes.GetSampleRate();
|
|
wBitsPerSample = nes.GetSampleBits();
|
|
nChannels = 1 + (nes.GetSpeaker() == Nes::Sound::SPEAKER_STEREO);
|
|
nBlockAlign = wBitsPerSample / 8 * nChannels;
|
|
nAvgBytesPerSec = nSamplesPerSec * nBlockAlign;
|
|
}
|
|
};
|
|
|
|
struct SoundInfo : Object::Pod<AVISTREAMINFO>
|
|
{
|
|
SoundInfo(const WaveFormat& waveFormat)
|
|
{
|
|
fccType = streamtypeAUDIO;
|
|
fccHandler = 1;
|
|
dwScale = waveFormat.nBlockAlign;
|
|
dwInitialFrames = 1;
|
|
dwRate = waveFormat.nAvgBytesPerSec;
|
|
dwQuality = DWORD(-1);
|
|
dwSampleSize = waveFormat.nBlockAlign;
|
|
}
|
|
};
|
|
|
|
struct Buffer
|
|
{
|
|
uchar* const ptr;
|
|
const uint size;
|
|
|
|
Buffer(uint n) : ptr(n ? new uchar [n] : NULL), size(n) {}
|
|
~Buffer() { delete [] ptr; }
|
|
};
|
|
|
|
Movie movie( emulator, moviePath );
|
|
|
|
if (const uint ids = movie.Play())
|
|
return ids;
|
|
|
|
BitmapInfo bitmapInfo( renderState.width, renderState.height );
|
|
CompressVars compressVars;
|
|
|
|
if (!compressVars.Choose( bitmapInfo ))
|
|
return 0;
|
|
|
|
Application::Instance::Waiter wait;
|
|
|
|
const File avi( aviPath.Ptr() );
|
|
|
|
if (!avi)
|
|
return IDS_AVI_WRITE_ERR;
|
|
|
|
VideoInfo videoInfo( emulator, bitmapInfo, compressVars );
|
|
const Stream video( avi, videoInfo );
|
|
|
|
if (!video)
|
|
return IDS_AVI_WRITE_ERR;
|
|
|
|
CompressOptions compressOptions( compressVars );
|
|
const Stream compressor( video, compressOptions );
|
|
|
|
if (!compressor)
|
|
return IDS_AVI_WRITE_ERR;
|
|
|
|
if (!compressor.SetFormat( &bitmapInfo, bitmapInfo.biSize + bitmapInfo.biClrUsed * sizeof(RGBQUAD) ))
|
|
return IDS_AVI_WRITE_ERR;
|
|
|
|
Window::User::Inform( IDS_AVI_WRITE_INFO );
|
|
Application::Instance::Locker locker;
|
|
|
|
WaveFormat waveFormat( emulator );
|
|
SoundInfo soundInfo( waveFormat );
|
|
const Stream sound( Nes::Sound(emulator).IsAudible() ? &avi : NULL, soundInfo );
|
|
|
|
if (Nes::Sound(emulator).IsAudible())
|
|
{
|
|
if (!sound)
|
|
return IDS_AVI_WRITE_ERR;
|
|
|
|
if (!sound.SetFormat( &waveFormat, sizeof(WAVEFORMATEX) ))
|
|
return IDS_AVI_WRITE_ERR;
|
|
}
|
|
|
|
Buffer pixels( bitmapInfo.biSizeImage );
|
|
Buffer samples( waveFormat.nAvgBytesPerSec / videoInfo.dwRate );
|
|
|
|
// picture will be stored upside down, adjust the pitch
|
|
Nes::Video::Output videoOutput( pixels.ptr + (VIDEO_BPP/8 * bitmapInfo.biWidth * (renderState.height-1)), -int(VIDEO_BPP/8 * bitmapInfo.biWidth) );
|
|
Nes::Sound::Output soundOutput( samples.ptr, samples.size / waveFormat.nBlockAlign );
|
|
|
|
{
|
|
Nes::Video::RenderState tmp;
|
|
Nes::Video(emulator).GetRenderState( tmp );
|
|
|
|
tmp.bits.count = VIDEO_BPP;
|
|
tmp.bits.mask.r = VIDEO_R_MASK;
|
|
tmp.bits.mask.g = VIDEO_G_MASK;
|
|
tmp.bits.mask.b = VIDEO_B_MASK;
|
|
|
|
Nes::Video(emulator).SetRenderState( tmp );
|
|
}
|
|
|
|
for (uint frame=0, sample=0, size=0; Nes::Movie(emulator).IsPlaying() && size < MAX_FILE_SIZE; ++frame, sample += soundOutput.length[0])
|
|
{
|
|
::Sleep( 0 );
|
|
|
|
if (locker.CheckInput( VK_ESCAPE ))
|
|
return IDS_AVI_WRITE_ABORT;
|
|
|
|
if (NES_FAILED(emulator.Nes::Emulator::Execute( &videoOutput, sound ? &soundOutput : NULL, NULL )))
|
|
return IDS_AVI_WRITE_ERR;
|
|
|
|
if (bitmapInfo.biHeight == renderState.height * 2)
|
|
{
|
|
const uint pitch = -videoOutput.pitch;
|
|
|
|
for (uchar *src=pixels.ptr + (pitch * (renderState.height-1)), *dst = pixels.ptr + (bitmapInfo.biSizeImage - pitch); ; src -= pitch)
|
|
{
|
|
std::memcpy( dst, src, pitch );
|
|
dst -= pitch;
|
|
|
|
if (dst == pixels.ptr)
|
|
break;
|
|
|
|
std::memcpy( dst, src, pitch );
|
|
dst -= pitch;
|
|
}
|
|
}
|
|
|
|
long written[2] = {0,0};
|
|
|
|
if
|
|
(
|
|
(::AVIStreamWrite( compressor, frame, 1, pixels.ptr, pixels.size, AVIIF_KEYFRAME, NULL, written+0 ) != AVIERR_OK) ||
|
|
(sound && ::AVIStreamWrite( sound, sample, soundOutput.length[0], samples.ptr, samples.size, 0, NULL, written+1 ) != AVIERR_OK)
|
|
)
|
|
return IDS_AVI_WRITE_ERR;
|
|
|
|
size += (written[0] > 0 ? written[0] : 0) + (written[1] > 0 ? written[1] : 0);
|
|
}
|
|
|
|
avi.SetSuccess();
|
|
|
|
return IDS_AVI_WRITE_FINISHED;
|
|
}
|
|
catch (Io::File::Exception id)
|
|
{
|
|
return id;
|
|
}
|
|
catch (...)
|
|
{
|
|
return IDS_ERR_GENERIC;
|
|
}
|
|
}
|
|
|
|
#ifdef NST_MSVC_OPTIMIZE
|
|
#pragma optimize("", on)
|
|
#endif
|
|
|
|
AviConverter::~AviConverter()
|
|
{
|
|
if (saveState.Size())
|
|
emulator.LoadState( saveState, Emulator::QUIETLY );
|
|
|
|
if (!on)
|
|
emulator.Power( false );
|
|
|
|
Nes::Video::Output::lockCallback.Set( nesVideoLockFunc, nesVideoLockData );
|
|
Nes::Video::Output::unlockCallback.Set( nesVideoUnlockFunc, nesVideoUnlockData );
|
|
Nes::Sound::Output::lockCallback.Set( nesSoundLockFunc, nesSoundLockData );
|
|
Nes::Sound::Output::unlockCallback.Set( nesSoundUnlockFunc, nesSoundUnlockData );
|
|
Nes::Movie::eventCallback.Set( nesMovieEventFunc, nesMovieEventData );
|
|
|
|
Nes::Video(emulator).SetRenderState( renderState );
|
|
|
|
Application::Instance::GetMainWindow().Redraw();
|
|
}
|
|
}
|
|
}
|