1 //////////////////////////////////////////////////////////////////////////////////////// 2 // 3 // Nestopia - NES/Famicom emulator written in C++ 4 // 5 // Copyright (C) 2003-2008 Martin Freij 6 // 7 // This file is part of Nestopia. 8 // 9 // Nestopia is free software; you can redistribute it and/or modify 10 // it under the terms of the GNU General Public License as published by 11 // the Free Software Foundation; either version 2 of the License, or 12 // (at your option) any later version. 13 // 14 // Nestopia is distributed in the hope that it will be useful, 15 // but WITHOUT ANY WARRANTY; without even the implied warranty of 16 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 // GNU General Public License for more details. 18 // 19 // You should have received a copy of the GNU General Public License 20 // along with Nestopia; if not, write to the Free Software 21 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 22 // 23 //////////////////////////////////////////////////////////////////////////////////////// 24 25 #include "NstIoFile.hpp" 26 #include "NstIoStream.hpp" 27 #include "NstObjectPod.hpp" 28 #include "NstWindowUser.hpp" 29 #include "NstManager.hpp" 30 #include "NstManagerAviConverter.hpp" 31 #include "NstManagerMovie.hpp" 32 33 #if NST_MSVC 34 #pragma comment(lib,"vfw32") 35 #endif 36 37 #if NST_MSVC >= 1200 38 #pragma warning( push ) 39 #pragma warning( disable : 4201 ) 40 #endif 41 42 #include <vfw.h> 43 44 #if NST_MSVC >= 1200 45 #pragma warning( pop ) 46 #endif 47 48 namespace Nestopia 49 { 50 namespace Managers 51 { AviConverter(Emulator & e)52 AviConverter::AviConverter(Emulator& e) 53 : emulator(e), on(e.IsOn()) 54 { 55 Nes::Video::Output::lockCallback.Get( nesVideoLockFunc, nesVideoLockData ); 56 Nes::Video::Output::unlockCallback.Get( nesVideoUnlockFunc, nesVideoUnlockData ); 57 Nes::Sound::Output::lockCallback.Get( nesSoundLockFunc, nesSoundLockData ); 58 Nes::Sound::Output::unlockCallback.Get( nesSoundUnlockFunc, nesSoundUnlockData ); 59 Nes::Movie::eventCallback.Get( nesMovieEventFunc, nesMovieEventData ); 60 61 Nes::Video::Output::lockCallback.Unset(); 62 Nes::Video::Output::unlockCallback.Unset(); 63 Nes::Sound::Output::lockCallback.Unset(); 64 Nes::Sound::Output::unlockCallback.Unset(); 65 Nes::Movie::eventCallback.Unset(); 66 67 Nes::Video(emulator).GetRenderState( renderState ); 68 69 if (!on) 70 emulator.Power( true ); 71 72 if (emulator.IsOn()) 73 emulator.SaveState( saveState, false, Emulator::QUIETLY ); 74 } 75 76 #ifdef NST_MSVC_OPTIMIZE 77 #pragma optimize("t", on) 78 #endif 79 Record(const Path & moviePath,const Path & aviPath) const80 uint AviConverter::Record(const Path& moviePath,const Path& aviPath) const 81 { 82 if (moviePath.Empty() || aviPath.Empty() || !emulator.IsOn()) 83 return 0; 84 85 try 86 { 87 class Movie 88 { 89 Emulator& emulator; 90 Io::Stream::In stream; 91 92 public: 93 94 explicit Movie(Emulator& e,const Path& path) 95 : emulator(e), stream(path) {} 96 97 uint Play() 98 { 99 const Nes::Result result = Nes::Movie(emulator).Play( stream ); 100 return NES_FAILED(result) ? Emulator::ResultToString( result ) : 0; 101 } 102 103 ~Movie() 104 { 105 Nes::Movie(emulator).Stop(); 106 } 107 }; 108 109 struct BitmapInfo : Object::Pod<BITMAPINFOHEADER> 110 { 111 BitmapInfo(const uint width,const uint height) 112 { 113 NST_ASSERT( width && height ); 114 115 biSize = sizeof(BITMAPINFOHEADER); 116 biWidth = width; 117 biHeight = height << uint(width / height >= 2); 118 biPlanes = 1; 119 biBitCount = VIDEO_BPP; 120 biCompression = BI_RGB; 121 biSizeImage = VIDEO_BPP/8 * biWidth * biHeight; 122 } 123 }; 124 125 struct CompressVars : Object::Pod<COMPVARS> 126 { 127 CompressVars() 128 { 129 cbSize = sizeof(COMPVARS); 130 } 131 132 ~CompressVars() 133 { 134 ::ICCompressorFree( this ); 135 } 136 137 BOOL Choose(BitmapInfo& bitmapInfo) 138 { 139 char title[] = "Choose Video Codec"; 140 141 return ::ICCompressorChoose 142 ( 143 Application::Instance::GetMainWindow(), 144 ICMF_CHOOSE_DATARATE|ICMF_CHOOSE_KEYFRAME, 145 &bitmapInfo, 146 NULL, 147 this, 148 title 149 ); 150 } 151 }; 152 153 class File 154 { 155 PAVIFILE file; 156 mutable wcstring name; 157 158 public: 159 160 explicit File(wcstring n) 161 : name(n) 162 { 163 NST_ASSERT( name && *name ); 164 165 ::AVIFileInit(); 166 167 if (::AVIFileOpen( &file, name, OF_WRITE|OF_CREATE, NULL ) != AVIERR_OK) 168 file = NULL; 169 } 170 171 operator PAVIFILE() const 172 { 173 return file; 174 } 175 176 void SetSuccess() const 177 { 178 name = NULL; 179 } 180 181 ~File() 182 { 183 if (file) 184 { 185 ::AVIFileRelease( file ); 186 187 if (name) 188 ::DeleteFile( name ); 189 } 190 191 ::AVIFileExit(); 192 } 193 }; 194 195 class Stream 196 { 197 PAVISTREAM stream; 198 199 public: 200 201 Stream() 202 : stream(NULL) {} 203 204 Stream(const File* avi,AVISTREAMINFO& info) 205 { 206 NST_ASSERT( !avi || bool(*avi) ); 207 208 if (!avi || ::AVIFileCreateStream( *avi, &stream, &info ) != AVIERR_OK) 209 stream = NULL; 210 } 211 212 Stream(const File& avi,AVISTREAMINFO& info) 213 { 214 NST_ASSERT( bool(avi) ); 215 216 if (::AVIFileCreateStream( avi, &stream, &info ) != AVIERR_OK) 217 stream = NULL; 218 } 219 220 Stream(const Stream& base,AVICOMPRESSOPTIONS& options) 221 { 222 if (::AVIMakeCompressedStream( &stream, base, &options, NULL ) != AVIERR_OK) 223 stream = NULL; 224 } 225 226 operator PAVISTREAM() const 227 { 228 return stream; 229 } 230 231 bool SetFormat(void* info,uint size) const 232 { 233 NST_ASSERT( stream && info && size ); 234 235 return ::AVIStreamSetFormat( stream, 0, info, size ) == AVIERR_OK; 236 } 237 238 ~Stream() 239 { 240 if (stream) 241 ::AVIStreamRelease( stream ); 242 } 243 }; 244 245 struct CompressOptions : Object::Pod<AVICOMPRESSOPTIONS> 246 { 247 CompressOptions(const CompressVars& compressVars) 248 { 249 fccType = streamtypeVIDEO; 250 fccHandler = compressVars.fccHandler; 251 dwKeyFrameEvery = compressVars.lKey; 252 dwQuality = compressVars.lQ; 253 dwBytesPerSecond = compressVars.lDataRate; 254 255 if (compressVars.lDataRate > 0) 256 dwFlags |= AVICOMPRESSF_DATARATE; 257 258 if (compressVars.lKey > 0) 259 dwFlags |= AVICOMPRESSF_KEYFRAMES; 260 } 261 }; 262 263 struct VideoInfo : Object::Pod<AVISTREAMINFO> 264 { 265 VideoInfo(Emulator& emulator,const BitmapInfo& bitmapInfo,const CompressVars& compressVars) 266 { 267 fccType = streamtypeVIDEO; 268 fccHandler = compressVars.fccHandler; 269 dwScale = 1; 270 dwRate = emulator.GetDefaultSpeed(); 271 dwQuality = compressVars.lQ; 272 dwSuggestedBufferSize = bitmapInfo.biSizeImage; 273 rcFrame.right = bitmapInfo.biWidth; 274 rcFrame.bottom = bitmapInfo.biHeight; 275 } 276 }; 277 278 struct WaveFormat : Object::Pod<WAVEFORMATEX> 279 { 280 WaveFormat(const Nes::Sound nes) 281 { 282 wFormatTag = WAVE_FORMAT_PCM; 283 nSamplesPerSec = nes.GetSampleRate(); 284 wBitsPerSample = nes.GetSampleBits(); 285 nChannels = 1 + (nes.GetSpeaker() == Nes::Sound::SPEAKER_STEREO); 286 nBlockAlign = wBitsPerSample / 8 * nChannels; 287 nAvgBytesPerSec = nSamplesPerSec * nBlockAlign; 288 } 289 }; 290 291 struct SoundInfo : Object::Pod<AVISTREAMINFO> 292 { 293 SoundInfo(const WaveFormat& waveFormat) 294 { 295 fccType = streamtypeAUDIO; 296 fccHandler = 1; 297 dwScale = waveFormat.nBlockAlign; 298 dwInitialFrames = 1; 299 dwRate = waveFormat.nAvgBytesPerSec; 300 dwQuality = DWORD(-1); 301 dwSampleSize = waveFormat.nBlockAlign; 302 } 303 }; 304 305 struct Buffer 306 { 307 uchar* const ptr; 308 const uint size; 309 310 Buffer(uint n) : ptr(n ? new uchar [n] : NULL), size(n) {} 311 ~Buffer() { delete [] ptr; } 312 }; 313 314 Movie movie( emulator, moviePath ); 315 316 if (const uint ids = movie.Play()) 317 return ids; 318 319 BitmapInfo bitmapInfo( renderState.width, renderState.height ); 320 CompressVars compressVars; 321 322 if (!compressVars.Choose( bitmapInfo )) 323 return 0; 324 325 Application::Instance::Waiter wait; 326 327 const File avi( aviPath.Ptr() ); 328 329 if (!avi) 330 return IDS_AVI_WRITE_ERR; 331 332 VideoInfo videoInfo( emulator, bitmapInfo, compressVars ); 333 const Stream video( avi, videoInfo ); 334 335 if (!video) 336 return IDS_AVI_WRITE_ERR; 337 338 CompressOptions compressOptions( compressVars ); 339 const Stream compressor( video, compressOptions ); 340 341 if (!compressor) 342 return IDS_AVI_WRITE_ERR; 343 344 if (!compressor.SetFormat( &bitmapInfo, bitmapInfo.biSize + bitmapInfo.biClrUsed * sizeof(RGBQUAD) )) 345 return IDS_AVI_WRITE_ERR; 346 347 Window::User::Inform( IDS_AVI_WRITE_INFO ); 348 Application::Instance::Locker locker; 349 350 WaveFormat waveFormat( emulator ); 351 SoundInfo soundInfo( waveFormat ); 352 const Stream sound( Nes::Sound(emulator).IsAudible() ? &avi : NULL, soundInfo ); 353 354 if (Nes::Sound(emulator).IsAudible()) 355 { 356 if (!sound) 357 return IDS_AVI_WRITE_ERR; 358 359 if (!sound.SetFormat( &waveFormat, sizeof(WAVEFORMATEX) )) 360 return IDS_AVI_WRITE_ERR; 361 } 362 363 Buffer pixels( bitmapInfo.biSizeImage ); 364 Buffer samples( waveFormat.nAvgBytesPerSec / videoInfo.dwRate ); 365 366 // picture will be stored upside down, adjust the pitch 367 Nes::Video::Output videoOutput( pixels.ptr + (VIDEO_BPP/8 * bitmapInfo.biWidth * (renderState.height-1)), -int(VIDEO_BPP/8 * bitmapInfo.biWidth) ); 368 Nes::Sound::Output soundOutput( samples.ptr, samples.size / waveFormat.nBlockAlign ); 369 370 { 371 Nes::Video::RenderState tmp; 372 Nes::Video(emulator).GetRenderState( tmp ); 373 374 tmp.bits.count = VIDEO_BPP; 375 tmp.bits.mask.r = VIDEO_R_MASK; 376 tmp.bits.mask.g = VIDEO_G_MASK; 377 tmp.bits.mask.b = VIDEO_B_MASK; 378 379 Nes::Video(emulator).SetRenderState( tmp ); 380 } 381 382 for (uint frame=0, sample=0, size=0; Nes::Movie(emulator).IsPlaying() && size < MAX_FILE_SIZE; ++frame, sample += soundOutput.length[0]) 383 { 384 ::Sleep( 0 ); 385 386 if (locker.CheckInput( VK_ESCAPE )) 387 return IDS_AVI_WRITE_ABORT; 388 389 if (NES_FAILED(emulator.Nes::Emulator::Execute( &videoOutput, sound ? &soundOutput : NULL, NULL ))) 390 return IDS_AVI_WRITE_ERR; 391 392 if (bitmapInfo.biHeight == renderState.height * 2) 393 { 394 const uint pitch = -videoOutput.pitch; 395 396 for (uchar *src=pixels.ptr + (pitch * (renderState.height-1)), *dst = pixels.ptr + (bitmapInfo.biSizeImage - pitch); ; src -= pitch) 397 { 398 std::memcpy( dst, src, pitch ); 399 dst -= pitch; 400 401 if (dst == pixels.ptr) 402 break; 403 404 std::memcpy( dst, src, pitch ); 405 dst -= pitch; 406 } 407 } 408 409 long written[2] = {0,0}; 410 411 if 412 ( 413 (::AVIStreamWrite( compressor, frame, 1, pixels.ptr, pixels.size, AVIIF_KEYFRAME, NULL, written+0 ) != AVIERR_OK) || 414 (sound && ::AVIStreamWrite( sound, sample, soundOutput.length[0], samples.ptr, samples.size, 0, NULL, written+1 ) != AVIERR_OK) 415 ) 416 return IDS_AVI_WRITE_ERR; 417 418 size += (written[0] > 0 ? written[0] : 0) + (written[1] > 0 ? written[1] : 0); 419 } 420 421 avi.SetSuccess(); 422 423 return IDS_AVI_WRITE_FINISHED; 424 } 425 catch (Io::File::Exception id) 426 { 427 return id; 428 } 429 catch (...) 430 { 431 return IDS_ERR_GENERIC; 432 } 433 } 434 435 #ifdef NST_MSVC_OPTIMIZE 436 #pragma optimize("", on) 437 #endif 438 ~AviConverter()439 AviConverter::~AviConverter() 440 { 441 if (saveState.Size()) 442 emulator.LoadState( saveState, Emulator::QUIETLY ); 443 444 if (!on) 445 emulator.Power( false ); 446 447 Nes::Video::Output::lockCallback.Set( nesVideoLockFunc, nesVideoLockData ); 448 Nes::Video::Output::unlockCallback.Set( nesVideoUnlockFunc, nesVideoUnlockData ); 449 Nes::Sound::Output::lockCallback.Set( nesSoundLockFunc, nesSoundLockData ); 450 Nes::Sound::Output::unlockCallback.Set( nesSoundUnlockFunc, nesSoundUnlockData ); 451 Nes::Movie::eventCallback.Set( nesMovieEventFunc, nesMovieEventData ); 452 453 Nes::Video(emulator).SetRenderState( renderState ); 454 455 Application::Instance::GetMainWindow().Redraw(); 456 } 457 } 458 } 459