HatariWii/src/avi_record.c

1021 lines
33 KiB
C

/*
Hatari - avi_record.c
This file is distributed under the GNU General Public License, version 2
or at your option any later version. Read the file gpl.txt for details.
AVI File recording
This allows Hatari to record a video file, with both video and audio
streams, at full frame rate.
Video frames are saved using the current video frequency of the emulated
machine (50 Hz, 60 Hz, 70 Hz, ...). Frames can be stored using different
codecs. So far, supported codecs are :
- BMP : uncompressed RGB images. Very fast to save, very few cpu needed
but requires a lot of disk bandwidth and a lot of space.
- PNG : compressed RBG images. Depending on the compression level, this
can require more cpu and could slow down Hatari. As compressed images
are much smaller than BMP images, this will require less space on disk
and much less disk bandwidth. Compression levels 3 or 4 give good
tradeoff between cpu usage and file size and should not slow down Hatari
with recent computers.
PNG compression will often give a x20 ratio when compared to BMP and should
be used if you have a powerful enough cpu.
Sound is saved as 16 bits pcm stereo, using the current Hatari sound output
frequency. For best accuracy, sound frequency should be a multiple of the
video frequency ; this means 44.1 kHz is the best choice for 50/60 Hz video.
The AVI file is divided into multiple chunks. Hatari will save one video stream
and one audio stream, so the overall structure of the file is the following :
RIFF avi
LIST
hdrl
avih
LIST
strl
strh (vids)
strf
LIST
strl
strh (auds)
strf
LIST
INFO
LIST
movi
00db
01wb
...
idx1
*/
const char AVIRecord_fileid[] = "Hatari avi_record.c : " __DATE__ " " __TIME__;
#include <SDL.h>
#include <SDL_endian.h>
#include "main.h"
#include "version.h"
#include "audio.h"
#include "configuration.h"
#include "log.h"
#include "screen.h"
#include "screenSnapShot.h"
#include "sound.h"
#include "statusbar.h"
#include "avi_record.h"
/* after above that brings in config.h */
#if HAVE_LIBPNG
#include <png.h>
#endif
#include "pixel_convert.h" /* inline functions */
typedef struct
{
Uint8 ChunkName[4]; /* '00db', '01wb', 'idx1' */
Uint8 ChunkSize[4];
} AVI_CHUNK;
typedef struct
{
Uint8 identifier[4]; /* '00db', '01wb', 'idx1' */
Uint8 flags[4];
Uint8 offset[4];
Uint8 length[4];
} AVI_CHUNK_INDEX;
typedef struct
{
Uint8 ChunkName[4]; /* 'strh' */
Uint8 ChunkSize[4];
Uint8 stream_type[4]; /* 'vids' or 'auds' */
Uint8 stream_handler[4];
Uint8 flags[4];
Uint8 priority[2];
Uint8 language[2];
Uint8 initial_frames[4];
Uint8 time_scale[4];
Uint8 data_rate[4];
Uint8 start_time[4];
Uint8 data_length[4];
Uint8 buffer_size[4];
Uint8 quality[4];
Uint8 sample_size[4];
Uint8 dest_left[2];
Uint8 dest_top[2];
Uint8 dest_right[2];
Uint8 dest_bottom[2];
} AVI_STREAM_HEADER;
typedef struct
{
Uint8 ChunkName[4]; /* 'strf' */
Uint8 ChunkSize[4];
Uint8 size[4];
Uint8 width[4];
Uint8 height[4];
Uint8 planes[2];
Uint8 bit_count[2];
Uint8 compression[4];
Uint8 size_image[4];
Uint8 xpels_meter[4];
Uint8 ypels_meter[4];
Uint8 clr_used[4];
Uint8 clr_important[4];
} AVI_STREAM_FORMAT_VIDS;
typedef struct
{
Uint8 ChunkName[4]; /* 'LIST' */
Uint8 ChunkSize[4];
Uint8 Name[4]; /* 'strl' */
AVI_STREAM_HEADER Header; /* 'strh' */
AVI_STREAM_FORMAT_VIDS Format; /* 'strf' */
} AVI_STREAM_LIST_VIDS;
typedef struct
{
Uint8 ChunkName[4]; /* 'strf' */
Uint8 ChunkSize[4];
Uint8 codec[2];
Uint8 channels[2];
Uint8 sample_rate[4];
Uint8 bit_rate[4];
Uint8 block_align[2];
Uint8 bits_per_sample[2];
Uint8 ext_size[2];
} AVI_STREAM_FORMAT_AUDS;
typedef struct
{
Uint8 ChunkName[4]; /* 'LIST' */
Uint8 ChunkSize[4];
Uint8 Name[4]; /* 'strl' */
AVI_STREAM_HEADER Header; /* 'strh' */
AVI_STREAM_FORMAT_AUDS Format; /* 'strf' */
} AVI_STREAM_LIST_AUDS;
typedef struct {
Uint8 ChunkName[4]; /* 'avih' */
Uint8 ChunkSize[4];
Uint8 microsec_per_frame[4];
Uint8 max_bytes_per_second[4];
Uint8 padding_granularity[4];
Uint8 flags[4];
Uint8 total_frames[4];
Uint8 init_frame[4];
Uint8 nb_streams[4];
Uint8 buffer_size[4];
Uint8 width[4];
Uint8 height[4];
Uint8 scale[4];
Uint8 rate[4];
Uint8 start[4];
Uint8 length[4];
} AVI_STREAM_AVIH;
typedef struct
{
Uint8 ChunkName[4]; /* 'LIST' */
Uint8 ChunkSize[4];
Uint8 Name[4]; /* 'hdrl' */
AVI_STREAM_AVIH Header;
} AVI_STREAM_LIST_AVIH;
typedef struct {
Uint8 ChunkName[4]; /* 'ISFT' (software used) */
Uint8 ChunkSize[4];
// Uint8 Text[2]; /* Text's size should be multiple of 2 (including '\0') */
} AVI_STREAM_INFO;
typedef struct
{
Uint8 ChunkName[4]; /* 'LIST' */
Uint8 ChunkSize[4];
Uint8 Name[4]; /* 'INFO' */
AVI_STREAM_INFO Info;
} AVI_STREAM_LIST_INFO;
typedef struct
{
Uint8 ChunkName[4]; /* 'LIST' */
Uint8 ChunkSize[4];
Uint8 Name[4]; /* 'movi' */
} AVI_STREAM_LIST_MOVI;
typedef struct
{
Uint8 signature[4]; /* 'RIFF' */
Uint8 filesize[4];
Uint8 type[4]; /* 'AVI ' */
} RIFF_HEADER;
typedef struct {
RIFF_HEADER RiffHeader;
AVI_STREAM_LIST_AVIH AviHeader;
AVI_STREAM_LIST_VIDS VideoStream;
AVI_STREAM_LIST_AUDS AudioStream;
} AVI_FILE_HEADER;
#define AUDIO_STREAM_WAVE_FORMAT_PCM 0x0001
#define VIDEO_STREAM_RGB 0x00000000 /* fourcc for BMP video frames */
#define VIDEO_STREAM_PNG "MPNG" /* fourcc for PNG video frames */
#define AVIF_HASINDEX 0x00000010 /* index at the end of the file */
#define AVIF_ISINTERLEAVED 0x00000100 /* data are interleaved */
#define AVIF_TRUSTCKTYPE 0x00000800 /* trust chunk type */
#define AVIIF_KEYFRAME 0x00000010 /* frame is a keyframe */
typedef struct {
/* Input params to start recording */
int VideoCodec;
int VideoCodecCompressionLevel; /* 0-9 for png compression */
SDL_Surface *Surface;
int CropLeft;
int CropRight;
int CropTop;
int CropBottom;
int Fps; /* Fps << 24 */
int Fps_scale; /* 1 << 24 */
int AudioCodec;
int AudioFreq;
/* Internal data used by the avi recorder */
int Width;
int Height;
int BitCount;
FILE *FileOut; /* file to write to */
int TotalVideoFrames; /* number of recorded video frames */
int TotalAudioSamples; /* number of recorded audio samples */
long MoviChunkPosStart; /* as returned by ftell() */
long MoviChunkPosEnd; /* as returned by ftell() */
} RECORD_AVI_PARAMS;
bool bRecordingAvi = false;
static RECORD_AVI_PARAMS AviParams;
static AVI_FILE_HEADER AviFileHeader;
static void Avi_StoreU16 ( Uint8 *p , Uint16 val );
static void Avi_StoreU32 ( Uint8 *p , Uint32 val );
static void Avi_Store4cc ( Uint8 *p , const char *text );
static Uint32 Avi_ReadU32 ( Uint8 *p );
static int Avi_GetBmpSize ( int Width , int Height , int BitCount );
static bool Avi_RecordVideoStream_BMP ( RECORD_AVI_PARAMS *pAviParams );
#if HAVE_LIBPNG
static bool Avi_RecordVideoStream_PNG ( RECORD_AVI_PARAMS *pAviParams );
#endif
static bool Avi_RecordAudioStream_PCM ( RECORD_AVI_PARAMS *pAviParams , Sint16 pSamples[][2], int SampleIndex, int SampleLength );
static void Avi_BuildFileHeader ( RECORD_AVI_PARAMS *pAviParams , AVI_FILE_HEADER *pAviFileHeader );
static bool Avi_BuildIndex ( RECORD_AVI_PARAMS *pAviParams );
static bool Avi_StartRecording_WithParams ( RECORD_AVI_PARAMS *pAviParams , char *AviFileName );
static bool Avi_StopRecording_WithParams ( RECORD_AVI_PARAMS *pAviParams );
static void Avi_StoreU16 ( Uint8 *p , Uint16 val )
{
*p++ = val & 0xff;
val >>= 8;
*p = val & 0xff;
}
static void Avi_StoreU32 ( Uint8 *p , Uint32 val )
{
*p++ = val & 0xff;
val >>= 8;
*p++ = val & 0xff;
val >>= 8;
*p++ = val & 0xff;
val >>= 8;
*p = val & 0xff;
}
static void Avi_Store4cc ( Uint8 *p , const char *text )
{
memcpy ( p , text , 4 );
}
static Uint32 Avi_ReadU32 ( Uint8 *p )
{
return (p[3]<<24) + (p[2]<<16) + (p[1]<<8) +p[0];
}
static int Avi_GetBmpSize ( int Width , int Height , int BitCount )
{
return ( Width * Height * BitCount / 8 ); /* bytes in one video frame */
}
static bool Avi_RecordVideoStream_BMP ( RECORD_AVI_PARAMS *pAviParams )
{
AVI_CHUNK Chunk;
int SizeImage;
Uint8 LineBuf[ 3 * pAviParams->Width ]; /* temp buffer to convert to 24-bit BGR format */
Uint8 *pBitmapIn , *pBitmapOut;
int y;
int NeedLock;
SizeImage = Avi_GetBmpSize ( pAviParams->Width , pAviParams->Height , pAviParams->BitCount );
/* Write the video frame header */
Avi_Store4cc ( Chunk.ChunkName , "00db" ); /* stream 0, uncompressed DIB bytes */
Avi_StoreU32 ( Chunk.ChunkSize , SizeImage ); /* max size of RGB image */
if ( fwrite ( &Chunk , sizeof ( Chunk ) , 1 , pAviParams->FileOut ) != 1 )
{
perror ( "Avi_RecordVideoStream_BMP" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write bmp frame header" );
return false;
}
/* Write the video frame data */
NeedLock = SDL_MUSTLOCK( pAviParams->Surface );
/* Points to the top left pixel after cropping borders */
/* For BMP format, frame is stored from bottom to top (origin is in bottom left corner) */
/* and bytes are in BGR order (not RGB) */
pBitmapIn = (Uint8 *)pAviParams->Surface->pixels
+ pAviParams->Surface->pitch * ( pAviParams->CropTop + pAviParams->Height )
+ pAviParams->CropLeft * pAviParams->Surface->format->BytesPerPixel;
for ( y=0 ; y<pAviParams->Height ; y++ )
{
if ( NeedLock )
SDL_LockSurface ( pAviParams->Surface );
pBitmapOut = LineBuf;
switch ( pAviParams->Surface->format->BytesPerPixel ) {
case 1 : PixelConvert_8to24Bits_BGR(LineBuf, pBitmapIn, pAviParams->Width, pAviParams->Surface->format->palette->colors);
break;
case 2 : PixelConvert_16to24Bits_BGR(LineBuf, (Uint16 *)pBitmapIn, pAviParams->Width, pAviParams->Surface->format);
break;
case 3 : PixelConvert_24to24Bits_BGR(LineBuf, pBitmapIn, pAviParams->Width);
break;
case 4 : PixelConvert_32to24Bits_BGR(LineBuf, (Uint32 *)pBitmapIn, pAviParams->Width, pAviParams->Surface->format);
break;
}
if ( NeedLock )
SDL_UnlockSurface ( pAviParams->Surface );
if ( (int)fwrite ( pBitmapOut , 1 , pAviParams->Width*3 , pAviParams->FileOut ) != pAviParams->Width*3 )
{
perror ( "Avi_RecordVideoStream_BMP" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write bmp video frame" );
return false;
}
pBitmapIn -= pAviParams->Surface->pitch; /* go from bottom to top */
}
return true;
}
#if HAVE_LIBPNG
static bool Avi_RecordVideoStream_PNG ( RECORD_AVI_PARAMS *pAviParams )
{
AVI_CHUNK Chunk;
int SizeImage;
long ChunkPos;
Uint8 TempSize[4];
/* Write the video frame header */
ChunkPos = ftell ( pAviParams->FileOut );
Avi_Store4cc ( Chunk.ChunkName , "00dc" ); /* stream 0, compressed DIB bytes */
Avi_StoreU32 ( Chunk.ChunkSize , 0 ); /* size of PNG image (-> completed later) */
if ( fwrite ( &Chunk , sizeof ( Chunk ) , 1 , pAviParams->FileOut ) != 1 )
goto png_error;
/* Write the video frame data */
SizeImage = ScreenSnapShot_SavePNG_ToFile ( pAviParams->Surface , pAviParams->FileOut ,
pAviParams->VideoCodecCompressionLevel , PNG_FILTER_NONE ,
pAviParams->CropLeft , pAviParams->CropRight , pAviParams->CropTop , pAviParams->CropBottom );
if ( SizeImage <= 0 )
goto png_error;
if ( SizeImage & 1 )
{
SizeImage++; /* add an extra '\0' byte to get an even size */
fputc ( '\0' , pAviParams->FileOut ); /* next chunk must be aligned on 16 bits boundary */
}
/* Update the size of the video chunk */
Avi_StoreU32 ( TempSize , SizeImage );
if ( fseek ( pAviParams->FileOut , ChunkPos+4 , SEEK_SET ) != 0 )
goto png_error;
if ( fwrite ( TempSize , sizeof ( TempSize ) , 1 , pAviParams->FileOut ) != 1 )
goto png_error;
/* Go to the end of the video frame data */
if ( fseek ( pAviParams->FileOut , 0 , SEEK_END ) != 0 )
goto png_error;
return true;
png_error:
perror ( "Avi_RecordVideoStream_PNG" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write png frame" );
return false;
}
#endif /* HAVE_LIBPNG */
bool Avi_RecordVideoStream ( void )
{
if ( AviParams.VideoCodec == AVI_RECORD_VIDEO_CODEC_BMP )
{
if ( Avi_RecordVideoStream_BMP ( &AviParams ) == false )
{
return false;
}
}
#if HAVE_LIBPNG
else if ( AviParams.VideoCodec == AVI_RECORD_VIDEO_CODEC_PNG )
{
if ( Avi_RecordVideoStream_PNG ( &AviParams ) == false )
{
return false;
}
}
#endif
else
{
return false;
}
if (++AviParams.TotalVideoFrames % ( AviParams.Fps / AviParams.Fps_scale ) == 0)
{
int secs = AviParams.TotalVideoFrames / ( AviParams.Fps / AviParams.Fps_scale );
char str[6] = "00:00";
str[0] = '0' + (secs / 60) / 10;
str[1] = '0' + (secs / 60) % 10;
str[3] = '0' + (secs % 60) / 10;
str[4] = '0' + (secs % 60) % 10;
Main_SetTitle(str);
}
return true;
}
static bool Avi_RecordAudioStream_PCM ( RECORD_AVI_PARAMS *pAviParams , Sint16 pSamples[][2] , int SampleIndex , int SampleLength )
{
AVI_CHUNK Chunk;
Sint16 sample[2];
int i;
/* Write the audio frame header */
Avi_Store4cc ( Chunk.ChunkName , "01wb" ); /* stream 1, wave bytes */
Avi_StoreU32 ( Chunk.ChunkSize , SampleLength * 4 ); /* 16 bits, stereo -> 4 bytes */
if ( fwrite ( &Chunk , sizeof ( Chunk ) , 1 , pAviParams->FileOut ) != 1 )
{
perror ( "Avi_RecordAudioStream_PCM" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write pcm frame header" );
return false;
}
/* Write the audio frame data */
for ( i = 0 ; i < SampleLength; i++ )
{
/* Convert sample to little endian */
sample[0] = SDL_SwapLE16 ( pSamples[ (SampleIndex+i) % MIXBUFFER_SIZE ][0]);
sample[1] = SDL_SwapLE16 ( pSamples[ (SampleIndex+i) % MIXBUFFER_SIZE ][1]);
/* And store */
if ( fwrite ( &sample , sizeof ( sample ) , 1 , pAviParams->FileOut ) != 1 )
{
perror ( "Avi_RecordAudioStream_PCM" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write pcm frame" );
return false;
}
}
return true;
}
bool Avi_RecordAudioStream ( Sint16 pSamples[][2] , int SampleIndex , int SampleLength )
{
if ( AviParams.AudioCodec == AVI_RECORD_AUDIO_CODEC_PCM )
{
if ( Avi_RecordAudioStream_PCM ( &AviParams , pSamples , SampleIndex , SampleLength ) == false )
{
return false;
}
}
else
{
return false;
}
AviParams.TotalAudioSamples += SampleLength;
return true;
}
static void Avi_BuildFileHeader ( RECORD_AVI_PARAMS *pAviParams , AVI_FILE_HEADER *pAviFileHeader )
{
int Width , Height , BitCount , Fps , Fps_scale , SizeImage;
int AudioFreq;
memset ( pAviFileHeader , 0 , sizeof ( *pAviFileHeader ) );
Width = pAviParams->Width;
Height =pAviParams->Height;
BitCount = pAviParams->BitCount;
Fps = pAviParams->Fps;
Fps_scale = pAviParams->Fps_scale;
AudioFreq = pAviParams->AudioFreq;
SizeImage = 0;
if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_BMP )
SizeImage = Avi_GetBmpSize ( Width , Height , BitCount ); /* size of a BMP image */
else if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_PNG )
SizeImage = Avi_GetBmpSize ( Width , Height , BitCount ); /* max size of a PNG image */
/* RIFF / AVI headers */
Avi_Store4cc ( pAviFileHeader->RiffHeader.signature , "RIFF" );
Avi_StoreU32 ( pAviFileHeader->RiffHeader.filesize , 0 ); /* total file size (-> completed later) */
Avi_Store4cc ( pAviFileHeader->RiffHeader.type , "AVI " );
Avi_Store4cc ( pAviFileHeader->AviHeader.ChunkName , "LIST" );
Avi_StoreU32 ( pAviFileHeader->AviHeader.ChunkSize , sizeof ( AVI_STREAM_LIST_AVIH )
+ sizeof ( AVI_STREAM_LIST_VIDS ) + sizeof ( AVI_STREAM_LIST_AUDS ) - 8 );
Avi_Store4cc ( pAviFileHeader->AviHeader.Name , "hdrl" );
Avi_Store4cc ( pAviFileHeader->AviHeader.Header.ChunkName , "avih" );
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.ChunkSize , sizeof ( AVI_STREAM_AVIH ) - 8 );
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.microsec_per_frame , (Uint32)( ( 1000000 * (Sint64)Fps_scale ) / Fps ) );
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.max_bytes_per_second , (Uint32)( ( (Sint64)SizeImage * Fps ) / Fps_scale + AudioFreq * 4 ) );
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.padding_granularity , 0 );
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.flags , AVIF_HASINDEX | AVIF_ISINTERLEAVED | AVIF_TRUSTCKTYPE );
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.total_frames , 0 ); /* number of video frames (-> completed later) */
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.init_frame , 0 );
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.nb_streams , 2 ); /* 1 video and 1 audio */
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.buffer_size , SizeImage );
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.width , Width );
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.height , Height );
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.scale , 0 ); /* reserved */
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.rate , 0 ); /* reserved */
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.start , 0 ); /* reserved */
Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.length , 0 ); /* reserved */
/* Video Stream */
Avi_Store4cc ( pAviFileHeader->VideoStream.ChunkName , "LIST" );
Avi_StoreU32 ( pAviFileHeader->VideoStream.ChunkSize , sizeof ( AVI_STREAM_LIST_VIDS ) - 8 );
Avi_Store4cc ( pAviFileHeader->VideoStream.Name , "strl" );
Avi_Store4cc ( pAviFileHeader->VideoStream.Header.ChunkName , "strh" );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.ChunkSize , sizeof ( AVI_STREAM_HEADER ) - 8 );
Avi_Store4cc ( pAviFileHeader->VideoStream.Header.stream_type , "vids" );
if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_BMP )
Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.stream_handler , VIDEO_STREAM_RGB );
else if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_PNG )
Avi_Store4cc ( pAviFileHeader->VideoStream.Header.stream_handler , VIDEO_STREAM_PNG );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.flags , 0 );
Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.priority , 0 );
Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.language , 0 );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.initial_frames , 0 );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.time_scale , Fps_scale );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.data_rate , Fps );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.start_time , 0 );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.data_length , 0 ); /* number of video frames (-> completed later) */
Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.buffer_size , SizeImage ); /* size of an uncompressed frame */
Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.quality , -1 ); /* use default quality */
Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.sample_size , 0 ); /* 0 for video */
Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.dest_left , 0 );
Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.dest_top , 0 );
Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.dest_right , Width );
Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.dest_bottom , Height );
Avi_Store4cc ( pAviFileHeader->VideoStream.Format.ChunkName , "strf" );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.ChunkSize , sizeof ( AVI_STREAM_FORMAT_VIDS ) - 8 );
if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_BMP )
{
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.size , sizeof ( AVI_STREAM_FORMAT_VIDS ) - 8 );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.width , Width );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.height , Height );
Avi_StoreU16 ( pAviFileHeader->VideoStream.Format.planes , 1 ); /* always 1 */
Avi_StoreU16 ( pAviFileHeader->VideoStream.Format.bit_count , BitCount );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.compression , VIDEO_STREAM_RGB );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.size_image , SizeImage );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.xpels_meter , 0 );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.ypels_meter , 0 );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.clr_used , 0 ); /* no color map */
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.clr_important , 0 ); /* no color map */
}
else if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_PNG )
{
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.size , sizeof ( AVI_STREAM_FORMAT_VIDS ) - 8 );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.width , Width );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.height , Height );
Avi_StoreU16 ( pAviFileHeader->VideoStream.Format.planes , 1 ); /* always 1 */
Avi_StoreU16 ( pAviFileHeader->VideoStream.Format.bit_count , BitCount );
Avi_Store4cc ( pAviFileHeader->VideoStream.Format.compression , VIDEO_STREAM_PNG );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.size_image , SizeImage ); /* max size if uncompressed */
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.xpels_meter , 0 );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.ypels_meter , 0 );
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.clr_used , 0 ); /* no color map */
Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.clr_important , 0 ); /* no color map */
}
/* Audio Stream */
Avi_Store4cc ( pAviFileHeader->AudioStream.ChunkName , "LIST" );
Avi_StoreU32 ( pAviFileHeader->AudioStream.ChunkSize , sizeof ( AVI_STREAM_LIST_AUDS ) - 8 );
Avi_Store4cc ( pAviFileHeader->AudioStream.Name , "strl" );
Avi_Store4cc ( pAviFileHeader->AudioStream.Header.ChunkName , "strh" );
Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.ChunkSize , sizeof ( AVI_STREAM_HEADER ) - 8 );
Avi_Store4cc ( pAviFileHeader->AudioStream.Header.stream_type , "auds" );
Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.stream_handler , 0 ); /* not used (or could be 1 for pcm ?) */
Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.flags , 0 );
Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.priority , 0 );
Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.language , 0 );
Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.initial_frames , 0 ); /* should be 1 in interleaved ? */
Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.time_scale , 1 );
Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.data_rate , AudioFreq );
Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.start_time , 0 );
Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.data_length , 0 ); /* number of audio samples (-> completed later) */
Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.buffer_size , AudioFreq * 4 / 50 ); /* min VBL freq is 50 Hz */
Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.quality , -1 ); /* use default quality */
Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.sample_size , 4 ); /* 2 bytes, stereo */
Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.dest_left , 0 );
Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.dest_top , 0 );
Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.dest_right , 0 );
Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.dest_bottom , 0 );
Avi_Store4cc ( pAviFileHeader->AudioStream.Format.ChunkName , "strf" );
Avi_StoreU32 ( pAviFileHeader->AudioStream.Format.ChunkSize , sizeof ( AVI_STREAM_FORMAT_AUDS ) - 8 );
if ( pAviParams->AudioCodec == AVI_RECORD_AUDIO_CODEC_PCM ) /* 16 bits stereo pcm */
{
Avi_StoreU16 ( pAviFileHeader->AudioStream.Format.codec , AUDIO_STREAM_WAVE_FORMAT_PCM ); /* 0x0001 */
Avi_StoreU16 ( pAviFileHeader->AudioStream.Format.channels , 2 );
Avi_StoreU32 ( pAviFileHeader->AudioStream.Format.sample_rate , AudioFreq );
Avi_StoreU32 ( pAviFileHeader->AudioStream.Format.bit_rate , AudioFreq * 2 * 2 ); /* 2 channels * 2 bytes */
Avi_StoreU16 ( pAviFileHeader->AudioStream.Format.block_align , 4 );
Avi_StoreU16 ( pAviFileHeader->AudioStream.Format.bits_per_sample , 16 );
Avi_StoreU16 ( pAviFileHeader->AudioStream.Format.ext_size , 0 );
}
}
static bool Avi_BuildIndex ( RECORD_AVI_PARAMS *pAviParams )
{
AVI_CHUNK Chunk;
long IndexChunkPosStart;
long Pos , PosWrite;
Uint8 TempSize[4];
AVI_CHUNK_INDEX ChunkIndex;
Uint32 Size;
if (fseek(pAviParams->FileOut, 0, SEEK_END) != 0) /* go to the end of the file */
goto index_error;
/* Write the 'idx1' chunk header */
IndexChunkPosStart = ftell ( pAviParams->FileOut );
Avi_Store4cc ( Chunk.ChunkName , "idx1" ); /* stream 0, uncompressed DIB bytes */
Avi_StoreU32 ( Chunk.ChunkSize , 0 ); /* index size (-> completed later) */
if ( fwrite ( &Chunk , sizeof ( Chunk ) , 1 , pAviParams->FileOut ) != 1 )
goto index_error;
PosWrite = ftell ( pAviParams->FileOut ); /* position to start writing indexes */
/* Go to the first data chunk in the 'movi' chunk */
if (fseek(pAviParams->FileOut, pAviParams->MoviChunkPosStart + sizeof(AVI_STREAM_LIST_MOVI), SEEK_SET) != 0)
goto index_error;
Pos = ftell ( pAviParams->FileOut );
/* Build the index : we seek/read one data chunk and seek/write the */
/* corresponding infos at the end of the index, until we reach the */
/* end of the 'movi' chunk. */
while ( Pos < pAviParams->MoviChunkPosEnd )
{
/* Read the header for this data chunk: ChunkName and ChunkSize */
if (fread(&Chunk, sizeof(Chunk), 1, pAviParams->FileOut) != 1)
goto index_error;
Size = Avi_ReadU32 ( Chunk.ChunkSize );
/* Write the index infos for this chunk */
if (fseek(pAviParams->FileOut, PosWrite, SEEK_SET) != 0)
goto index_error;
Avi_Store4cc ( ChunkIndex.identifier , (char *)Chunk.ChunkName ); /* 00dc, 00db, 01wb, ... */
Avi_StoreU32 ( ChunkIndex.flags , AVIIF_KEYFRAME ); /* AVIIF_KEYFRAME */
Avi_StoreU32 ( ChunkIndex.offset , Pos - pAviParams->MoviChunkPosStart - 8 ); /* pos relative to 'movi' */
Avi_StoreU32 ( ChunkIndex.length , Size );
if (fwrite ( &ChunkIndex , sizeof ( ChunkIndex ) , 1 , pAviParams->FileOut ) != 1)
goto index_error;
PosWrite = ftell ( pAviParams->FileOut ); /* position for the next index */
/* Go to the next data chunk in the 'movi' chunk */
Pos = Pos + sizeof ( Chunk ) + Size; /* position of the next data chunk */
if (fseek(pAviParams->FileOut, Pos, SEEK_SET) != 0)
goto index_error;
}
/* Update the size of the 'idx1' chunk */
Avi_StoreU32 ( TempSize , PosWrite - IndexChunkPosStart - 8 );
if ( fseek ( pAviParams->FileOut , IndexChunkPosStart+4 , SEEK_SET ) != 0 )
goto index_error;
if ( fwrite ( TempSize , sizeof ( TempSize ) , 1 , pAviParams->FileOut ) != 1 )
goto index_error;
return true;
index_error:
perror ( "Avi_BuildIndex" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to create index header" );
return false;
}
static bool Avi_StartRecording_WithParams ( RECORD_AVI_PARAMS *pAviParams , char *AviFileName )
{
AVI_STREAM_LIST_INFO ListInfo;
char InfoString[ 100 ];
int Len , Len_rounded;
AVI_STREAM_LIST_MOVI ListMovi;
if ( bRecordingAvi == true ) /* already recording ? */
return false;
/* Compute some video parameters */
pAviParams->Width = pAviParams->Surface->w - pAviParams->CropLeft - pAviParams->CropRight;
pAviParams->Height = pAviParams->Surface->h - pAviParams->CropTop - pAviParams->CropBottom;
pAviParams->BitCount = 24;
#if !HAVE_LIBPNG
if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_PNG )
{
perror ( "AviStartRecording" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : Hatari was not built with libpng support" );
return false;
}
#endif
/* Open the file */
pAviParams->FileOut = fopen ( AviFileName , "wb+" );
if ( !pAviParams->FileOut )
{
perror ( "AviStartRecording" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to open file" );
return false;
}
/* Build the AVI header */
Avi_BuildFileHeader ( pAviParams , &AviFileHeader );
/* Write the AVI header */
if ( fwrite ( &AviFileHeader , sizeof ( AviFileHeader ) , 1 , pAviParams->FileOut ) != 1 )
{
perror ( "AviStartRecording" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write avi header" );
return false;
}
/* Write the INFO header */
memset ( InfoString , 0 , sizeof ( InfoString ) );
Len = snprintf ( InfoString , sizeof ( InfoString ) , "%s - the Atari ST, STE, TT and Falcon emulator" , PROG_NAME ) + 1;
Len_rounded = Len + ( Len % 2 == 0 ? 0 : 1 ); /* round Len to the next multiple of 2 */
Avi_Store4cc ( ListInfo.ChunkName , "LIST" );
Avi_StoreU32 ( ListInfo.ChunkSize , sizeof ( AVI_STREAM_LIST_INFO ) - 8 + Len_rounded );
Avi_Store4cc ( ListInfo.Name , "INFO" );
Avi_Store4cc ( ListInfo.Info.ChunkName , "ISFT" );
Avi_StoreU32 ( ListInfo.Info.ChunkSize , Len );
if ( fwrite ( &ListInfo , sizeof ( ListInfo ) , 1 , pAviParams->FileOut ) != 1 )
{
perror ( "AviStartRecording" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write info header" );
return false;
}
/* Write the info string + '\0' and write an optional extra '\0' byte to get a total multiple of 2 */
if ( fwrite ( InfoString , Len_rounded , 1 , pAviParams->FileOut ) != 1 )
{
perror ( "AviStartRecording" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write info header" );
return false;
}
/* Write the MOVI header */
Avi_Store4cc ( ListMovi.ChunkName , "LIST" );
Avi_StoreU32 ( ListMovi.ChunkSize , 0 ); /* completed when recording stops */
Avi_Store4cc ( ListMovi.Name , "movi" );
pAviParams->MoviChunkPosStart = ftell ( pAviParams->FileOut );
if ( fwrite ( &ListMovi , sizeof ( ListMovi ) , 1 , pAviParams->FileOut ) != 1 )
{
perror ( "AviStartRecording" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write movi header" );
return false;
}
/* We're ok to record */
Log_AlertDlg ( LOG_INFO, "AVI recording has been started");
bRecordingAvi = true;
return true;
}
static bool Avi_StopRecording_WithParams ( RECORD_AVI_PARAMS *pAviParams )
{
long FileSize;
Uint8 TempSize[4];
if ( bRecordingAvi == false ) /* no recording ? */
return true;
/* Update the size of the 'movi' chunk */
if (fseek(pAviParams->FileOut, 0, SEEK_END) != 0) /* go to the end of the 'movi' chunk */
goto stoprec_error;
pAviParams->MoviChunkPosEnd = ftell ( pAviParams->FileOut );
Avi_StoreU32 ( TempSize , pAviParams->MoviChunkPosEnd - pAviParams->MoviChunkPosStart - 8 );
if ( fseek ( pAviParams->FileOut , pAviParams->MoviChunkPosStart+4 , SEEK_SET ) != 0 )
goto stoprec_error;
if ( fwrite ( TempSize , sizeof ( TempSize ) , 1 , pAviParams->FileOut ) != 1 )
goto stoprec_error;
/* Build the index chunk */
if ( ! Avi_BuildIndex ( pAviParams ) )
{
perror ( "AviStopRecording" );
Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to build index" );
return false;
}
/* Update the avi header (file size, number of output frames, ...) */
if (fseek(pAviParams->FileOut, 0, SEEK_END) != 0) /* go to the end of the file */
goto stoprec_error;
FileSize = ftell ( pAviParams->FileOut );
Avi_StoreU32 ( AviFileHeader.RiffHeader.filesize , FileSize - 8 ); /* 32 bits, limited to 4GB */
Avi_StoreU32 ( AviFileHeader.AviHeader.Header.total_frames , pAviParams->TotalVideoFrames ); /* number of video frames */
Avi_StoreU32 ( AviFileHeader.VideoStream.Header.data_length , pAviParams->TotalVideoFrames ); /* number of video frames */
Avi_StoreU32 ( AviFileHeader.AudioStream.Header.data_length , pAviParams->TotalAudioSamples ); /* number of audio samples */
if ( fseek ( pAviParams->FileOut , 0 , SEEK_SET ) != 0 )
goto stoprec_error;
if ( fwrite ( &AviFileHeader , sizeof ( AviFileHeader ) , 1 , pAviParams->FileOut ) != 1 )
goto stoprec_error;
/* Close the file */
fclose ( pAviParams->FileOut );
Log_AlertDlg ( LOG_INFO, "AVI recording has been stopped");
bRecordingAvi = false;
return true;
stoprec_error:
fclose (pAviParams->FileOut);
perror("AviStopRecording");
Log_AlertDlg(LOG_ERROR, "AVI recording : failed to update header");
return false;
}
/*-----------------------------------------------------------------------*/
/**
* Are we recording an AVI ?
*/
bool Avi_AreWeRecording ( void )
{
return bRecordingAvi;
}
/* PNG compression level, 0-9 */
static int compression_level = 9;
/**
* Set recording level from given string
* return true for valid, false for invalid value
*/
bool Avi_SetCompressionLevel(const char *str)
{
char *end;
long level = strtol(str, &end, 10);
if (*end)
return false;
if (level < 0 || level > 9)
return false;
compression_level = level;
return true;
}
bool Avi_StartRecording ( char *FileName , bool CropGui , Uint32 Fps , Uint32 Fps_scale , int VideoCodec )
{
memset ( &AviParams , 0 , sizeof ( AviParams ) );
AviParams.VideoCodec = VideoCodec;
AviParams.VideoCodecCompressionLevel = compression_level;
AviParams.AudioCodec = AVI_RECORD_AUDIO_CODEC_PCM;
AviParams.AudioFreq = ConfigureParams.Sound.nPlaybackFreq;
AviParams.Surface = sdlscrn;
/* Some video players (quicktime, ...) don't support a value of Fps_scale */
/* above 100000. So we decrease the precision from << 24 to << 16 for Fps and Fps_scale */
AviParams.Fps = Fps >> 8; /* refresh rate << 16 */
AviParams.Fps_scale = Fps_scale >> 8; /* 1 << 16 */
if ( !CropGui ) /* Keep gui's status bar */
{
AviParams.CropLeft = 0;
AviParams.CropRight = 0;
AviParams.CropTop = 0;
AviParams.CropBottom = 0;
}
else /* Record only the content of the Atari's screen */
{
AviParams.CropLeft = 0;
AviParams.CropRight = 0;
AviParams.CropTop = 0;
AviParams.CropBottom = Statusbar_GetHeight();
}
if (Avi_StartRecording_WithParams ( &AviParams , FileName ))
{
Main_SetTitle("00:00");
return true;
}
return false;
}
bool Avi_StopRecording ( void )
{
if (Avi_StopRecording_WithParams ( &AviParams ))
{
Main_SetTitle(NULL);
return true;
}
return false;
}