mirror of
https://git.code.sf.net/p/fuse-emulator/fuse
synced 2026-01-28 14:20:54 +03:00
490 lines
13 KiB
C
490 lines
13 KiB
C
/* movie.c: Routines for creating 'movie' with border
|
|
Copyright (c) 2006-2015 Gergely Szasz
|
|
Copyright (c) 2015 Stuart Brady
|
|
|
|
This program 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.
|
|
|
|
This program 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 this program; if not, write to the Free Software Foundation, Inc.,
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
Author contact information:
|
|
|
|
E-mail: philip-fuse@shadowmagic.org.uk
|
|
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
#include <libspectrum.h>
|
|
#ifdef HAVE_ZLIB_H
|
|
#define ZLIB_CONST
|
|
#include <zlib.h>
|
|
#endif
|
|
|
|
#include "display.h"
|
|
#include "fuse.h"
|
|
#include "machine.h"
|
|
#include "movie.h"
|
|
#include "movie_tables.h"
|
|
#include "options.h"
|
|
#include "peripherals/scld.h"
|
|
#include "screenshot.h"
|
|
#include "settings.h"
|
|
#include "sound.h"
|
|
#include "ui/ui.h"
|
|
|
|
#undef MOVIE_DEBUG_PRINT
|
|
|
|
/*
|
|
FMF - Fuse Movie File
|
|
|
|
Fuse Movie File:
|
|
File Header:
|
|
off len data description
|
|
0 4 "FMF_" Magic header
|
|
4 2 "V1" Version
|
|
6 1 <e|E> Endianness (e - little / E- big)
|
|
7 1 <U|Z> Compression ( U - uncompressed / Z - zlib compressed )
|
|
8 1 # Frame rate ( 1:# )
|
|
9 1 <$|R|C|X> Screen type
|
|
10 1 <A|B|C|D|E> timing code
|
|
11 1 <P|U|A> Sound encoding
|
|
12 2 Freq Sound freq in Hz
|
|
14 1 <S|M> Sound stereo / mono
|
|
15 1 "\n" padding (<new line>)
|
|
|
|
e.g. FMF_V1eZ\001$AU\000\175M -> little endian compressed normal screen, 48k timing, u-Law mono 32000Hz sound
|
|
Data
|
|
Frame data header
|
|
off len data description
|
|
0 1 <$|S|N|X> Data chunk type
|
|
|
|
$ -> screen area (slice)
|
|
off len data description
|
|
0 1 x X coord (0-39)
|
|
1 2 y Y coord (0-239)
|
|
3 1 w width (1-40)
|
|
4 2 h height (1-240)
|
|
6 ? runlength encoded data bytes
|
|
bitmap1; attrib1 ZX$/TX$/HiCol ($/X/C)
|
|
bitmap1; bitmap2; attrib HiR (R)
|
|
runlength encoding:
|
|
abcdefghbb#gg# -> two identical byte plus len code a #+2 len
|
|
|
|
S -> sound chunk
|
|
off len data description
|
|
0 1 <P|U|A> sound encoding type P-> 16bit signed PCM, U -> u-Law, A -> A-Law
|
|
1 2 Freq sound freq in Hz
|
|
3 1 <S|M> channels S-> stereo, M-> mono
|
|
4 2 length-1 length in frames (0-65535) -> 1-65536 sound frame
|
|
|
|
N -> New Frame
|
|
off len data description
|
|
0 1 # frame rate 1:# (1:1 -> ~50/s, 1:2 -> ~25/a ...)
|
|
1 1 <$|R|C|X> screen type $ - standard screen,
|
|
R - HiRes,
|
|
C - HiColor,
|
|
X - standard screen on Timex (double size)
|
|
2 1 <A|B|C|D> frame timing code A - 16, 48, TC2048, TC2068, Scorpion, SE
|
|
B - 128, +2, +2A, +3, +3E
|
|
C - TS2068
|
|
D - Pentagon
|
|
E - 48 NTSC
|
|
In a frame there are no, one or several screen rectangle, changed from
|
|
the previouse frame.
|
|
|
|
X -> End of recording
|
|
off len data description
|
|
There is no any data... It mark the end of the last frame. So we can
|
|
concat several FMF file without any problem...
|
|
*/
|
|
|
|
int movie_recording = 0;
|
|
static int movie_paused = 0;
|
|
|
|
static int frame_no, slice_no;
|
|
|
|
static FILE *of = NULL; /* out file */
|
|
static int fmf_screen;
|
|
static libspectrum_byte head[8];
|
|
static int freq = 0;
|
|
static char stereo = 'M';
|
|
static char format = '?';
|
|
static int framesiz = 4;
|
|
|
|
static libspectrum_byte sbuff[ 4096 ];
|
|
#ifdef HAVE_ZLIB_H
|
|
#define ZBUF_SIZE 8192
|
|
static int fmf_compr = -1;
|
|
static z_stream zstream;
|
|
static unsigned char zbuf_o[ ZBUF_SIZE ];
|
|
#endif /* HAVE_ZLIB_H */
|
|
|
|
static unsigned char alaw_table[2048 + 1] = { ALAW_ENC_TAB };
|
|
|
|
void movie_start_frame( void );
|
|
void movie_init_sound( int f, int s );
|
|
|
|
static char
|
|
get_timing( void )
|
|
{
|
|
switch( machine_current->machine ) {
|
|
case LIBSPECTRUM_MACHINE_16:
|
|
case LIBSPECTRUM_MACHINE_48:
|
|
case LIBSPECTRUM_MACHINE_TC2048:
|
|
case LIBSPECTRUM_MACHINE_TC2068:
|
|
case LIBSPECTRUM_MACHINE_SCORP:
|
|
case LIBSPECTRUM_MACHINE_SE:
|
|
return 'A';
|
|
case LIBSPECTRUM_MACHINE_128:
|
|
case LIBSPECTRUM_MACHINE_PLUS2:
|
|
case LIBSPECTRUM_MACHINE_PLUS2A:
|
|
case LIBSPECTRUM_MACHINE_PLUS3:
|
|
case LIBSPECTRUM_MACHINE_PLUS3E:
|
|
return 'B';
|
|
case LIBSPECTRUM_MACHINE_TS2068:
|
|
return 'C';
|
|
case LIBSPECTRUM_MACHINE_PENT:
|
|
case LIBSPECTRUM_MACHINE_PENT512:
|
|
case LIBSPECTRUM_MACHINE_PENT1024:
|
|
return 'D';
|
|
case LIBSPECTRUM_MACHINE_48_NTSC:
|
|
return 'E';
|
|
case LIBSPECTRUM_MACHINE_UNKNOWN:
|
|
default:
|
|
return '?';
|
|
}
|
|
}
|
|
|
|
static char
|
|
get_screentype( void )
|
|
{
|
|
if( machine_current->timex ) { /* ALTDFILE and default */
|
|
if( scld_last_dec.name.hires )
|
|
return 'R'; /* HIRES screen */
|
|
else if( scld_last_dec.name.b1 )
|
|
return 'C'; /* HICOLOR screen */
|
|
else
|
|
return 'X'; /* STANDARD screen on timex machine */
|
|
}
|
|
return '$'; /* STANDARD screen */
|
|
}
|
|
|
|
#ifdef HAVE_ZLIB_H
|
|
static void
|
|
fwrite_compr( const void *b, size_t n, size_t m, FILE *f )
|
|
{
|
|
if( fmf_compr == 0 ) {
|
|
fwrite( b, n, m, f );
|
|
} else {
|
|
zstream.avail_in = n * m;
|
|
zstream.next_in = b;
|
|
zstream.avail_out = ZBUF_SIZE;
|
|
zstream.next_out = zbuf_o;
|
|
do {
|
|
deflate( &zstream, Z_NO_FLUSH );
|
|
while( zstream.avail_out != ZBUF_SIZE ) {
|
|
fwrite( zbuf_o, ZBUF_SIZE - zstream.avail_out, 1, of );
|
|
zstream.avail_out = ZBUF_SIZE;
|
|
zstream.next_out = zbuf_o;
|
|
deflate( &zstream, Z_NO_FLUSH );
|
|
}
|
|
} while ( zstream.avail_in != 0 );
|
|
}
|
|
}
|
|
#else /* HAVE_ZLIB_H */
|
|
#define fwrite_compr fwrite
|
|
#endif /* HAVE_ZLIB_H */
|
|
|
|
static void
|
|
movie_compress_area( int x, int y, int w, int h, int s )
|
|
{
|
|
libspectrum_dword *dpoint, *dline;
|
|
libspectrum_byte d, d1, *b;
|
|
libspectrum_byte buff[ 960 ];
|
|
int w0, h0, l;
|
|
|
|
dline = &display_last_screen[x + 40 * y];
|
|
b = buff; l = -1;
|
|
d1 = ( ( *dline >> s ) & 0xff ) + 1; /* *d1 != dpoint :-) */
|
|
|
|
for( h0 = h; h0 > 0; h0--, dline += 40 ) {
|
|
dpoint = dline;
|
|
for( w0 = w; w0 > 0; w0--, dpoint++) {
|
|
d = ( *dpoint >> s ) & 0xff; /* bitmask1 */
|
|
if( d != d1 ) {
|
|
if( l > -1 ) { /* save running length 0-255 */
|
|
*b++ = l;
|
|
l = -1; /* reset l */
|
|
}
|
|
*b++ = d1 = d;
|
|
} else if( l >= 0 ) {
|
|
if( l == 255 ) { /* close run, and may start a new? */
|
|
*b++ = l; *b++ = d; l = -1;
|
|
} else {
|
|
l++;
|
|
}
|
|
} else {
|
|
*b++ = d;
|
|
l++;
|
|
}
|
|
/* d1 = d; */
|
|
}
|
|
if( b - buff > 960 - 128 ) { /* worst case 40*1.5 per line */
|
|
fwrite_compr( buff, b - buff, 1, of );
|
|
b = buff;
|
|
}
|
|
}
|
|
if( l > -1 ) { /* save running length 0-255 */
|
|
*b++ = l;
|
|
}
|
|
if( b != buff ) { /* dump remain */
|
|
fwrite_compr( buff, b - buff, 1, of );
|
|
}
|
|
}
|
|
|
|
/* Fetch pixel (x, y). On a Timex this will be a point on a 640x480 canvas,
|
|
on a Sinclair/Amstrad/Russian clone this will be a point on a 320x240
|
|
canvas */
|
|
|
|
/* x: 0 - 39; y: 0 - 239 */
|
|
|
|
/* abcdefghijkl... cc# where # mean cc + # c char*/
|
|
|
|
void
|
|
movie_add_area( int x, int y, int w, int h )
|
|
{
|
|
if( movie_paused ) {
|
|
movie_start_frame();
|
|
return;
|
|
}
|
|
head[0] = '$'; /* RLE compressed data... */
|
|
head[1] = x;
|
|
head[2] = y & 0xff;
|
|
head[3] = y >> 8;
|
|
head[4] = w;
|
|
head[5] = h & 0xff;
|
|
head[6] = h >> 8;
|
|
fwrite_compr( head, 7, 1, of );
|
|
movie_compress_area( x, y, w, h, 0 ); /* Bitmap1 */
|
|
movie_compress_area( x, y, w, h, 8 ); /* Attrib/B2 */
|
|
if( fmf_screen == 'R' ) {
|
|
movie_compress_area( x, y, w, h, 16 ); /* HiRes attrib */
|
|
}
|
|
slice_no++;
|
|
}
|
|
|
|
static void
|
|
movie_start_fmf( const char *name )
|
|
{
|
|
if( ( of = fopen(name, "wb") ) == NULL ) { /* trunc old file ? or append ? */
|
|
ui_error( UI_ERROR_ERROR, "error opening movie file '%s': %s", name,
|
|
strerror( errno ) );
|
|
return;
|
|
}
|
|
#ifdef WORDS_BIGENDIAN
|
|
fwrite( "FMF_V1E", 7, 1, of ); /* write magic header Fuse Movie File */
|
|
#else /* WORDS_BIGENDIAN */
|
|
fwrite( "FMF_V1e", 7, 1, of ); /* write magic header Fuse Movie File */
|
|
#endif /* WORDS_BIGENDIAN */
|
|
#ifdef HAVE_ZLIB_H
|
|
if( option_enumerate_movie_movie_compr() == 0 ) {
|
|
fmf_compr = 0;
|
|
fwrite( "U", 1, 1, of ); /* not compressed */
|
|
} else {
|
|
fmf_compr = Z_DEFAULT_COMPRESSION;
|
|
fwrite( "Z", 1, 1, of ); /* compressed */
|
|
}
|
|
if( fmf_compr != 0 ) {
|
|
zstream.zalloc = Z_NULL;
|
|
zstream.zfree = Z_NULL;
|
|
zstream.opaque = Z_NULL;
|
|
zstream.avail_in = 0;
|
|
zstream.next_in = Z_NULL;
|
|
deflateInit( &zstream, fmf_compr );
|
|
}
|
|
#else /* HAVE_ZLIB_H */
|
|
fwrite( "U", 1, 1, of ); /* cannot be compressed */
|
|
#endif /* HAVE_ZLIB_H */
|
|
movie_init_sound( settings_current.sound_freq,
|
|
sound_stereo_ay != SOUND_STEREO_AY_NONE );
|
|
head[0] = settings_current.frame_rate;
|
|
head[1] = get_screentype();
|
|
head[2] = get_timing();
|
|
head[3] = format; /* sound format */
|
|
head[4] = freq & 0xff;
|
|
head[5] = freq >> 8;
|
|
head[6] = stereo;
|
|
head[7] = '\n'; /* padding */
|
|
fwrite( head, 8, 1, of ); /* write initial params */
|
|
movie_add_area( 0, 0, 40, 240 );
|
|
}
|
|
|
|
void
|
|
movie_start( const char *name ) /* some init, open file (name)*/
|
|
{
|
|
frame_no = slice_no = 0;
|
|
if( name == NULL || *name == '\0' )
|
|
name = "fuse.fmf"; /* fuse movie file */
|
|
|
|
movie_start_fmf( name );
|
|
movie_recording = 1;
|
|
ui_menu_activate( UI_MENU_ITEM_FILE_MOVIE_RECORDING, 1 );
|
|
ui_menu_activate( UI_MENU_ITEM_FILE_MOVIE_PAUSE, 1 );
|
|
}
|
|
|
|
void
|
|
movie_stop( void )
|
|
{
|
|
if( !movie_paused && !movie_recording ) return;
|
|
|
|
fwrite_compr( "X", 1, 1, of ); /* End of Recording! */
|
|
#ifdef HAVE_ZLIB_H
|
|
{
|
|
if( fmf_compr != 0 ) { /* close zlib */
|
|
zstream.avail_in = 0;
|
|
do {
|
|
zstream.avail_out = ZBUF_SIZE;
|
|
zstream.next_out = zbuf_o;
|
|
deflate( &zstream, Z_SYNC_FLUSH );
|
|
if( zstream.avail_out != ZBUF_SIZE )
|
|
fwrite( zbuf_o, ZBUF_SIZE - zstream.avail_out, 1, of );
|
|
} while ( zstream.avail_out != ZBUF_SIZE );
|
|
deflateEnd( &zstream );
|
|
fmf_compr = -1;
|
|
}
|
|
}
|
|
#endif /* HAVE_ZLIB_H */
|
|
format = '?';
|
|
if( of ) {
|
|
fclose( of );
|
|
of = NULL;
|
|
}
|
|
#ifdef MOVIE_DEBUG_PRINT
|
|
fprintf( stderr, "Debug movie: saved %d.%d frame(.slice)\n", frame_no, slice_no );
|
|
#endif /* MOVIE_DEBUG_PRINT */
|
|
movie_recording = 0;
|
|
movie_paused = 0;
|
|
ui_menu_activate( UI_MENU_ITEM_FILE_MOVIE_RECORDING, 0 );
|
|
}
|
|
|
|
void
|
|
movie_pause( void )
|
|
{
|
|
if( !movie_paused && !movie_recording ) return;
|
|
|
|
if( movie_recording ) {
|
|
movie_recording = 0;
|
|
movie_paused = 1;
|
|
ui_menu_activate( UI_MENU_ITEM_FILE_MOVIE_PAUSE, 0 );
|
|
} else {
|
|
movie_recording = 1;
|
|
movie_paused = 1;
|
|
ui_menu_activate( UI_MENU_ITEM_FILE_MOVIE_PAUSE, 1 );
|
|
}
|
|
}
|
|
|
|
void
|
|
movie_init_sound( int f, int s )
|
|
{
|
|
/* initialise sound format */
|
|
format = option_enumerate_movie_movie_compr() == 2 ? 'A' : 'P';
|
|
freq = f;
|
|
stereo = ( s ? 'S' : 'M' );
|
|
framesiz = ( stereo == 'S' ? 2 : 1 ) * ( format == 'P' ? 2 : 1 );
|
|
}
|
|
|
|
static inline void
|
|
write_alaw( libspectrum_signed_word *buff, int len )
|
|
{
|
|
int i = 0;
|
|
while( len-- ) {
|
|
if( *buff >= 0)
|
|
sbuff[i++] = alaw_table[*buff >> 4];
|
|
else
|
|
sbuff[i++] = 0x7f & alaw_table [- *buff >> 4];
|
|
buff++;
|
|
if( i == 4096 ) {
|
|
i = 0;
|
|
fwrite_compr( sbuff, 4096, 1, of ); /* write frame */
|
|
}
|
|
}
|
|
if( i )
|
|
fwrite_compr( sbuff, i, 1, of ); /* write remaind */
|
|
}
|
|
|
|
static void
|
|
add_sound( libspectrum_signed_word *buff, int len )
|
|
{
|
|
head[0] = 'S'; /* sound frame */
|
|
head[1] = format; /* sound format */
|
|
head[2] = freq & 0xff;
|
|
head[3] = freq >> 8;
|
|
head[4] = stereo;
|
|
len--; /*len - 1*/
|
|
head[5] = len & 0xff;
|
|
head[6] = len >> 8;
|
|
len++; /* len :-) */
|
|
fwrite_compr( head, 7, 1, of ); /* Sound frame */
|
|
if( format == 'P' )
|
|
fwrite_compr( buff, len * framesiz , 1, of ); /* write frame */
|
|
else if( format == 'A' )
|
|
write_alaw( buff, len * framesiz );
|
|
}
|
|
|
|
void
|
|
movie_add_sound( libspectrum_signed_word *buff, int len )
|
|
{
|
|
while( len ) {
|
|
if( stereo == 'S' ) {
|
|
add_sound( buff, len > 131072 ? 65536 : len >> 1 );
|
|
buff += len > 131072 ? 131072 : len;
|
|
len -= len > 131072 ? 131072 : len;
|
|
} else {
|
|
add_sound( buff, len > 65536 ? 65536 : len );
|
|
buff += len > 65536 ? 65536 : len;
|
|
len -= len > 65536 ? 65536 : len;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
movie_start_frame( void )
|
|
{
|
|
/* $ - ZX$, T - TX$, C - HiCol, R - HiRes */
|
|
head[0] = 'N';
|
|
head[1] = settings_current.frame_rate;
|
|
head[2] = get_screentype();
|
|
head[3] = get_timing();
|
|
fwrite_compr( head, 4, 1, of ); /* New frame! */
|
|
frame_no++;
|
|
if( movie_paused ) {
|
|
movie_paused = 0;
|
|
movie_add_area( 0, 0, 40, 240 );
|
|
}
|
|
}
|
|
|
|
void
|
|
movie_init( void )
|
|
{
|
|
/* start movie recording if user requested... */
|
|
if( settings_current.movie_start )
|
|
movie_start( settings_current.movie_start );
|
|
}
|