//mp3 menu and track selector. //was origonally an mp3 track selector, now handles lots of media specific stuff - like q3 films! //should rename to m_media.c #include "quakedef.h" #ifdef GLQUAKE #include "glquake.h" #endif #ifdef VKQUAKE #include "../vk/vkrenderer.h" #endif #include "shader.h" #include "gl_draw.h" #if defined(HAVE_JUKEBOX) #if defined(_WIN32) && !defined(WINRT) && !defined(NOMEDIAMENU) //#define WINAMP #endif #endif #if (defined(HAVE_MEDIA_DECODER) || defined(HAVE_MEDIA_ENCODER)) && defined(_WIN32) && !defined(WINRT) #define HAVE_API_VFW #endif #ifdef _WIN32 #include "winquake.h" #endif #if defined(AVAIL_MP3_ACM) || defined(HAVE_API_VFW) //equivelent to //#include #undef CDECL //windows is stupid at times. #define CDECL __cdecl #if defined(_MSC_VER) && (_MSC_VER < 1300) #define DWORD_PTR DWORD #endif DECLARE_HANDLE(HACMSTREAM); typedef HACMSTREAM *LPHACMSTREAM; DECLARE_HANDLE(HACMDRIVER); typedef struct { DWORD cbStruct; DWORD fdwStatus; DWORD_PTR dwUser; LPBYTE pbSrc; DWORD cbSrcLength; DWORD cbSrcLengthUsed; DWORD_PTR dwSrcUser; LPBYTE pbDst; DWORD cbDstLength; DWORD cbDstLengthUsed; DWORD_PTR dwDstUser; DWORD dwReservedDriver[10]; } ACMSTREAMHEADER, *LPACMSTREAMHEADER; #define ACM_STREAMCONVERTF_BLOCKALIGN 0x00000004 //mingw workarounds #define LPWAVEFILTER void * #include MMRESULT (WINAPI *qacmStreamUnprepareHeader) (HACMSTREAM has, LPACMSTREAMHEADER pash, DWORD fdwUnprepare); MMRESULT (WINAPI *qacmStreamConvert) (HACMSTREAM has, LPACMSTREAMHEADER pash, DWORD fdwConvert); MMRESULT (WINAPI *qacmStreamPrepareHeader) (HACMSTREAM has, LPACMSTREAMHEADER pash, DWORD fdwPrepare); MMRESULT (WINAPI *qacmStreamOpen) (LPHACMSTREAM phas, HACMDRIVER had, LPWAVEFORMATEX pwfxSrc, LPWAVEFORMATEX pwfxDst, LPWAVEFILTER pwfltr, DWORD_PTR dwCallback, DWORD_PTR dwInstance, DWORD fdwOpen); MMRESULT (WINAPI *qacmStreamClose) (HACMSTREAM has, DWORD fdwClose); static qboolean qacmStartup(void) { static int inited; static dllhandle_t *module; if (!inited) { dllfunction_t funcs[] = { {(void*)&qacmStreamUnprepareHeader, "acmStreamUnprepareHeader"}, {(void*)&qacmStreamConvert, "acmStreamConvert"}, {(void*)&qacmStreamPrepareHeader, "acmStreamPrepareHeader"}, {(void*)&qacmStreamOpen, "acmStreamOpen"}, {(void*)&qacmStreamClose, "acmStreamClose"}, {NULL,NULL} }; inited = true; module = Sys_LoadLibrary("msacm32.dll", funcs); } return module?true:false; } #endif static char media_currenttrack[MAX_QPATH]; static cvar_t music_fade = CVAR("music_fade", "1"); //higher bits have priority (if they have something to play). #define MEDIA_GAMEMUSIC (1u<<0) //cd music. also music command etc. #ifdef HAVE_JUKEBOX #define MEDIA_CVARLIST (1u<<1) //cvar abuse. handy for preserving times when switching tracks. #define MEDIA_PLAYLIST (1u<<2) // static unsigned int media_playlisttypes; static unsigned int media_playlistcurrent; //cvar abuse static int music_playlist_last; static cvar_t music_playlist_index = CVAR("music_playlist_index", "-1"); // created dynamically: CVAR("music_playlist_list0+", ""), // created dynamically: CVAR("music_playlist_sampleposition0+", "-1"), typedef struct mediatrack_s{ char filename[MAX_QPATH]; char nicename[MAX_QPATH]; int length; struct mediatrack_s *next; } mediatrack_t; //info about the current stuff that is playing. static char media_friendlyname[MAX_QPATH]; int lasttrackplayed; cvar_t media_shuffle = CVAR("media_shuffle", "1"); cvar_t media_repeat = CVAR("media_repeat", "1"); #ifdef WINAMP cvar_t media_hijackwinamp = CVAR("media_hijackwinamp", "0"); #endif int selectedoption=-1; int numtracks; int nexttrack=-1; mediatrack_t *tracks; char media_iofilename[MAX_OSPATH]=""; #if !defined(NOMEDIAMENU) && !defined(NOBUILTINMENUS) static int mouseselectedoption=-1; void Media_LoadTrackNames (char *listname); void Media_SaveTrackNames (char *listname); int loadedtracknames; #endif qboolean Media_EvaluateNextTrack(void); #else #define NOMEDIAMENU //the media menu requires us to be able to queue up random tracks and stuff. #endif #ifdef HAVE_CDPLAYER static int cdplayingtrack; //currently playing cd track (becomes 0 when paused) static int cdpausedtrack; //currently paused cd track //info about (fake)cd tracks that we want to play int cdplaytracknum; //info about (fake)cd tracks that we could play if asked. #define REMAPPED_TRACKS 256 static struct { char fname[MAX_QPATH]; } cdremap[REMAPPED_TRACKS]; static qboolean cdenabled; static int cdnumtracks; //maximum cd track we can play. #endif static char media_playtrack[MAX_QPATH]; //name of track to play next/now static char media_loopingtrack[MAX_QPATH]; //name of track to loop afterwards qboolean media_fadeout; float media_fadeouttime; qboolean Media_CleanupTrackName(const char *track, int *out_track, char *result, size_t resultsize); //whatever music track was previously playing has terminated. //return value is the new sample to start playing. //*starttime says the time into the track that we should resume playing at //this is on the main thread with the mixer locked, its safe to do stuff, but try not to block sfx_t *Media_NextTrack(int musicchannelnum, float *starttime) { sfx_t *s = NULL; if (bgmvolume.value <= 0 || mastervolume.value <= 0) return NULL; media_fadeout = false; //it has actually ended now, at least on one device. don't fade the new track too... Q_strncpyz(media_currenttrack, "", sizeof(media_currenttrack)); #ifdef HAVE_JUKEBOX Q_strncpyz(media_friendlyname, "", sizeof(media_friendlyname)); music_playlist_index.modified = false; music_playlist_last = -1; media_playlistcurrent = 0; if (!media_playlistcurrent && (media_playlisttypes & MEDIA_PLAYLIST)) { #if !defined(NOMEDIAMENU) && !defined(NOBUILTINMENUS) if (!loadedtracknames) Media_LoadTrackNames("sound/media.m3u"); #endif if (Media_EvaluateNextTrack()) { media_playlistcurrent = MEDIA_PLAYLIST; if (*media_currenttrack == '#') return S_PrecacheSound2(media_currenttrack+1, true); else return S_PrecacheSound(media_currenttrack); } } if (!media_playlistcurrent && (media_playlisttypes & MEDIA_CVARLIST)) { if (music_playlist_index.ival >= 0) { cvar_t *list = Cvar_Get(va("music_playlist_list%i", music_playlist_index.ival), "", 0, "compat"); if (list) { cvar_t *sampleposition = Cvar_Get(va("music_playlist_sampleposition%i", music_playlist_index.ival), "-1", 0, "compat"); Q_snprintfz(media_currenttrack, sizeof(media_currenttrack), "sound/cdtracks/%s", list->string); Q_strncpyz(media_friendlyname, "", sizeof(media_friendlyname)); media_playlistcurrent = MEDIA_CVARLIST; music_playlist_last = music_playlist_index.ival; if (sampleposition) { *starttime = sampleposition->value; if (*starttime == -1) *starttime = 0; } else *starttime = 0; s = S_PrecacheSound(media_currenttrack); } } } if (!media_playlistcurrent && (media_playlisttypes & MEDIA_GAMEMUSIC)) #endif { #ifdef HAVE_CDPLAYER if (cdplaytracknum) { if (cdplayingtrack != cdplaytracknum && cdpausedtrack != cdplaytracknum) { CDAudio_Play(cdplaytracknum); cdplayingtrack = cdplaytracknum; } #ifdef HAVE_JUKEBOX media_playlistcurrent = MEDIA_GAMEMUSIC; #endif return NULL; } #endif if (*media_playtrack || *media_loopingtrack) { if (*media_playtrack) { Q_strncpyz(media_currenttrack, media_playtrack, sizeof(media_currenttrack)); *media_playtrack = 0; } else { if (!Media_CleanupTrackName(media_loopingtrack, NULL, media_currenttrack, sizeof(media_currenttrack))) Q_strncpyz(media_currenttrack, "", sizeof(media_currenttrack)); } #ifdef HAVE_JUKEBOX Q_strncpyz(media_friendlyname, "", sizeof(media_friendlyname)); media_playlistcurrent = MEDIA_GAMEMUSIC; #endif s = S_PrecacheSound(media_currenttrack); } } return s; } //begin cross-fading static qboolean Media_Changed (unsigned int mediatype) { #ifdef HAVE_JUKEBOX //something changed, but it has a lower priority so we don't care if (mediatype < media_playlistcurrent) return false; #endif #ifdef HAVE_CDPLAYER //make sure we're not playing any cd music. if (cdplayingtrack || cdpausedtrack) { cdplayingtrack = 0; cdpausedtrack = 0; CDAudio_Stop(); } #endif media_fadeout = music_fade.ival; media_fadeouttime = realtime; return true; } //returns the new volume the sample should be at, to support fading. //if we return < 0, the mixer will know to kill whatever is currently playing, ready for a new track. //this is on the main thread with the mixer locked, its safe to do stuff, but try not to block float Media_CrossFade(int musicchanel, float vol, float time) { if (media_fadeout) { float fadetime = 1; float frac = (fadetime + media_fadeouttime - realtime)/fadetime; vol *= frac; } #ifdef HAVE_JUKEBOX else if (music_playlist_index.modified) { if (Media_Changed(MEDIA_CVARLIST)) { if (music_playlist_last >= 0) { cvar_t *sampleposition = Cvar_Get(va("music_playlist_sampleposition%i", music_playlist_last), "-1", 0, "compat"); if (sampleposition && sampleposition->value != -1) Cvar_SetValue(sampleposition, time); } vol = -1; //kill it NOW } } #endif return vol; } void Media_WriteCurrentTrack(sizebuf_t *buf) { //fixme: for demo playback MSG_WriteByte (buf, svc_cdtrack); MSG_WriteByte (buf, 0); } qboolean Media_CleanupTrackName(const char *track, int *out_track, char *result, size_t resultsize) { //FIXME: for q2, gog uses ../music/Track%02i.ogg, with various remapping requirements for the mission packs. static char *path[] = { "music/", "sound/cdtracks/", "", NULL }; static char *ext[] = { "", #if defined(AVAIL_OGGOPUS) || defined(FTE_TARGET_WEB) ".opus", //opus might be the future, but ogg is the present #endif #if defined(AVAIL_OGGVORBIS) || defined(FTE_TARGET_WEB) ".ogg", #endif #if defined(AVAIL_MP3_ACM) || defined(FTE_TARGET_WEB) ".mp3", #endif ".wav", NULL }; unsigned int tracknum; char *trackend; unsigned int ip, ie; int bestdepth = 0x7fffffff, d; char tryname[MAX_QPATH]; //check if its a proper number (0123456789 without any other weird stuff. if so, we can use fake track paths or actual cd tracks) tracknum = strtoul(track, &trackend, 10); if (*trackend) tracknum = 0; //not a single integer #ifdef HAVE_CDPLAYER if (tracknum > 0 && tracknum < REMAPPED_TRACKS && *cdremap[tracknum].fname) { //remap the track if its remapped. track = cdremap[tracknum].fname; tracknum = strtoul(track, &trackend, 0); if (*trackend) tracknum = 0; } #endif #ifndef HAVE_LEGACY if (!tracknum) //might as well require exact file { Q_snprintfz(result, resultsize, "%s", track); d = COM_FCheckExists(result); } else #endif for(ip = 0; path[ip]; ip++) { if (tracknum) { if (*path[ip]) { if (tracknum <= 999) { for(ie = 0; ext[ie]; ie++) { Q_snprintfz(tryname, sizeof(tryname), "%strack%03u%s", path[ip], tracknum, ext[ie]); d = COM_FDepthFile(tryname, false); if (d < bestdepth) { bestdepth = d; Q_strncpy(result, tryname, resultsize); } } } if (tracknum <= 99) { for(ie = 0; ext[ie]; ie++) { Q_snprintfz(tryname, sizeof(tryname), "%strack%02u%s", path[ip], tracknum, ext[ie]); d = COM_FDepthFile(tryname, false); if (d < bestdepth) { bestdepth = d; Q_strncpy(result, tryname, resultsize); } } } } } else { for(ie = 0; ext[ie]; ie++) { Q_snprintfz(tryname, sizeof(tryname), "%s%s%s", path[ip], track, ext[ie]); d = COM_FDepthFile(tryname, false); if (d < bestdepth) { bestdepth = d; Q_strncpy(result, tryname, resultsize); } } } } if (out_track) *out_track = tracknum; if (bestdepth < 0x7fffffff) return true; return false; } //controls which music track should be playing right now //track and looptrack will usually be the same thing, track is what to play NOW, looptrack is what to keep re-playing after, or "-" for stop. qboolean Media_NamedTrack(const char *track, const char *looptrack) { unsigned int tracknum; char trackname[MAX_QPATH]; if (!track && !looptrack) { *media_playtrack = *media_loopingtrack = 0; Media_Changed(MEDIA_GAMEMUSIC); return true; } if (!track || !*track) //ignore calls if the primary track is invalid. whatever is already playing will continue to play. return false; if (!looptrack || !*looptrack) //null or empty looptrack loops using the primary track, for compat with q3. looptrack = track; if (!strcmp(looptrack, "-")) //- for the looptrack argument can be used to prevent looping. looptrack = ""; //okay, do that faketrack thing if we got one. if (Media_CleanupTrackName(track, &tracknum, trackname, sizeof(trackname))) { #ifdef HAVE_CDPLAYER cdplaytracknum = 0; #endif Q_strncpyz(media_playtrack, trackname, sizeof(media_playtrack)); Q_strncpyz(media_loopingtrack, looptrack, sizeof(media_loopingtrack)); Media_Changed(MEDIA_GAMEMUSIC); return true; } #ifdef HAVE_CDPLAYER //couldn't do a faketrack, resort to actual cd tracks, if we're allowed if (tracknum && cdenabled) { Q_strncpyz(media_loopingtrack, looptrack, sizeof(media_loopingtrack)); if (!CDAudio_Startup()) return false; if (cdnumtracks <= 0) cdnumtracks = CDAudio_GetAudioDiskInfo(); if (tracknum > cdnumtracks) { Con_DPrintf("CDAudio: Bad track number %u.\n", tracknum); return false; //can't play that, sorry. its not an available track } if (cdplayingtrack == tracknum) return true; //already playing, don't need to do anything cdplaytracknum = tracknum; Q_strncpyz(media_playtrack, "", sizeof(media_playtrack)); Media_Changed(MEDIA_GAMEMUSIC); return true; } #endif return false; } //for q3 void Media_NamedTrack_f(void) { if (Cmd_Argc() == 3) Media_NamedTrack(Cmd_Argv(1), Cmd_Argv(2)); else Media_NamedTrack(Cmd_Argv(1), Cmd_Argv(1)); } void Media_StopTrack_f(void) { *media_playtrack = *media_loopingtrack = 0; Media_Changed(MEDIA_GAMEMUSIC); } void Media_NumberedTrack(unsigned int initialtrack, unsigned int looptrack) { char *init = initialtrack?va("%u", initialtrack):NULL; char *loop = looptrack?va("%u", looptrack):NULL; Media_NamedTrack(init, loop); } void Media_EndedTrack(void) { #ifdef HAVE_CDPLAYER cdplayingtrack = 0; cdpausedtrack = 0; #endif if (*media_loopingtrack) Media_NamedTrack(media_loopingtrack, media_loopingtrack); } void Media_SetPauseTrack(qboolean paused) { #ifdef HAVE_CDPLAYER if (paused) { if (!cdplayingtrack) return; cdpausedtrack = cdplayingtrack; cdplayingtrack = 0; CDAudio_Pause(); } else { if (!cdpausedtrack) return; cdplayingtrack = cdpausedtrack; cdpausedtrack = 0; CDAudio_Resume(); } #endif } #ifdef HAVE_CDPLAYER void Media_SaveTracks(vfsfile_t *outcfg) { unsigned int n; char buf[MAX_QPATH*4]; for (n = 1; n < REMAPPED_TRACKS; n++) if (*cdremap[n].fname) Con_Printf("cd remap %u %s\n", n, COM_QuotedString(cdremap[n].fname, buf, sizeof(buf), false)); } void CD_f (void) { char *command; int ret; int n; if (Cmd_Argc() < 2) return; command = Cmd_Argv (1); if (Q_strcasecmp(command, "play") == 0) { Media_NamedTrack(Cmd_Argv(2), "-"); return; } if (Q_strcasecmp(command, "loop") == 0) { if (Cmd_Argc() < 4) Media_NamedTrack(Cmd_Argv(2), NULL); else Media_NamedTrack(Cmd_Argv(2), Cmd_Argv(3)); return; } if (Q_strcasecmp(command, "remap") == 0) { ret = Cmd_Argc() - 2; if (ret <= 0) { for (n = 1; n < REMAPPED_TRACKS; n++) if (*cdremap[n].fname) Con_Printf(" %u -> %s\n", n, cdremap[n].fname); return; } for (n = 1; n <= ret && n < REMAPPED_TRACKS; n++) Q_strncpyz(cdremap[n].fname, Cmd_Argv (n+1), sizeof(cdremap[n].fname)); return; } if (Q_strcasecmp(command, "stop") == 0) { *media_playtrack = *media_loopingtrack = 0; cdplaytracknum = 0; Media_Changed(MEDIA_GAMEMUSIC); return; } if (!bgmvolume.value) { Con_Printf("Background music is disabled: %s is 0\n", bgmvolume.name); return; } if (!CDAudio_Startup()) { Con_Printf("No cd drive detected\n"); return; } if (Q_strcasecmp(command, "on") == 0) { cdenabled = true; return; } if (Q_strcasecmp(command, "off") == 0) { if (cdplayingtrack || cdpausedtrack) CDAudio_Stop(); cdenabled = false; *media_playtrack = *media_loopingtrack = 0; cdplaytracknum = 0; Media_Changed(MEDIA_GAMEMUSIC); return; } if (Q_strcasecmp(command, "reset") == 0) { cdenabled = true; if (cdplayingtrack || cdpausedtrack) CDAudio_Stop(); for (n = 0; n < REMAPPED_TRACKS; n++) strcpy(cdremap[n].fname, ""); cdnumtracks = CDAudio_GetAudioDiskInfo(); return; } if (Q_strcasecmp(command, "close") == 0) { CDAudio_CloseDoor(); return; } if (cdnumtracks <= 0) { cdnumtracks = CDAudio_GetAudioDiskInfo(); if (cdnumtracks < 0) { Con_Printf("No CD in player.\n"); return; } } if (Q_strcasecmp(command, "pause") == 0) { Media_SetPauseTrack(true); return; } if (Q_strcasecmp(command, "resume") == 0) { Media_SetPauseTrack(false); return; } if (Q_strcasecmp(command, "eject") == 0) { if (Cmd_IsInsecure()) return; if (cdplayingtrack || cdpausedtrack) CDAudio_Stop(); CDAudio_Eject(); cdnumtracks = -1; return; } if (Q_strcasecmp(command, "info") == 0) { Con_Printf("%u tracks\n", cdnumtracks); if (cdplayingtrack > 0) Con_Printf("Currently %s track %u\n", *media_loopingtrack ? "looping" : "playing", cdplayingtrack); else if (cdpausedtrack > 0) Con_Printf("Paused %s track %u\n", *media_loopingtrack ? "looping" : "playing", cdpausedtrack); return; } } #endif #ifdef HAVE_JUKEBOX #ifdef WINAMP #include "winamp.h" HWND hwnd_winamp; #endif qboolean Media_EvaluateNextTrack(void); #ifdef WINAMP qboolean WinAmp_GetHandle (void) { if ((hwnd_winamp = FindWindowW(L"Winamp", NULL))) return true; if ((hwnd_winamp = FindWindowW(L"Winamp v1.x", NULL))) return true; *currenttrack.nicename = '\0'; return false; } qboolean WinAmp_StartTune(char *name) { int trys; int pos; COPYDATASTRUCT cds; if (!WinAmp_GetHandle()) return false; //FIXME: extract from fs if it's in a pack. //FIXME: always give absolute path cds.dwData = IPC_PLAYFILE; cds.lpData = (void *) name; cds.cbData = strlen((char *) cds.lpData)+1; // include space for null char SendMessage(hwnd_winamp,WM_WA_IPC,0,IPC_DELETE); SendMessage(hwnd_winamp,WM_COPYDATA,(WPARAM)NULL,(LPARAM)&cds); SendMessage(hwnd_winamp,WM_WA_IPC,(WPARAM)0,IPC_STARTPLAY ); for (trys = 1000; trys; trys--) { pos = SendMessage(hwnd_winamp,WM_WA_IPC,0,IPC_GETOUTPUTTIME); if (pos>100 && SendMessage(hwnd_winamp,WM_WA_IPC,0,IPC_GETOUTPUTTIME)>=0) //tune has started break; Sleep(10); //give it a chance. if (!WinAmp_GetHandle()) break; } return true; } void WinAmp_Think(void) { int pos; int len; if (!WinAmp_GetHandle()) return; pos = bgmvolume.value*255; if (pos > 255) pos = 255; if (pos < 0) pos = 0; PostMessage(hwnd_winamp, WM_WA_IPC,pos,IPC_SETVOLUME); //optimise this to reduce calls? pos = SendMessage(hwnd_winamp,WM_WA_IPC,0,IPC_GETOUTPUTTIME); len = SendMessage(hwnd_winamp,WM_WA_IPC,1,IPC_GETOUTPUTTIME)*1000; if ((pos > len || pos <= 100) && len != -1) if (Media_EvaluateNextTrack()) WinAmp_StartTune(currenttrack.filename); } #endif void Media_Seek (float time) { #ifdef WINAMP if (media_hijackwinamp.value) { int pos; if (WinAmp_GetHandle()) { pos = SendMessage(hwnd_winamp,WM_WA_IPC,0,IPC_GETOUTPUTTIME); pos += time*1000; PostMessage(hwnd_winamp,WM_WA_IPC,pos,IPC_JUMPTOTIME); WinAmp_Think(); } } #endif S_Music_Seek(time); } void Media_FForward_f(void) { float time = atoi(Cmd_Argv(1)); if (!time) time = 15; Media_Seek(time); } void Media_Rewind_f (void) { float time = atoi(Cmd_Argv(1)); if (!time) time = 15; Media_Seek(-time); } qboolean Media_EvaluateNextTrack(void) { mediatrack_t *track; int trnum; if (!tracks) return false; if (nexttrack>=0) { trnum = nexttrack; for (track = tracks; track; track=track->next) { if (!trnum) { Q_strncpyz(media_currenttrack, track->filename, sizeof(media_currenttrack)); Q_strncpyz(media_friendlyname, track->nicename, sizeof(media_friendlyname)); lasttrackplayed = nexttrack; break; } trnum--; } nexttrack = -1; } else { if (media_shuffle.value) nexttrack=((float)(rand()&0x7fff)/0x7fff)*numtracks; else { nexttrack = lasttrackplayed+1; if (nexttrack >= numtracks) { if (media_repeat.value) nexttrack = 0; else { *media_currenttrack='\0'; *media_friendlyname='\0'; return false; } } } trnum = nexttrack; for (track = tracks; track; track=track->next) { if (!trnum) { Q_strncpyz(media_currenttrack, track->filename, sizeof(media_currenttrack)); Q_strncpyz(media_friendlyname, track->nicename, sizeof(media_friendlyname)); lasttrackplayed = nexttrack; break; } trnum--; } nexttrack = -1; } return true; } //actually, this func just flushes and states that it should be playing. the ambientsound func actually changes the track. void Media_Next_f (void) { Media_Changed(MEDIA_PLAYLIST); #ifdef WINAMP if (media_hijackwinamp.value) { if (WinAmp_GetHandle()) if (Media_EvaluateNextTrack()) WinAmp_StartTune(currenttrack.filename); } #endif } void Media_AddTrack(const char *fname) { mediatrack_t *newtrack; if (!*fname) return; for (newtrack = tracks; newtrack; newtrack = newtrack->next) { if (!strcmp(newtrack->filename, fname)) return; //already added. ho hum } newtrack = Z_Malloc(sizeof(mediatrack_t)); Q_strncpyz(newtrack->filename, fname, sizeof(newtrack->filename)); Q_strncpyz(newtrack->nicename, COM_SkipPath(fname), sizeof(newtrack->nicename)); newtrack->length = 0; newtrack->next = tracks; tracks = newtrack; numtracks++; if (numtracks == 1) Media_Changed(MEDIA_PLAYLIST); } void Media_RemoveTrack(const char *fname) { mediatrack_t **link, *newtrack; if (!*fname) return; for (link = &tracks; *link; link = &(*link)->next) { newtrack = *link; if (!strcmp(newtrack->filename, fname)) { *link = newtrack->next; numtracks--; if (!strcmp(media_currenttrack, newtrack->filename)) Media_Changed(MEDIA_PLAYLIST); Z_Free(newtrack); return; } } } #if !defined(NOMEDIAMENU) && !defined(NOBUILTINMENUS) void M_Media_Add_f (void) { char *fname = Cmd_Argv(1); if (!loadedtracknames) Media_LoadTrackNames("sound/media.m3u"); if (Cmd_Argc() == 1) Con_Printf("%s \n", Cmd_Argv(0)); else Media_AddTrack(fname); Media_SaveTrackNames("sound/media.m3u"); } void M_Media_Remove_f (void) { char *fname = Cmd_Argv(1); if (Cmd_Argc() == 1) Con_Printf("%s \n", Cmd_Argv(0)); else Media_RemoveTrack(fname); Media_SaveTrackNames("sound/media.m3u"); } void Media_LoadTrackNames (char *listname); #define MEDIA_MIN -7 #define MEDIA_VOLUME -7 #define MEDIA_FASTFORWARD -6 #define MEDIA_CLEARLIST -5 #define MEDIA_ADDTRACK -4 #define MEDIA_ADDLIST -3 #define MEDIA_SHUFFLE -2 #define MEDIA_REPEAT -1 void M_Media_Draw (emenu_t *menu) { mediatrack_t *track; int y; int op, i, mop; qboolean playing; float time, duration; char title[256]; playing = S_GetMusicInfo(0, &time, &duration, title, sizeof(title)); #define MP_Hightlight(x,y,text,hl) (hl?M_PrintWhite(x, y, text):M_Print(x, y, text)) if (!bgmvolume.value) M_Print (12, 32, "Not playing - no volume"); else if (!*media_currenttrack) { if (!tracks) M_Print (12, 32, "Not playing - no track to play"); else { #ifdef WINAMP if (!WinAmp_GetHandle()) M_Print (12, 32, "Please start WinAmp 2"); else #endif M_Print (12, 32, "Not playing - switched off"); } } else { M_Print (12, 32, "Currently playing:"); if (*title) { M_Print (12, 40, title); M_Print (12, 48, media_currenttrack); } else M_Print (12, 40, *media_friendlyname?media_friendlyname:media_currenttrack); } y=60; op = selectedoption - (vid.height-y)/16; if (op + (vid.height-y)/8>numtracks) op = numtracks - (vid.height-y)/8; if (op < MEDIA_MIN) op = MEDIA_MIN; mop = (mousecursor_y - y)/8; mop += MEDIA_MIN; mouseselectedoption = MEDIA_MIN-1; if (mousecursor_x < 12 + ((vid.width - 320)>>1) || mousecursor_x > 320-24 + ((vid.width - 320)>>1)) mop = mouseselectedoption; while(op < 0) { if (op == mop) { float alphamax = 0.5, alphamin = 0.2; mouseselectedoption = op; R2D_ImageColours(.5,.4,0,(sin(realtime*2)+1)*0.5*(alphamax-alphamin)+alphamin); R2D_FillBlock(12 + ((vid.width - 320)>>1), y, 320-24, 8); R2D_ImageColours(1.0, 1.0, 1.0, 1.0); } switch(op) { case MEDIA_VOLUME: MP_Hightlight (12, y, va("<< Volume %2i%% >>", (int)(100*bgmvolume.value)), op == selectedoption); y+=8; break; case MEDIA_CLEARLIST: MP_Hightlight (12, y, "Clear all", op == selectedoption); y+=8; break; case MEDIA_FASTFORWARD: { if (playing) { int itime = time; int iduration = duration; if (iduration) { int pct = (int)((100*time)/duration); MP_Hightlight (12, y, va("<< %i:%02i / %i:%02i - %i%% >>", itime/60, itime%60, iduration/60, iduration%60, pct), op == selectedoption); } else MP_Hightlight (12, y, va("<< %i:%02i >>", itime/60, itime%60), op == selectedoption); } else MP_Hightlight (12, y, "<< skip >>", op == selectedoption); } y+=8; break; case MEDIA_ADDTRACK: MP_Hightlight (12, y, "Add Track", op == selectedoption); if (op == selectedoption) M_PrintWhite (12+9*8, y, media_iofilename); y+=8; break; case MEDIA_ADDLIST: MP_Hightlight (12, y, "Add List", op == selectedoption); if (op == selectedoption) M_PrintWhite (12+9*8, y, media_iofilename); y+=8; break; case MEDIA_SHUFFLE: if (media_shuffle.value) MP_Hightlight (12, y, "Shuffle on", op == selectedoption); else MP_Hightlight (12, y, "Shuffle off", op == selectedoption); y+=8; break; case MEDIA_REPEAT: if (!media_shuffle.value) { if (media_repeat.value) MP_Hightlight (12, y, "Repeat on", op == selectedoption); else MP_Hightlight (12, y, "Repeat off", op == selectedoption); } else { if (media_repeat.value) MP_Hightlight (12, y, "(Repeat on)", op == selectedoption); else MP_Hightlight (12, y, "(Repeat off)", op == selectedoption); } y+=8; break; } op++; } for (track = tracks, i=0; track && inext, i++); for (; track; track=track->next, y+=8, op++) { if (track->length != (int)duration && *title && !strcmp(track->filename, media_currenttrack)) { Q_strncpyz(track->nicename, title, sizeof(track->nicename)); track->length = duration; Media_SaveTrackNames("sound/media.m3u"); } if (op == mop) { float alphamax = 0.5, alphamin = 0.2; mouseselectedoption = op; R2D_ImageColours(.5,.4,0,(sin(realtime*2)+1)*0.5*(alphamax-alphamin)+alphamin); R2D_FillBlock(12 + ((vid.width - 320)>>1), y, 320-24, 8); R2D_ImageColours(1.0, 1.0, 1.0, 1.0); } if (op == selectedoption) M_PrintWhite (12, y, track->nicename); else M_Print (12, y, track->nicename); } } char compleatenamepath[MAX_OSPATH]; char compleatenamename[MAX_OSPATH]; qboolean compleatenamemultiple; int QDECL Com_CompleatenameCallback(const char *name, qofs_t size, time_t mtime, void *data, searchpathfuncs_t *spath) { if (*compleatenamename) compleatenamemultiple = true; Q_strncpyz(compleatenamename, name, sizeof(compleatenamename)); return true; } void Com_CompleateOSFileName(char *name) { char *ending; compleatenamemultiple = false; strcpy(compleatenamepath, name); ending = COM_SkipPath(compleatenamepath); if (compleatenamepath!=ending) ending[-1] = '\0'; //strip a slash *compleatenamename='\0'; Sys_EnumerateFiles(NULL, va("%s*", name), Com_CompleatenameCallback, NULL, NULL); Sys_EnumerateFiles(NULL, va("%s*.*", name), Com_CompleatenameCallback, NULL, NULL); if (*compleatenamename) strcpy(name, compleatenamename); } qboolean M_Media_Key (int key, emenu_t *menu) { int dir; if (key == K_ESCAPE || key == K_GP_BACK || key == K_MOUSE2) { return false; } else if (key == K_RIGHTARROW || key == K_KP_RIGHTARROW || key == K_GP_DPAD_RIGHT || key == K_LEFTARROW || key == K_KP_LEFTARROW || key == K_GP_DPAD_LEFT) { if (key == K_RIGHTARROW || key == K_KP_RIGHTARROW || key == K_GP_DPAD_RIGHT) dir = 1; else dir = -1; switch(selectedoption) { case MEDIA_VOLUME: bgmvolume.value += dir * 0.1; if (bgmvolume.value < 0) bgmvolume.value = 0; if (bgmvolume.value > 1) bgmvolume.value = 1; Cvar_SetValue (&bgmvolume, bgmvolume.value); break; case MEDIA_FASTFORWARD: Media_Seek(15*dir); break; default: if (selectedoption >= 0) Media_Next_f(); break; } } else if (key == K_DOWNARROW || key == K_KP_DOWNARROW || key == K_GP_DPAD_DOWN) { selectedoption++; if (selectedoption>=numtracks) selectedoption = numtracks-1; } else if (key == K_PGDN) { selectedoption+=10; if (selectedoption>=numtracks) selectedoption = numtracks-1; } else if (key == K_UPARROW || key == K_KP_UPARROW || key == K_GP_DPAD_UP) { selectedoption--; if (selectedoption < MEDIA_MIN) selectedoption = MEDIA_MIN; } else if (key == K_PGUP) { selectedoption-=10; if (selectedoption < MEDIA_MIN) selectedoption = MEDIA_MIN; } else if (key == K_DEL) { if (selectedoption>=0) { mediatrack_t *prevtrack=NULL, *tr; int num=0; tr=tracks; while(tr) { if (num == selectedoption) { if (prevtrack) prevtrack->next = tr->next; else tracks = tr->next; numtracks--; if (!strcmp(media_currenttrack, tr->filename)) Media_Changed(MEDIA_PLAYLIST); Z_Free(tr); break; } prevtrack = tr; tr=tr->next; num++; } } } else if (key == K_ENTER || key == K_KP_ENTER || key == K_GP_START || key == K_MOUSE1) { if (key == K_MOUSE1) { if (mouseselectedoption < MEDIA_MIN) return false; selectedoption = mouseselectedoption; } switch(selectedoption) { case MEDIA_FASTFORWARD: Media_Seek(15); break; case MEDIA_CLEARLIST: { mediatrack_t *prevtrack; while(tracks) { prevtrack = tracks; tracks=tracks->next; Z_Free(prevtrack); numtracks--; } if (numtracks!=0) { numtracks=0; Con_SafePrintf("numtracks should be 0\n"); } Media_Changed(MEDIA_PLAYLIST); } break; case MEDIA_ADDTRACK: if (*media_iofilename) Media_AddTrack(media_iofilename); else Cmd_ExecuteString("menu_mediafiles", RESTRICT_LOCAL); break; case MEDIA_ADDLIST: if (*media_iofilename) Media_LoadTrackNames(media_iofilename); break; case MEDIA_SHUFFLE: Cvar_Set(&media_shuffle, media_shuffle.value?"0":"1"); break; case MEDIA_REPEAT: Cvar_Set(&media_repeat, media_repeat.value?"0":"1"); break; default: if (selectedoption>=0) { nexttrack = selectedoption; Media_Next_f(); return true; } return false; } return true; } else { if (selectedoption == MEDIA_ADDLIST || selectedoption == MEDIA_ADDTRACK) { if (key == K_TAB) { Com_CompleateOSFileName(media_iofilename); return true; } else if (key == K_BACKSPACE) { dir = strlen(media_iofilename); if (dir) media_iofilename[dir-1] = '\0'; return true; } else if ((key >= 'a' && key <= 'z') || (key >= '0' && key <= '9') || key == '/' || key == '_' || key == '.' || key == ':') { dir = strlen(media_iofilename); media_iofilename[dir] = key; media_iofilename[dir+1] = '\0'; return true; } } else if (selectedoption>=0) { mediatrack_t *tr; int num=0; tr=tracks; while(tr) { if (num == selectedoption) break; tr=tr->next; num++; } if (!tr) return false; if (key == K_BACKSPACE) { dir = strlen(tr->nicename); if (dir) tr->nicename[dir-1] = '\0'; return true; } else if ((key >= 'a' && key <= 'z') || (key >= '0' && key <= '9') || key == '/' || key == '_' || key == '.' || key == ':' || key == '&' || key == '|' || key == '#' || key == '\'' || key == '\"' || key == '\\' || key == '*' || key == '@' || key == '!' || key == '(' || key == ')' || key == '%' || key == '^' || key == '?' || key == '[' || key == ']' || key == ';' || key == ':' || key == '+' || key == '-' || key == '=') { dir = strlen(tr->nicename); tr->nicename[dir] = key; tr->nicename[dir+1] = '\0'; return true; } } } return false; } void M_Menu_Media_f (void) { emenu_t *menu; if (!loadedtracknames) Media_LoadTrackNames("sound/media.m3u"); menu = M_CreateMenu(0); // MC_AddPicture(menu, 16, 4, 32, 144, "gfx/qplaque.lmp"); MC_AddCenterPicture(menu, 4, 24, "gfx/p_option.lmp"); menu->key = M_Media_Key; menu->postdraw = M_Media_Draw; } void Media_SaveTrackNames (char *listname) { mediatrack_t *tr; vfsfile_t *f = FS_OpenVFS(listname, "wb", FS_GAMEONLY); if (!f) return; VFS_PRINTF(f, "#EXTM3U\n"); for (tr = tracks; tr; tr = tr->next) { VFS_PRINTF(f, "#EXTINF:%i,%s\n%s\n", tr->length, tr->nicename, tr->filename); } VFS_CLOSE(f); } //safeprints only. void Media_LoadTrackNames (char *listname) { char *lineend; char *len; char *filename; char *trackname; mediatrack_t *newtrack; size_t fsize; char *data = COM_LoadTempFile(listname, FSLF_IGNOREPURE, &fsize); loadedtracknames=true; if (!data) return; if (!Q_strncasecmp(data, "#extm3u", 7)) { data = strchr(data, '\n')+1; for(;;) { lineend = strchr(data, '\n'); if (Q_strncasecmp(data, "#extinf:", 8)) { if (!lineend) return; Con_SafePrintf("Bad m3u file\n"); return; } len = data+8; trackname = strchr(data, ',')+1; if (!trackname) return; if (lineend > data && lineend[-1] == '\r') lineend[-1]='\0'; else lineend[0]='\0'; filename = data = lineend+1; lineend = strchr(data, '\n'); if (lineend) { if (lineend > data && lineend[-1] == '\r') lineend[-1]='\0'; else lineend[0]='\0'; data = lineend+1; } newtrack = Z_Malloc(sizeof(mediatrack_t)); Q_strncpyz(newtrack->filename, filename, sizeof(newtrack->filename)); Q_strncpyz(newtrack->nicename, trackname, sizeof(newtrack->nicename)); newtrack->length = atoi(len); newtrack->next = tracks; tracks = newtrack; numtracks++; if (!lineend) return; } } else { for(;;) { trackname = filename = data; lineend = strchr(data, '\n'); if (!lineend && !*data) break; lineend[-1]='\0'; data = lineend+1; newtrack = Z_Malloc(sizeof(mediatrack_t)); Q_strncpyz(newtrack->filename, filename, sizeof(newtrack->filename)); Q_strncpyz(newtrack->nicename, COM_SkipPath(trackname), sizeof(newtrack->nicename)); newtrack->length = 0; newtrack->next = tracks; tracks = newtrack; numtracks++; if (!lineend) break; } } } #endif #endif ///temporary residence for media handling #ifdef HAVE_API_VFW #if 0 #include #else typedef struct { DWORD fccType; DWORD fccHandler; DWORD dwFlags; DWORD dwCaps; WORD wPriority; WORD wLanguage; DWORD dwScale; DWORD dwRate; DWORD dwStart; DWORD dwLength; DWORD dwInitialFrames; DWORD dwSuggestedBufferSize; DWORD dwQuality; DWORD dwSampleSize; RECT rcFrame; DWORD dwEditCount; DWORD dwFormatChangeCount; TCHAR szName[64]; } AVISTREAMINFOA, *LPAVISTREAMINFOA; typedef struct AVISTREAM *PAVISTREAM; typedef struct AVIFILE *PAVIFILE; typedef struct GETFRAME *PGETFRAME; typedef struct { DWORD fccType; DWORD fccHandler; DWORD dwKeyFrameEvery; DWORD dwQuality; DWORD dwBytesPerSecond; DWORD dwFlags; LPVOID lpFormat; DWORD cbFormat; LPVOID lpParms; DWORD cbParms; DWORD dwInterleaveEvery; } AVICOMPRESSOPTIONS; #define streamtypeVIDEO mmioFOURCC('v', 'i', 'd', 's') #define streamtypeAUDIO mmioFOURCC('a', 'u', 'd', 's') #define AVISTREAMREAD_CONVENIENT (-1L) #define AVIIF_KEYFRAME 0x00000010L #endif ULONG (WINAPI *qAVIStreamRelease) (PAVISTREAM pavi); HRESULT (WINAPI *qAVIStreamEndStreaming) (PAVISTREAM pavi); HRESULT (WINAPI *qAVIStreamGetFrameClose) (PGETFRAME pg); HRESULT (WINAPI *qAVIStreamRead) (PAVISTREAM pavi, LONG lStart, LONG lSamples, LPVOID lpBuffer, LONG cbBuffer, LONG FAR * plBytes, LONG FAR * plSamples); LPVOID (WINAPI *qAVIStreamGetFrame) (PGETFRAME pg, LONG lPos); HRESULT (WINAPI *qAVIStreamReadFormat) (PAVISTREAM pavi, LONG lPos,LPVOID lpFormat,LONG FAR *lpcbFormat); LONG (WINAPI *qAVIStreamStart) (PAVISTREAM pavi); PGETFRAME(WINAPI*qAVIStreamGetFrameOpen) (PAVISTREAM pavi, LPBITMAPINFOHEADER lpbiWanted); HRESULT (WINAPI *qAVIStreamBeginStreaming) (PAVISTREAM pavi, LONG lStart, LONG lEnd, LONG lRate); LONG (WINAPI *qAVIStreamSampleToTime) (PAVISTREAM pavi, LONG lSample); LONG (WINAPI *qAVIStreamLength) (PAVISTREAM pavi); HRESULT (WINAPI *qAVIStreamInfoA) (PAVISTREAM pavi, LPAVISTREAMINFOA psi, LONG lSize); ULONG (WINAPI *qAVIFileRelease) (PAVIFILE pfile); HRESULT (WINAPI *qAVIFileGetStream) (PAVIFILE pfile, PAVISTREAM FAR * ppavi, DWORD fccType, LONG lParam); HRESULT (WINAPI *qAVIFileOpenA) (PAVIFILE FAR *ppfile, LPCSTR szFile, UINT uMode, LPCLSID lpHandler); void (WINAPI *qAVIFileInit) (void); HRESULT (WINAPI *qAVIStreamWrite) (PAVISTREAM pavi, LONG lStart, LONG lSamples, LPVOID lpBuffer, LONG cbBuffer, DWORD dwFlags, LONG FAR *plSampWritten, LONG FAR *plBytesWritten); HRESULT (WINAPI *qAVIStreamSetFormat) (PAVISTREAM pavi, LONG lPos,LPVOID lpFormat,LONG cbFormat); HRESULT (WINAPI *qAVIMakeCompressedStream) (PAVISTREAM FAR * ppsCompressed, PAVISTREAM ppsSource, AVICOMPRESSOPTIONS FAR * lpOptions, CLSID FAR *pclsidHandler); HRESULT (WINAPI *qAVIFileCreateStreamA) (PAVIFILE pfile, PAVISTREAM FAR *ppavi, AVISTREAMINFOA FAR * psi); static qboolean qAVIStartup(void) { static int aviinited; static dllhandle_t *avimodule; if (!aviinited) { dllfunction_t funcs[] = { {(void*)&qAVIFileInit, "AVIFileInit"}, {(void*)&qAVIStreamRelease, "AVIStreamRelease"}, {(void*)&qAVIStreamEndStreaming, "AVIStreamEndStreaming"}, {(void*)&qAVIStreamGetFrameClose, "AVIStreamGetFrameClose"}, {(void*)&qAVIStreamRead, "AVIStreamRead"}, {(void*)&qAVIStreamGetFrame, "AVIStreamGetFrame"}, {(void*)&qAVIStreamReadFormat, "AVIStreamReadFormat"}, {(void*)&qAVIStreamStart, "AVIStreamStart"}, {(void*)&qAVIStreamGetFrameOpen, "AVIStreamGetFrameOpen"}, {(void*)&qAVIStreamBeginStreaming, "AVIStreamBeginStreaming"}, {(void*)&qAVIStreamSampleToTime, "AVIStreamSampleToTime"}, {(void*)&qAVIStreamLength, "AVIStreamLength"}, {(void*)&qAVIStreamInfoA, "AVIStreamInfoA"}, {(void*)&qAVIFileRelease, "AVIFileRelease"}, {(void*)&qAVIFileGetStream, "AVIFileGetStream"}, {(void*)&qAVIFileOpenA, "AVIFileOpenA"}, {(void*)&qAVIStreamWrite, "AVIStreamWrite"}, {(void*)&qAVIStreamSetFormat, "AVIStreamSetFormat"}, {(void*)&qAVIMakeCompressedStream, "AVIMakeCompressedStream"}, {(void*)&qAVIFileCreateStreamA, "AVIFileCreateStreamA"}, {NULL,NULL} }; aviinited = true; avimodule = Sys_LoadLibrary("avifil32.dll", funcs); if (avimodule) qAVIFileInit(); } return avimodule?true:false; } #endif #ifdef HAVE_MEDIA_DECODER static char *cin_prop_buf; static size_t cin_prop_bufsize; struct cin_s { qboolean (*decodeframe)(cin_t *cin, qboolean nosound, qboolean forcevideo, double mediatime, void (QDECL *uploadtexture)(void *ctx, uploadfmt_t fmt, int width, int height, void *data, void *palette), void *ctx); void (*shutdown)(cin_t *cin); //warning: doesn't free cin_t void (*rewind)(cin_t *cin); //these are any interactivity functions you might want... void (*cursormove) (struct cin_s *cin, float posx, float posy); //pos is 0-1 void (*key) (struct cin_s *cin, int code, int unicode, int event); qboolean (*setsize) (struct cin_s *cin, int width, int height); void (*getsize) (struct cin_s *cin, int *width, int *height, float *aspect); void (*changestream) (struct cin_s *cin, const char *streamname); qboolean (*getproperty) (struct cin_s *cin, const char *key, char *buffer, size_t *buffersize); //these are the outputs (not always power of two!) cinstates_t playstate; qboolean forceframe; float filmpercentage; texid_t texture; #ifdef HAVE_API_VFW struct { AVISTREAMINFOA vidinfo; PAVISTREAM pavivideo; AVISTREAMINFOA audinfo; PAVISTREAM pavisound; PAVIFILE pavi; PGETFRAME pgf; HACMSTREAM audiodecoder; LPWAVEFORMATEX pWaveFormat; //sound stuff int soundpos; int currentframe; //previously displayed frame, to avoid excess decoding //source info float filmfps; int num_frames; } avi; #endif #ifdef PLUGINS struct { void *ctx; struct plugin_s *plug; media_decoder_funcs_t *funcs; /*fixme*/ struct cin_s *next; struct cin_s *prev; } plugin; #endif struct { qbyte *data; uploadfmt_t format; int width; int height; } image; struct { struct roq_info_s *roqfilm; // float lastmediatime; float nextframetime; } roq; struct { struct cinematics_s *cin; } q2cin; float filmstarttime; qbyte *framedata; //Z_Malloced buffer }; static menu_t videomenu; //to capture keys+draws+etc. a singleton - multiple videos will be queued instead of simultaneous. static shader_t *videoshader; ////////////////////////////////////////////////////////////////////////////////// //AVI Support (windows) #ifdef HAVE_API_VFW static void Media_WinAvi_Shutdown(struct cin_s *cin) { qAVIStreamGetFrameClose(cin->avi.pgf); qAVIStreamEndStreaming(cin->avi.pavivideo); qAVIStreamRelease(cin->avi.pavivideo); //we don't need to free the file (we freed it immediatly after getting the stream handles) } static void Media_WinAvi_Rewind (cin_t *cin) { cin->avi.soundpos = 0; cin->avi.currentframe = -1; } static qboolean Media_WinAvi_DecodeFrame (cin_t *cin, qboolean nosound, qboolean forcevideo, double mediatime, void (QDECL *uploadtexture)(void *ctx, uploadfmt_t fmt, int width, int height, void *data, void *palette), void *ctx) { LPBITMAPINFOHEADER lpbi; // Holds The Bitmap Header Information float newframe; int newframei; int wantsoundtime; extern cvar_t _snd_mixahead; newframe = ((mediatime * cin->avi.vidinfo.dwRate) / cin->avi.vidinfo.dwScale) + cin->avi.vidinfo.dwInitialFrames; newframei = newframe; if (newframei>=cin->avi.num_frames) return false; if (newframei == cin->avi.currentframe && !forcevideo) return true; if (cin->avi.currentframe < newframei-1) Con_DPrintf("Dropped %i frame(s)\n", (newframei - cin->avi.currentframe)-1); cin->avi.currentframe = newframei; cin->filmpercentage = newframe / cin->avi.num_frames; if (newframei>=cin->avi.num_frames) return false; lpbi = (LPBITMAPINFOHEADER)qAVIStreamGetFrame(cin->avi.pgf, newframei); // Grab Data From The AVI Stream if (!lpbi || lpbi->biBitCount != 24)//oops return false; uploadtexture(ctx, TF_BGR24_FLIP, lpbi->biWidth, lpbi->biHeight, (char*)lpbi+lpbi->biSize, NULL); if(nosound) wantsoundtime = 0; else wantsoundtime = (((mediatime + _snd_mixahead.value + 0.02) * cin->avi.audinfo.dwRate) / cin->avi.audinfo.dwScale) + cin->avi.audinfo.dwInitialFrames; while (cin->avi.pavisound && cin->avi.soundpos < wantsoundtime) { LONG lSize; LPBYTE pBuffer; LONG samples; /*if the audio skipped more than a second, drop it all and start at a sane time, so our raw audio playing code doesn't buffer too much*/ if (cin->avi.soundpos + (1*cin->avi.audinfo.dwRate / cin->avi.audinfo.dwScale) < wantsoundtime) { cin->avi.soundpos = wantsoundtime; break; } qAVIStreamRead(cin->avi.pavisound, cin->avi.soundpos, AVISTREAMREAD_CONVENIENT, NULL, 0, &lSize, &samples); pBuffer = cin->framedata; qAVIStreamRead(cin->avi.pavisound, cin->avi.soundpos, AVISTREAMREAD_CONVENIENT, pBuffer, lSize, NULL, &samples); cin->avi.soundpos+=samples; /*if no progress, stop!*/ if (!samples) break; if (cin->avi.audiodecoder) { ACMSTREAMHEADER strhdr; char buffer[1024*256]; memset(&strhdr, 0, sizeof(strhdr)); strhdr.cbStruct = sizeof(strhdr); strhdr.pbSrc = pBuffer; strhdr.cbSrcLength = lSize; strhdr.pbDst = buffer; strhdr.cbDstLength = sizeof(buffer); qacmStreamPrepareHeader(cin->avi.audiodecoder, &strhdr, 0); qacmStreamConvert(cin->avi.audiodecoder, &strhdr, ACM_STREAMCONVERTF_BLOCKALIGN); qacmStreamUnprepareHeader(cin->avi.audiodecoder, &strhdr, 0); S_RawAudio(-1, strhdr.pbDst, cin->avi.pWaveFormat->nSamplesPerSec, strhdr.cbDstLengthUsed/4, cin->avi.pWaveFormat->nChannels, 2, volume.value); } else S_RawAudio(-1, pBuffer, cin->avi.pWaveFormat->nSamplesPerSec, samples, cin->avi.pWaveFormat->nChannels, 2, volume.value); } return true; } static cin_t *Media_WinAvi_TryLoad(char *name) { cin_t *cin; PAVIFILE pavi; flocation_t loc; if (strchr(name, ':')) return NULL; if (!qAVIStartup()) return NULL; FS_FLocateFile(name, FSLF_IFFOUND, &loc); if (!loc.offset && *loc.rawname && !qAVIFileOpenA(&pavi, loc.rawname, OF_READ, NULL))//!AVIStreamOpenFromFile(&pavi, name, streamtypeVIDEO, 0, OF_READ, NULL)) { int filmwidth; int filmheight; cin = Z_Malloc(sizeof(cin_t)); cin->avi.pavi = pavi; if (qAVIFileGetStream(cin->avi.pavi, &cin->avi.pavivideo, streamtypeVIDEO, 0)) //retrieve video stream { qAVIFileRelease(pavi); Con_Printf("%s contains no video stream\n", name); return NULL; } if (qAVIFileGetStream(cin->avi.pavi, &cin->avi.pavisound, streamtypeAUDIO, 0)) //retrieve audio stream { Con_DPrintf("%s contains no audio stream\n", name); cin->avi.pavisound=NULL; } qAVIFileRelease(cin->avi.pavi); //play with video qAVIStreamInfoA(cin->avi.pavivideo, &cin->avi.vidinfo, sizeof(cin->avi.vidinfo)); filmwidth=cin->avi.vidinfo.rcFrame.right-cin->avi.vidinfo.rcFrame.left; // Width Is Right Side Of Frame Minus Left filmheight=cin->avi.vidinfo.rcFrame.bottom-cin->avi.vidinfo.rcFrame.top; // Height Is Bottom Of Frame Minus Top cin->framedata = BZ_Malloc(filmwidth*filmheight*4); cin->avi.num_frames=qAVIStreamLength(cin->avi.pavivideo); // The Last Frame Of The Stream cin->avi.filmfps=1000.0f*(float)cin->avi.num_frames/(float)qAVIStreamSampleToTime(cin->avi.pavivideo,cin->avi.num_frames); // Calculate Rough Milliseconds Per Frame qAVIStreamBeginStreaming(cin->avi.pavivideo, 0, cin->avi.num_frames, 100); cin->avi.pgf=qAVIStreamGetFrameOpen(cin->avi.pavivideo, NULL); if (!cin->avi.pgf) { Con_Printf("AVIStreamGetFrameOpen failed. Please install a vfw codec for '%c%c%c%c'. Try ffdshow.\n", ((unsigned char*)&cin->avi.vidinfo.fccHandler)[0], ((unsigned char*)&cin->avi.vidinfo.fccHandler)[1], ((unsigned char*)&cin->avi.vidinfo.fccHandler)[2], ((unsigned char*)&cin->avi.vidinfo.fccHandler)[3] ); } cin->avi.currentframe=-1; cin->filmstarttime = Sys_DoubleTime(); cin->avi.soundpos=0; //play with sound if (cin->avi.pavisound) { LONG lSize; LPBYTE pChunk; qAVIStreamInfoA(cin->avi.pavisound, &cin->avi.audinfo, sizeof(cin->avi.audinfo)); qAVIStreamRead(cin->avi.pavisound, 0, AVISTREAMREAD_CONVENIENT, NULL, 0, &lSize, NULL); if (!lSize) cin->avi.pWaveFormat = NULL; else { pChunk = BZ_Malloc(sizeof(qbyte)*lSize); if(qAVIStreamReadFormat(cin->avi.pavisound, qAVIStreamStart(cin->avi.pavisound), pChunk, &lSize)) { // error Con_Printf("Failure reading sound info\n"); } cin->avi.pWaveFormat = (LPWAVEFORMATEX)pChunk; } if (!cin->avi.pWaveFormat) { Con_Printf("VFW: unable to decode audio\n"); qAVIStreamRelease(cin->avi.pavisound); cin->avi.pavisound=NULL; } else if (cin->avi.pWaveFormat->wFormatTag != 1) { WAVEFORMATEX pcm_format; HACMDRIVER drv = NULL; memset (&pcm_format, 0, sizeof(pcm_format)); pcm_format.wFormatTag = WAVE_FORMAT_PCM; pcm_format.nChannels = cin->avi.pWaveFormat->nChannels; pcm_format.nSamplesPerSec = cin->avi.pWaveFormat->nSamplesPerSec; pcm_format.nBlockAlign = 4; pcm_format.nAvgBytesPerSec = pcm_format.nSamplesPerSec*4; pcm_format.wBitsPerSample = 16; pcm_format.cbSize = 0; if (!qacmStartup() || 0!=qacmStreamOpen(&cin->avi.audiodecoder, drv, cin->avi.pWaveFormat, &pcm_format, NULL, 0, 0, 0)) { Con_Printf("Failed to open audio decoder\n"); //FIXME: so that it no longer is... qAVIStreamRelease(cin->avi.pavisound); cin->avi.pavisound=NULL; } } } cin->decodeframe = Media_WinAvi_DecodeFrame; cin->rewind = Media_WinAvi_Rewind; cin->shutdown = Media_WinAvi_Shutdown; return cin; } return NULL; } #endif //AVI Support (windows) ////////////////////////////////////////////////////////////////////////////////// //Plugin Support #ifdef PLUGINS static media_decoder_funcs_t *plugindecodersfunc[8]; static struct plugin_s *plugindecodersplugin[8]; static cin_t *active_cin_plugins; qboolean Media_RegisterDecoder(struct plugin_s *plug, media_decoder_funcs_t *funcs) { int i; if (!funcs || funcs->structsize != sizeof(*funcs)) return false; for (i = 0; i < sizeof(plugindecodersfunc)/sizeof(plugindecodersfunc[0]); i++) { if (plugindecodersfunc[i] == NULL) { plugindecodersfunc[i] = funcs; plugindecodersplugin[i] = plug; return true; } } return false; } /*funcs==null closes ALL decoders from this plugin*/ qboolean Media_UnregisterDecoder(struct plugin_s *plug, media_decoder_funcs_t *funcs) { qboolean success = true; int i; static media_decoder_funcs_t deadfuncs; struct plugin_s *oldplug = currentplug; cin_t *cin, *next; for (i = 0; i < sizeof(plugindecodersfunc)/sizeof(plugindecodersfunc[0]); i++) { if (plugindecodersfunc[i] == funcs || (!funcs && plugindecodersplugin[i] == plug)) { //kill any cinematics currently using that decoder for (cin = active_cin_plugins; cin; cin = next) { next = cin->plugin.next; if (cin->plugin.plug == plug && cin->plugin.funcs == plugindecodersfunc[i]) { //we don't kill the engine's side of it, not just yet anyway. currentplug = cin->plugin.plug; if (cin->plugin.funcs->shutdown) cin->plugin.funcs->shutdown(cin->plugin.ctx); cin->plugin.funcs = &deadfuncs; cin->plugin.plug = NULL; cin->plugin.ctx = NULL; } } currentplug = oldplug; plugindecodersfunc[i] = NULL; plugindecodersplugin[i] = NULL; if (funcs) return success; } } if (!funcs) { static media_decoder_funcs_t deadfuncs; struct plugin_s *oldplug = currentplug; cin_t *cin, *next; for (cin = active_cin_plugins; cin; cin = next) { next = cin->plugin.next; if (cin->plugin.plug == plug) { //we don't kill the engine's side of it, not just yet anyway. currentplug = cin->plugin.plug; if (cin->plugin.funcs->shutdown) cin->plugin.funcs->shutdown(cin->plugin.ctx); cin->plugin.funcs = &deadfuncs; cin->plugin.plug = NULL; cin->plugin.ctx = NULL; } } currentplug = oldplug; } return success; } static qboolean Media_Plugin_DecodeFrame (cin_t *cin, qboolean nosound, qboolean forcevideo, double mediatime, void (QDECL *uploadtexture)(void *ctx, uploadfmt_t fmt, int width, int height, void *data, void *palette), void *ctx) { qboolean okay; struct plugin_s *oldplug = currentplug; if (!cin->plugin.funcs->decodeframe) return false; //plugin closed or something currentplug = cin->plugin.plug; okay = cin->plugin.funcs->decodeframe(cin->plugin.ctx, nosound, forcevideo, mediatime, uploadtexture, ctx); currentplug = oldplug; return okay; } static void Media_Plugin_Shutdown(cin_t *cin) { struct plugin_s *oldplug = currentplug; currentplug = cin->plugin.plug; if (cin->plugin.funcs->shutdown) cin->plugin.funcs->shutdown(cin->plugin.ctx); currentplug = oldplug; if (cin->plugin.prev) cin->plugin.prev->plugin.next = cin->plugin.next; else active_cin_plugins = cin->plugin.next; if (cin->plugin.next) cin->plugin.next->plugin.prev = cin->plugin.prev; } static void Media_Plugin_Rewind(cin_t *cin) { struct plugin_s *oldplug = currentplug; currentplug = cin->plugin.plug; if (cin->plugin.funcs->rewind) cin->plugin.funcs->rewind(cin->plugin.ctx); currentplug = oldplug; } void Media_Plugin_MoveCursor(cin_t *cin, float posx, float posy) { struct plugin_s *oldplug = currentplug; currentplug = cin->plugin.plug; if (cin->plugin.funcs->cursormove) cin->plugin.funcs->cursormove(cin->plugin.ctx, posx, posy); currentplug = oldplug; } void Media_Plugin_KeyPress(cin_t *cin, int code, int unicode, int event) { struct plugin_s *oldplug = currentplug; currentplug = cin->plugin.plug; if (cin->plugin.funcs->key) cin->plugin.funcs->key(cin->plugin.ctx, code, unicode, event); currentplug = oldplug; } qboolean Media_Plugin_SetSize(cin_t *cin, int width, int height) { qboolean result = false; struct plugin_s *oldplug = currentplug; currentplug = cin->plugin.plug; if (cin->plugin.funcs->setsize) result = cin->plugin.funcs->setsize(cin->plugin.ctx, width, height); currentplug = oldplug; return result; } void Media_Plugin_GetSize(cin_t *cin, int *width, int *height, float *aspect) { struct plugin_s *oldplug = currentplug; currentplug = cin->plugin.plug; if (cin->plugin.funcs->getsize) cin->plugin.funcs->getsize(cin->plugin.ctx, width, height); currentplug = oldplug; } void Media_Plugin_ChangeStream(cin_t *cin, const char *streamname) { struct plugin_s *oldplug = currentplug; currentplug = cin->plugin.plug; if (cin->plugin.funcs->changestream) cin->plugin.funcs->changestream(cin->plugin.ctx, streamname); currentplug = oldplug; } qboolean Media_Plugin_GetProperty(cin_t *cin, const char *propname, char *data, size_t *datasize) { qboolean ret = false; struct plugin_s *oldplug = currentplug; currentplug = cin->plugin.plug; if (cin->plugin.funcs->getproperty) ret = cin->plugin.funcs->getproperty(cin->plugin.ctx, propname, data, datasize); currentplug = oldplug; return ret; } cin_t *Media_Plugin_TryLoad(char *name) { cin_t *cin; int i; media_decoder_funcs_t *funcs = NULL; struct plugin_s *plug = NULL; void *ctx = NULL; struct plugin_s *oldplug = currentplug; for (i = 0; i < sizeof(plugindecodersfunc)/sizeof(plugindecodersfunc[0]); i++) { funcs = plugindecodersfunc[i]; if (funcs) { plug = plugindecodersplugin[i]; currentplug = plug; ctx = funcs->createdecoder(name); if (ctx) break; } } currentplug = oldplug; if (ctx) { cin = Z_Malloc(sizeof(cin_t)); cin->plugin.funcs = funcs; cin->plugin.plug = plug; cin->plugin.ctx = ctx; cin->plugin.next = active_cin_plugins; cin->plugin.prev = NULL; if (cin->plugin.next) cin->plugin.next->plugin.prev = cin; active_cin_plugins = cin; cin->decodeframe = Media_Plugin_DecodeFrame; cin->shutdown = Media_Plugin_Shutdown; cin->rewind = Media_Plugin_Rewind; if (funcs->cursormove) cin->cursormove = Media_Plugin_MoveCursor; if (funcs->key) cin->key = Media_Plugin_KeyPress; if (funcs->setsize) cin->setsize = Media_Plugin_SetSize; if (funcs->getsize) cin->getsize = Media_Plugin_GetSize; if (funcs->changestream) cin->changestream = Media_Plugin_ChangeStream; if (funcs->getproperty) cin->getproperty = Media_Plugin_GetProperty; return cin; } return NULL; } #endif //Plugin Support ////////////////////////////////////////////////////////////////////////////////// //Quake3 RoQ Support #ifdef Q3CLIENT #include "roq.h" static void Media_Roq_Shutdown(struct cin_s *cin) { roq_close(cin->roq.roqfilm); cin->roq.roqfilm=NULL; } static qboolean Media_Roq_DecodeFrame (cin_t *cin, qboolean nosound, qboolean forcevideo, double mediatime, void (QDECL *uploadtexture)(void *ctx, uploadfmt_t fmt, int width, int height, void *data, void *palette), void *ctx) { qboolean doupdate = forcevideo; //seeking could probably be done a bit better... //key frames or something, I dunno while (mediatime >= cin->roq.nextframetime) { switch (roq_read_frame(cin->roq.roqfilm)) //0 if end, -1 if error, 1 if success { default: //error case 0: //eof if (!doupdate) return false; break; case 1: break; //success } cin->roq.nextframetime += 1.0/30; doupdate = true; } if (doupdate) { //#define LIMIT(x) ((x)<0xFFFF)?(x)>>16:0xFF; #define LIMIT(x) ((((x) > 0xffffff) ? 0xff0000 : (((x) <= 0xffff) ? 0 : (x) & 0xff0000)) >> 16) unsigned char *pa=cin->roq.roqfilm->y[0]; unsigned char *pb=cin->roq.roqfilm->u[0]; unsigned char *pc=cin->roq.roqfilm->v[0]; int pix=0; int num_columns=(cin->roq.roqfilm->width)>>1; int num_rows=cin->roq.roqfilm->height; int y; int x; qbyte *framedata; if (cin->roq.roqfilm->num_frames) cin->filmpercentage = cin->roq.roqfilm->frame_num / cin->roq.roqfilm->num_frames; else cin->filmpercentage = 0; { framedata = cin->framedata; for(y = 0; y < num_rows; ++y) //roq playing doesn't give nice data. It's still fairly raw. { //convert it properly. for(x = 0; x < num_columns; ++x) { int r, g, b, y1, y2, u, v, t; y1 = *(pa++); y2 = *(pa++); u = pb[x] - 128; v = pc[x] - 128; y1 <<= 16; y2 <<= 16; r = 91881 * v; g = -22554 * u + -46802 * v; b = 116130 * u; t=r+y1; framedata[pix] =(unsigned char) LIMIT(t); t=g+y1; framedata[pix+1] =(unsigned char) LIMIT(t); t=b+y1; framedata[pix+2] =(unsigned char) LIMIT(t); t=r+y2; framedata[pix+4] =(unsigned char) LIMIT(t); t=g+y2; framedata[pix+5] =(unsigned char) LIMIT(t); t=b+y2; framedata[pix+6] =(unsigned char) LIMIT(t); pix+=8; } if(y & 0x01) { pb += num_columns; pc += num_columns; } } } uploadtexture(ctx, TF_RGBX32, cin->roq.roqfilm->width, cin->roq.roqfilm->height, cin->framedata, NULL); if (!nosound) { while (cin->roq.roqfilm->audio_channels && S_HaveOutput() && cin->roq.roqfilm->aud_pos < cin->roq.roqfilm->vid_pos) { if (roq_read_audio(cin->roq.roqfilm)>0) S_RawAudio(-1, cin->roq.roqfilm->audio, 22050, cin->roq.roqfilm->audio_size/cin->roq.roqfilm->audio_channels, cin->roq.roqfilm->audio_channels, 2, volume.value ); else break; } } } return true; } void Media_Roq_GetSize(struct cin_s *cin, int *width, int *height, float *aspect) { *width = cin->roq.roqfilm->width; *height = cin->roq.roqfilm->height; *aspect = (float)cin->roq.roqfilm->width/cin->roq.roqfilm->height; } static cin_t *Media_RoQ_TryLoad(char *name) { cin_t *cin; roq_info *roqfilm; if (strchr(name, ':')) return NULL; if ((roqfilm = roq_open(name))) { cin = Z_Malloc(sizeof(cin_t)); cin->decodeframe = Media_Roq_DecodeFrame; cin->shutdown = Media_Roq_Shutdown; cin->getsize = Media_Roq_GetSize; cin->roq.roqfilm = roqfilm; cin->framedata = BZ_Malloc(roqfilm->width*roqfilm->height*4); return cin; } return NULL; } #endif //Quake3 RoQ Support ////////////////////////////////////////////////////////////////////////////////// //Static Image Support #ifndef MINIMAL static void Media_Static_Shutdown(struct cin_s *cin) { BZ_Free(cin->image.data); cin->image.data = NULL; } static qboolean Media_Static_DecodeFrame (cin_t *cin, qboolean nosound, qboolean forcevideo, double mediatime, void (QDECL *uploadtexture)(void *ctx, uploadfmt_t fmt, int width, int height, void *data, void *palette), void *ctx) { if (forcevideo) uploadtexture(ctx, cin->image.format, cin->image.width, cin->image.height, cin->image.data, NULL); return true; } static cin_t *Media_Static_TryLoad(char *name) { cin_t *cin; char *dot = strrchr(name, '.'); if (dot && (!strcmp(dot, ".pcx") || !strcmp(dot, ".tga") || !strcmp(dot, ".png") || !strcmp(dot, ".jpg"))) { qbyte *staticfilmimage; int imagewidth; int imageheight; uploadfmt_t format = PTI_RGBA8; int fsize; char fullname[MAX_QPATH]; qbyte *file; Q_snprintfz(fullname, sizeof(fullname), "%s", name); fsize = FS_LoadFile(fullname, (void **)&file); if (!file) { Q_snprintfz(fullname, sizeof(fullname), "pics/%s", name); fsize = FS_LoadFile(fullname, (void **)&file); if (!file) return NULL; } staticfilmimage = ReadRawImageFile(file, fsize, &imagewidth, &imageheight, &format, false, fullname); FS_FreeFile(file); //done with the file now if (!staticfilmimage) { Con_Printf("Static cinematic format not supported.\n"); //not supported format return NULL; } cin = Z_Malloc(sizeof(cin_t)); cin->decodeframe = Media_Static_DecodeFrame; cin->shutdown = Media_Static_Shutdown; cin->image.data = staticfilmimage; cin->image.format = format; cin->image.width = imagewidth; cin->image.height = imageheight; return cin; } return NULL; } #endif //Static Image Support ////////////////////////////////////////////////////////////////////////////////// //Quake2 CIN Support #ifdef Q2CLIENT //fixme: this code really shouldn't be in this file. static void Media_Cin_Shutdown(struct cin_s *cin) { CIN_StopCinematic(cin->q2cin.cin); } static void Media_Cin_Rewind(struct cin_s *cin) { CIN_Rewind(cin->q2cin.cin); } static qboolean Media_Cin_DecodeFrame(cin_t *cin, qboolean nosound, qboolean forcevideo, double mediatime, void (QDECL *uploadtexture)(void *ctx, uploadfmt_t fmt, int width, int height, void *data, void *palette), void *ctx) { qbyte *outdata; int outwidth; int outheight; qbyte *outpalette; switch (CIN_RunCinematic(cin->q2cin.cin, mediatime, &outdata, &outwidth, &outheight, &outpalette)) { default: case 0: return false; case 1: break; case 2: if (!forcevideo) return true; break; } uploadtexture(ctx, TF_8PAL24, outwidth, outheight, outdata, outpalette); return true; } static cin_t *Media_Cin_TryLoad(char *name) { struct cinematics_s *q2cin; cin_t *cin; char *dot = strrchr(name, '.'); if (dot && (!strcmp(dot, ".cin"))) { q2cin = CIN_PlayCinematic(name); if (q2cin) { cin = Z_Malloc(sizeof(cin_t)); cin->q2cin.cin = q2cin; cin->decodeframe = Media_Cin_DecodeFrame; cin->shutdown = Media_Cin_Shutdown; cin->rewind = Media_Cin_Rewind; return cin; } } return NULL; } #endif //Quake2 CIN Support ////////////////////////////////////////////////////////////////////////////////// void Media_ShutdownCin(cin_t *cin) { if (!cin) return; if (cin->shutdown) cin->shutdown(cin); if (TEXVALID(cin->texture)) Image_UnloadTexture(cin->texture); if (cin->framedata) { BZ_Free(cin->framedata); cin->framedata = NULL; } Z_Free(cin); Z_Free(cin_prop_buf); cin_prop_buf = NULL; cin_prop_bufsize = 0; } cin_t *Media_StartCin(char *name) { cin_t *cin = NULL; if (!name || !*name) //clear only. return NULL; #ifndef MINIMAL if (!cin) cin = Media_Static_TryLoad(name); #endif #ifdef Q2CLIENT if (!cin) cin = Media_Cin_TryLoad(name); #endif #ifdef Q3CLIENT if (!cin) cin = Media_RoQ_TryLoad(name); #endif #ifdef HAVE_API_VFW if (!cin) cin = Media_WinAvi_TryLoad(name); #endif #ifdef PLUGINS if (!cin) cin = Media_Plugin_TryLoad(name); #endif if (cin) cin->filmstarttime = realtime; return cin; } static struct pendingfilms_s { struct pendingfilms_s *next; char name[1]; } *pendingfilms; static void MediaView_DrawFilm(menu_t *m) { if (videoshader) { cin_t *cin = R_ShaderGetCinematic(videoshader); if (cin && cin->playstate == CINSTATE_INVALID) Media_SetState(cin, CINSTATE_PLAY); //err... wot? must have vid_reloaded or something if (cin && cin->playstate == CINSTATE_ENDED) Media_StopFilm(false); else if (cin) { int cw, ch; float aspect; if (cin->cursormove) cin->cursormove(cin, mousecursor_x/(float)vid.width, mousecursor_y/(float)vid.height); if (cin->setsize) cin->setsize(cin, vid.pixelwidth, vid.pixelheight); //FIXME: should have a proper aspect ratio setting. RoQ files are always power of two, which makes things ugly. if (cin->getsize) cin->getsize(cin, &cw, &ch, &aspect); else { cw = 4; ch = 3; } R2D_Letterbox(0, 0, vid.fbvwidth, vid.fbvheight, videoshader, cw, ch); SCR_SetUpToDrawConsole(); if (scr_con_current) SCR_DrawConsole (false); } } else if (!videoshader) Menu_Unlink(m); } static qboolean MediaView_KeyEvent(menu_t *m, qboolean isdown, unsigned int devid, int key, int unicode) { if (isdown && key == K_ESCAPE) { Media_StopFilm(false); //skip to the next. return true; } Media_Send_KeyEvent(NULL, key, unicode, !isdown); return true; } static qboolean MediaView_MouseMove(menu_t *m, qboolean isabs, unsigned int devid, float x, float y) { cin_t *cin; cin = R_ShaderGetCinematic(videoshader); if (!cin || !cin->cursormove) return false; if (isabs) cin->cursormove(cin, x, y); return true; } static void MediaView_StopFilm(menu_t *m) { //display is going away for some reason. might as well kill them all Media_StopFilm(true); } static qboolean Media_BeginNextFilm(void) { cin_t *cin; char sname[MAX_QPATH]; struct pendingfilms_s *p; if (!pendingfilms) return false; p = pendingfilms; pendingfilms = p->next; snprintf(sname, sizeof(sname), "cinematic/%s", p->name); if (!qrenderer) { Z_Free(p); return false; } videoshader = R_RegisterCustom(sname, SUF_NONE, Shader_DefaultCinematic, p->name); cin = R_ShaderGetCinematic(videoshader); if (cin) { Media_SetState(cin, CINSTATE_PLAY); Media_Send_Reset(cin); if (cin->changestream) cin->changestream(cin, "cmd:focus"); } else { Con_Printf("Unable to play cinematic %s\n", p->name); R_UnloadShader(videoshader); videoshader = NULL; } Z_Free(p); if (videoshader) { videomenu.cursor = NULL; videomenu.release = MediaView_StopFilm; videomenu.drawmenu = MediaView_DrawFilm; videomenu.mousemove = MediaView_MouseMove; videomenu.keyevent = MediaView_KeyEvent; videomenu.isopaque = true; Menu_Push(&videomenu, false); } else Menu_Unlink(&videomenu); return !!videoshader; } qboolean Media_StopFilm(qboolean all) { if (!pendingfilms && !videoshader) return false; //nothing to do if (all) { struct pendingfilms_s *p; while(pendingfilms) { p = pendingfilms; pendingfilms = p->next; Z_Free(p); } } if (videoshader) { R_UnloadShader(videoshader); videoshader = NULL; S_RawAudio(-1, NULL, 0, 0, 0, 0, 0); } while (pendingfilms && !videoshader) { if (Media_BeginNextFilm()) break; } //for q2 cinematic-maps. //q2 sends 'nextserver' when the client's cinematics end so that the game can progress to the next map. //qc might want to make use of it too, but its probably best to just send a playfilm command at the start of the map and then just wait it out. maybe treat nextserver as an unpause request. if (!videoshader && cls.state == ca_active) { CL_SendClientCommand(true, "nextserver %i", cl.servercount); } return true; } qboolean Media_PlayFilm(char *name, qboolean enqueue) { if (!enqueue || !*name) Media_StopFilm(true); if (*name) { struct pendingfilms_s **p; for (p = &pendingfilms; *p; p = &(*p)->next) ; (*p) = Z_Malloc(sizeof(**p) + strlen(name)); strcpy((*p)->name, name); while (pendingfilms && !videoshader) { if (Media_BeginNextFilm()) break; } } if (videoshader) { #ifdef HAVE_CDPLAYER CDAudio_Stop(); #endif SCR_EndLoadingPlaque(); if (!Key_Dest_Has(kdm_console)) scr_con_current=0; return true; } else return false; } #ifndef SERVERONLY void QDECL Media_UpdateTexture(void *ctx, uploadfmt_t fmt, int width, int height, void *data, void *palette) { cin_t *cin = ctx; if (!TEXVALID(cin->texture)) TEXASSIGN(cin->texture, Image_CreateTexture("***cin***", NULL, IF_CLAMP|IF_NOMIPMAP)); Image_Upload(cin->texture, fmt, data, palette, width, height, IF_CLAMP|IF_NOMIPMAP|IF_NOGAMMA); } texid_tf Media_UpdateForShader(cin_t *cin) { float mediatime; qboolean force = false; if (!cin) //caller fucked up. return r_nulltex; if (cin->playstate == CINSTATE_FLUSHED) { cin->playstate = CINSTATE_PLAY; mediatime = 0; cin->filmstarttime = realtime; } else if (cin->playstate == CINSTATE_INVALID) { cin->playstate = CINSTATE_LOOP; mediatime = 0; cin->filmstarttime = realtime; } else if (cin->playstate == CINSTATE_ENDED) mediatime = 0; //if we reach the end of the video, we give up and just show the first frame for subsequent frames else if (cin->playstate == CINSTATE_PAUSE) mediatime = cin->filmstarttime; else mediatime = realtime - cin->filmstarttime; if (!TEXVALID(cin->texture)) { force = true; } if (!cin->decodeframe(cin, false, force, mediatime, Media_UpdateTexture, cin)) { //we got eof / error if (cin->playstate != CINSTATE_LOOP) Media_SetState(cin, CINSTATE_ENDED); if (cin->rewind) { Media_Send_Reset(cin); mediatime = 0; if (!cin->decodeframe(cin, true, force, mediatime, Media_UpdateTexture, cin)) return r_nulltex; } } return cin->texture; } #endif void Media_Send_KeyEvent(cin_t *cin, int button, int unicode, int event) { if (!cin) cin = R_ShaderGetCinematic(videoshader); if (!cin) return; if (cin->key) cin->key(cin, button, unicode, event); else if (button == K_SPACE && !event) { if (cin->playstate == CINSTATE_PAUSE) Media_SetState(cin, CINSTATE_PLAY); else Media_SetState(cin, CINSTATE_PAUSE); } else if (button == K_BACKSPACE && !event) { Media_Send_Reset(cin); } else if ((button == K_LEFTARROW || button == K_KP_LEFTARROW || button == K_GP_DPAD_LEFT) && !event) cin->filmstarttime += (cin->playstate == CINSTATE_PAUSE)?-10:10; else if ((button == K_RIGHTARROW || button == K_KP_RIGHTARROW || button == K_GP_DPAD_RIGHT) && !event) cin->filmstarttime -= (cin->playstate == CINSTATE_PAUSE)?-10:10; } void Media_Send_MouseMove(cin_t *cin, float x, float y) { if (!cin) cin = R_ShaderGetCinematic(videoshader); if (!cin || !cin->cursormove) return; cin->cursormove(cin, x, y); } void Media_Send_Resize(cin_t *cin, int x, int y) { if (!cin) cin = R_ShaderGetCinematic(videoshader); if (!cin || !cin->setsize) return; cin->setsize(cin, x, y); } void Media_Send_GetSize(cin_t *cin, int *x, int *y, float *aspect) { *x = 0; *y = 0; *aspect = 0; if (!cin) cin = R_ShaderGetCinematic(videoshader); if (!cin || !cin->getsize) return; cin->getsize(cin, x, y, aspect); } void Media_Send_Reset(cin_t *cin) { if (!cin) return; if (cin->playstate == CINSTATE_PAUSE) cin->filmstarttime = 0; //fixed on resume else cin->filmstarttime = realtime; if (cin->rewind) cin->rewind(cin); } void Media_Send_Command(cin_t *cin, const char *command) { if (!cin) cin = R_ShaderGetCinematic(videoshader); if (cin && cin->changestream) cin->changestream(cin, command); else if (cin && !strcmp(command, "cmd:rewind")) Media_Send_Reset(cin); } const char *Media_Send_GetProperty(cin_t *cin, const char *key) { if (!cin) cin = R_ShaderGetCinematic(videoshader); if (cin && cin->getproperty) { size_t needsize = 0; if (cin->getproperty(cin, key, NULL, &needsize)) { if (needsize+1 > cin_prop_bufsize) { cin_prop_buf = BZ_Realloc(cin_prop_buf, needsize+1); cin_prop_bufsize = needsize+1; } cin_prop_buf[needsize] = 0; if (cin->getproperty(cin, key, cin_prop_buf, &needsize)) return cin_prop_buf; } } return NULL; } void Media_SetState(cin_t *cin, cinstates_t newstate) { if (cin->playstate == newstate) return; if (newstate == CINSTATE_FLUSHED || newstate == CINSTATE_ENDED) cin->filmstarttime = realtime; //these technically both rewind it. else if (newstate == CINSTATE_PAUSE) cin->filmstarttime = realtime - cin->filmstarttime; else if (cin->playstate == CINSTATE_PAUSE) cin->filmstarttime = realtime - cin->filmstarttime; cin->playstate = newstate; } cinstates_t Media_GetState(cin_t *cin) { if (!cin) cin = R_ShaderGetCinematic(videoshader); if (!cin) return CINSTATE_INVALID; return cin->playstate; } void Media_PlayFilm_f (void) { int i; if (Cmd_Argc() < 2) { Con_Printf("playfilm "); } if (!strcmp(Cmd_Argv(0), "cinematic")) Media_PlayFilm(va("video/%s", Cmd_Argv(1)), false); else { Media_PlayFilm(Cmd_Argv(1), false); for (i = 2; i < Cmd_Argc(); i++) Media_PlayFilm(Cmd_Argv(i), true); } } void Media_PlayVideoWindowed_f (void) { char *videomap = Cmd_Argv(1); shader_t *s; console_t *con; if (!qrenderer) return; s = R_RegisterCustom(va("consolevid_%s", videomap), SUF_NONE, Shader_DefaultCinematic, videomap); if (!R_ShaderGetCinematic(s)) { R_UnloadShader(s); Con_Printf("Unable to load video %s\n", videomap); return; } con = Con_Create(videomap, 0); if (!con) return; con->parseflags = PFS_FORCEUTF8; con->flags = CONF_ISWINDOW; con->wnd_x = vid.width/4; con->wnd_y = vid.height/4; con->wnd_w = vid.width/2; con->wnd_h = vid.height/2; con->linebuffered = NULL; Q_strncpyz(con->backimage, "", sizeof(con->backimage)); if (con->backshader) R_UnloadShader(con->backshader); con->backshader = s; Con_SetActive(con); } #endif //HAVE_MEDIA_DECODER #ifdef HAVE_MEDIA_ENCODER soundcardinfo_t *capture_fakesounddevice; double captureframeinterval; //interval between video frames double capturelastvideotime; //time of last video frame int captureframe; fbostate_t capturefbo; int captureoldfbo; qboolean capturingfbo; texid_t capturetexture; qboolean captureframeforce; #if defined(GLQUAKE) && !defined(GLESONLY) #define GLQUAKE_PBOS #endif struct { #ifdef GLQUAKE_PBOS int pbo_handle; #endif enum uploadfmt format; int stride; int width; int height; } offscreen_queue[4]; //ringbuffer of offscreen_captureframe...captureframe int offscreen_captureframe; enum uploadfmt offscreen_format; #define CAPTURECODECDESC_DEFAULT "tga" #ifdef HAVE_API_VFW #define CAPTUREDRIVERDESC_AVI "avi: capture directly to avi (capturecodec should be a fourcc value).\n" #define CAPTURECODECDESC_AVI "With (win)avi, this should be a fourcc code like divx or xvid, may be blank for raw video.\n" #else #define CAPTUREDRIVERDESC_AVI #define CAPTURECODECDESC_AVI #endif qboolean capturepaused; extern cvar_t vid_conautoscale; cvar_t capturerate = CVARD("capturerate", "30", "The framerate of the video to capture"); cvar_t capturewidth = CVARD("capturedemowidth", "0", "When using capturedemo, this specifies the width of the FBO image used. Can be larger than your physical monitor."); cvar_t captureheight = CVARD("capturedemoheight", "0", "When using capturedemo, this specifies the height of the FBO image used."); cvar_t capturedriver = CVARD("capturedriver", "", "The driver to use to capture the demo. Use the capture command with no arguments for a list of drivers."); cvar_t capturecodec = CVARD("capturecodec", CAPTURECODECDESC_DEFAULT, "the compression/encoding codec to use.\n"CAPTURECODECDESC_AVI"With raw capturing, this should be one of tga,png,jpg,pcx (ie: screenshot extensions)."); cvar_t capturesound = CVARD("capturesound", "1", "Enables the capturing of game voice. If not using capturedemo, this can be combined with cl_voip_test to capture your own voice."); cvar_t capturesoundchannels = CVAR("capturesoundchannels", "2"); cvar_t capturesoundbits = CVAR("capturesoundbits", "16"); cvar_t capturemessage = CVAR("capturemessage", ""); cvar_t capturethrottlesize = CVARD("capturethrottlesize", "0", "If set, capturing will slow down significantly if there is less disk space available than this cvar (in mb). This is useful when recording a stream to (ram)disk while another program is simultaneously consuming frames."); qboolean recordingdemo; media_encoder_funcs_t *currentcapture_funcs; void *currentcapture_ctx; #if 1 /*screenshot capture*/ struct capture_raw_ctx { int frames; enum fs_relative fsroot; char videonameprefix[MAX_QPATH]; char videonameextension[16]; vfsfile_t *audio; }; qboolean FS_FixPath(char *path, size_t pathsize); static void *QDECL capture_raw_begin (char *streamname, int videorate, int width, int height, int *sndkhz, int *sndchannels, int *sndbits) { struct capture_raw_ctx *ctx = Z_Malloc(sizeof(*ctx)); if (!strcmp(capturecodec.string, "png") || !strcmp(capturecodec.string, "jpeg") || !strcmp(capturecodec.string, "jpg") || !strcmp(capturecodec.string, "bmp") || !strcmp(capturecodec.string, "pcx") || !strcmp(capturecodec.string, "tga")) Q_strncpyz(ctx->videonameextension, capturecodec.string, sizeof(ctx->videonameextension)); else Q_strncpyz(ctx->videonameextension, "tga", sizeof(ctx->videonameextension)); if (!FS_NativePath(va("%s", streamname), FS_GAMEONLY, ctx->videonameprefix, sizeof(ctx->videonameprefix))) { Z_Free(ctx); return NULL; } if (!FS_FixPath(ctx->videonameprefix, sizeof(ctx->videonameprefix))) { Z_Free(ctx); return NULL; } ctx->fsroot = FS_SYSTEM; ctx->audio = NULL; if (*sndkhz) { char filename[MAX_OSPATH]; if (*sndbits < 8) *sndbits = 8; if (*sndbits != 8) *sndbits = 16; if (*sndchannels > 6) *sndchannels = 6; if (*sndchannels < 1) *sndchannels = 1; Q_snprintfz(filename, sizeof(filename), "%saudio_%ichan_%ikhz_%ib.raw", ctx->videonameprefix, *sndchannels, *sndkhz/1000, *sndbits); FS_CreatePath(filename, ctx->fsroot); ctx->audio = FS_OpenVFS(filename, "wb", ctx->fsroot); } if (!ctx->audio) { *sndkhz = 0; *sndchannels = 0; *sndbits = 0; } return ctx; } static void QDECL capture_raw_video (void *vctx, int frame, void *data, int stride, int width, int height, enum uploadfmt fmt) { struct capture_raw_ctx *ctx = vctx; char filename[MAX_OSPATH]; ctx->frames = frame+1; Q_snprintfz(filename, sizeof(filename), "%s%8.8i.%s", ctx->videonameprefix, frame, ctx->videonameextension); if (!SCR_ScreenShot(filename, ctx->fsroot, &data, 1, stride, width, height, fmt, true)) { Sys_Sleep(1); if (!SCR_ScreenShot(filename, ctx->fsroot, &data, 1, stride, width, height, fmt, true)) Con_DPrintf("Error writing frame %s\n", filename); } if (capturethrottlesize.value) { char base[MAX_QPATH]; Q_strncpyz(base, ctx->videonameprefix, sizeof(base)); if (FS_NativePath(base, ctx->fsroot, filename, sizeof(filename))) { quint64_t diskfree = 0; if (Sys_GetFreeDiskSpace(filename, &diskfree)) { Con_DLPrintf(2, "Free Space: %"PRIu64", threshhold %"PRIu64"\n", diskfree, (quint64_t)(1024*1024*capturethrottlesize.value)); if (diskfree < (quint64_t)(1024*1024*capturethrottlesize.value)) Sys_Sleep(1); //throttle } else { Con_Printf("%s: unable to query free disk space. Disabling\n", capturethrottlesize.name); capturethrottlesize.ival = capturethrottlesize.value = 0; } } } } static void QDECL capture_raw_audio (void *vctx, void *data, int bytes) { struct capture_raw_ctx *ctx = vctx; if (ctx->audio) VFS_WRITE(ctx->audio, data, bytes); } static void QDECL capture_raw_end (void *vctx) { struct capture_raw_ctx *ctx = vctx; if (ctx->audio) VFS_CLOSE(ctx->audio); Con_Printf("%d video frames captured\n", ctx->frames); Z_Free(ctx); } static media_encoder_funcs_t capture_raw = { sizeof(media_encoder_funcs_t), "raw", "Saves the video as a series of image files within the named sub directory, one for each frame. Audio is recorded as raw pcm.", NULL, capture_raw_begin, capture_raw_video, capture_raw_audio, capture_raw_end }; #endif #if defined(HAVE_API_VFW) /*screenshot capture*/ struct capture_avi_ctx { PAVIFILE file; #define avi_video_stream(ctx) (ctx->codec_fourcc?ctx->compressed_video_stream:ctx->uncompressed_video_stream) PAVISTREAM uncompressed_video_stream; PAVISTREAM compressed_video_stream; PAVISTREAM uncompressed_audio_stream; WAVEFORMATEX wave_format; unsigned long codec_fourcc; int audio_frame_counter; }; static void QDECL capture_avi_end(void *vctx) { struct capture_avi_ctx *ctx = vctx; if (ctx->uncompressed_video_stream) qAVIStreamRelease(ctx->uncompressed_video_stream); if (ctx->compressed_video_stream) qAVIStreamRelease(ctx->compressed_video_stream); if (ctx->uncompressed_audio_stream) qAVIStreamRelease(ctx->uncompressed_audio_stream); if (ctx->file) qAVIFileRelease(ctx->file); Z_Free(ctx); } static void *QDECL capture_avi_begin (char *streamname, int videorate, int width, int height, int *sndkhz, int *sndchannels, int *sndbits) { struct capture_avi_ctx *ctx = Z_Malloc(sizeof(*ctx)); HRESULT hr; BITMAPINFOHEADER bitmap_info_header; AVISTREAMINFOA stream_header; FILE *f; char aviname[256]; char nativepath[256]; char *fourcc = capturecodec.string; if (strlen(fourcc) == 4) ctx->codec_fourcc = mmioFOURCC(*(fourcc+0), *(fourcc+1), *(fourcc+2), *(fourcc+3)); else ctx->codec_fourcc = 0; if (!qAVIStartup()) { Con_Printf("vfw support not available.\n"); capture_avi_end(ctx); return NULL; } /*convert to foo.avi*/ COM_StripExtension(streamname, aviname, sizeof(aviname)); COM_DefaultExtension (aviname, ".avi", sizeof(aviname)); /*find the system location of that*/ FS_NativePath(aviname, FS_GAMEONLY, nativepath, sizeof(nativepath)); //wipe it. f = fopen(nativepath, "rb"); if (f) { fclose(f); unlink(nativepath); } hr = qAVIFileOpenA(&ctx->file, nativepath, OF_WRITE | OF_CREATE, NULL); if (FAILED(hr)) { Con_Printf("Failed to open %s\n", nativepath); capture_avi_end(ctx); return NULL; } memset(&bitmap_info_header, 0, sizeof(BITMAPINFOHEADER)); bitmap_info_header.biSize = 40; bitmap_info_header.biWidth = width; bitmap_info_header.biHeight = height; bitmap_info_header.biPlanes = 1; bitmap_info_header.biBitCount = 24; bitmap_info_header.biCompression = BI_RGB; bitmap_info_header.biSizeImage = width*height * 3; memset(&stream_header, 0, sizeof(stream_header)); stream_header.fccType = streamtypeVIDEO; stream_header.fccHandler = ctx->codec_fourcc; stream_header.dwScale = 100; stream_header.dwRate = (unsigned long)(0.5 + 100.0/captureframeinterval); SetRect(&stream_header.rcFrame, 0, 0, width, height); hr = qAVIFileCreateStreamA(ctx->file, &ctx->uncompressed_video_stream, &stream_header); if (FAILED(hr)) { Con_Printf("Couldn't initialise the stream, check codec\n"); capture_avi_end(ctx); return NULL; } if (ctx->codec_fourcc) { AVICOMPRESSOPTIONS opts; memset(&opts, 0, sizeof(opts)); opts.fccType = stream_header.fccType; opts.fccHandler = ctx->codec_fourcc; // Make the stream according to compression hr = qAVIMakeCompressedStream(&ctx->compressed_video_stream, ctx->uncompressed_video_stream, &opts, NULL); if (FAILED(hr)) { Con_Printf("AVIMakeCompressedStream failed. check video codec.\n"); capture_avi_end(ctx); return NULL; } } hr = qAVIStreamSetFormat(avi_video_stream(ctx), 0, &bitmap_info_header, sizeof(BITMAPINFOHEADER)); if (FAILED(hr)) { Con_Printf("AVIStreamSetFormat failed\n"); capture_avi_end(ctx); return NULL; } if (*sndbits != 8 && *sndbits != 16) *sndbits = 8; if (*sndchannels < 1 && *sndchannels > 6) *sndchannels = 1; if (*sndkhz) { memset(&ctx->wave_format, 0, sizeof(WAVEFORMATEX)); ctx->wave_format.wFormatTag = WAVE_FORMAT_PCM; ctx->wave_format.nChannels = *sndchannels; ctx->wave_format.nSamplesPerSec = *sndkhz; ctx->wave_format.wBitsPerSample = *sndbits; ctx->wave_format.nBlockAlign = ctx->wave_format.wBitsPerSample/8 * ctx->wave_format.nChannels; ctx->wave_format.nAvgBytesPerSec = ctx->wave_format.nSamplesPerSec * ctx->wave_format.nBlockAlign; ctx->wave_format.cbSize = 0; memset(&stream_header, 0, sizeof(stream_header)); stream_header.fccType = streamtypeAUDIO; stream_header.dwScale = ctx->wave_format.nBlockAlign; stream_header.dwRate = stream_header.dwScale * (unsigned long)ctx->wave_format.nSamplesPerSec; stream_header.dwSampleSize = ctx->wave_format.nBlockAlign; //FIXME: be prepared to capture audio to mp3. hr = qAVIFileCreateStreamA(ctx->file, &ctx->uncompressed_audio_stream, &stream_header); if (FAILED(hr)) { capture_avi_end(ctx); return NULL; } hr = qAVIStreamSetFormat(ctx->uncompressed_audio_stream, 0, &ctx->wave_format, sizeof(WAVEFORMATEX)); if (FAILED(hr)) { capture_avi_end(ctx); return NULL; } } return ctx; } static void QDECL capture_avi_video(void *vctx, int frame, void *vdata, int stride, int width, int height, enum uploadfmt fmt) { //vfw api is bottom up. struct capture_avi_ctx *ctx = vctx; qbyte *data, *in, *out; int x, y; if (stride < 0) //if stride is negative, then its bottom-up, but the data pointer is at the start of the buffer (rather than 'first row') vdata = (char*)vdata - stride*(height-1); //we need to output a packed bottom-up bgr image. //switch the input from logical-top-down to bottom-up (regardless of the physical ordering of its rows) in = (qbyte*)vdata + stride*(height-1); stride = -stride; if (fmt == TF_BGR24 && stride == width*-3) { //no work needed! data = in; } else { int ipx = (fmt == TF_BGR24||fmt == TF_RGB24)?3:4; data = out = Hunk_TempAlloc(width*height*3); if (fmt == TF_RGB24 || fmt == TF_RGBX32 || fmt == TF_RGBA32) { //byteswap + strip alpha for (y = height; y --> 0; out += width*3, in += stride) { for (x = 0; x < width; x++) { out[x*3+0] = in[x*ipx+2]; out[x*3+1] = in[x*ipx+1]; out[x*3+2] = in[x*ipx+0]; } } } else if (fmt == TF_BGR24 || fmt == TF_BGRX32 || fmt == TF_BGRA32) { //just strip alpha (or just flip) for (y = height; y --> 0; out += width*3, in += stride) { for (x = 0; x < width; x++) { out[x*3+0] = in[x*ipx+0]; out[x*3+1] = in[x*ipx+1]; out[x*3+2] = in[x*ipx+2]; } } } else { //probably spammy, but oh well Con_Printf("capture_avi_video: Unsupported image format\n"); return; } } //FIXME: if we're allocating memory anyway, can we not push this to a thread? //write it if (FAILED(qAVIStreamWrite(avi_video_stream(ctx), frame, 1, data, width*height * 3, ((frame%15) == 0)?AVIIF_KEYFRAME:0, NULL, NULL))) Con_DPrintf("Recoring error\n"); } static void QDECL capture_avi_audio(void *vctx, void *data, int bytes) { struct capture_avi_ctx *ctx = vctx; if (ctx->uncompressed_audio_stream) qAVIStreamWrite(ctx->uncompressed_audio_stream, ctx->audio_frame_counter++, 1, data, bytes, AVIIF_KEYFRAME, NULL, NULL); } static media_encoder_funcs_t capture_avi = { sizeof(media_encoder_funcs_t), "avi", "Uses Windows' VideoForWindows API to record avi files. Codecs are specified with a fourcc, for instance 'xvid' or 'x264', depending on which ones you have installed.", ".avi", capture_avi_begin, capture_avi_video, capture_avi_audio, capture_avi_end }; #endif #ifdef _DEBUG struct capture_null_context { float starttime; int frames; }; static void QDECL capture_null_end(void *vctx) { struct capture_null_context *ctx = vctx; float duration = Sys_DoubleTime() - ctx->starttime; Con_Printf("%d video frames ignored, %g secs, %gfps\n", ctx->frames, duration, ctx->frames/duration); Z_Free(ctx); } static void *QDECL capture_null_begin (char *streamname, int videorate, int width, int height, int *sndkhz, int *sndchannels, int *sndbits) { struct capture_null_context *ctx = Z_Malloc(sizeof(*ctx)); *sndkhz = 11025; *sndchannels = 2; *sndbits = 32; //floats! ctx->starttime = Sys_DoubleTime(); return ctx; } static void QDECL capture_null_video(void *vctx, int frame, void *vdata, int stride, int width, int height, enum uploadfmt fmt) { struct capture_null_context *ctx = vctx; ctx->frames = frame+1; } static void QDECL capture_null_audio(void *vctx, void *data, int bytes) { } static media_encoder_funcs_t capture_null = { sizeof(media_encoder_funcs_t), "null", "An encoder that doesn't actually encode or write anything, for debugging.", ".null", capture_null_begin, capture_null_video, capture_null_audio, capture_null_end }; #endif static media_encoder_funcs_t *pluginencodersfunc[8]; static struct plugin_s *pluginencodersplugin[8]; qboolean Media_RegisterEncoder(struct plugin_s *plug, media_encoder_funcs_t *funcs) { int i; if (!funcs || funcs->structsize != sizeof(*funcs)) return false; for (i = 0; i < sizeof(pluginencodersfunc)/sizeof(pluginencodersfunc[0]); i++) { if (pluginencodersfunc[i] == NULL) { pluginencodersfunc[i] = funcs; pluginencodersplugin[i] = plug; return true; } } return false; } void Media_StopRecordFilm_f(void); /*funcs==null closes ALL decoders from this plugin*/ qboolean Media_UnregisterEncoder(struct plugin_s *plug, media_encoder_funcs_t *funcs) { qboolean success = false; int i; for (i = 0; i < sizeof(pluginencodersfunc)/sizeof(pluginencodersfunc[0]); i++) { if (pluginencodersplugin[i]) if (pluginencodersfunc[i] == funcs || (!funcs && pluginencodersplugin[i] == plug)) { if (currentcapture_funcs == pluginencodersfunc[i]) Media_StopRecordFilm_f(); success = true; pluginencodersfunc[i] = NULL; pluginencodersplugin[i] = NULL; if (funcs) return success; } } return success; } //returns 0 if not capturing. 1 if capturing live. 2 if capturing a demo (where frame timings are forced). int Media_Capturing (void) { if (!currentcapture_funcs) return 0; return captureframeforce?2:1; } void Media_CapturePause_f (void) { capturepaused = !capturepaused; } qboolean Media_PausedDemo (qboolean fortiming) { //if fortiming is set, then timing might need to advance if we still need to parse the demo to get the first valid data out of it. if (capturepaused) return true; //capturedemo doesn't record any frames when the console is visible //but that's okay, as we don't load any demo frames either. if ((cls.demoplayback && Media_Capturing())) if (Key_Dest_Has(~kdm_game) || scr_con_current > 0 || (!fortiming&&!cl.validsequence)) return true; return false; } static qboolean Media_ForceTimeInterval(void) { return (cls.demoplayback && Media_Capturing() && captureframeinterval>0 && !Media_PausedDemo(false)); } double Media_TweekCaptureFrameTime(double oldtime, double time) { if (Media_ForceTimeInterval()) { captureframeforce = true; //if we're forcing time intervals, then we use fixed time increments and generate a new video frame for every single frame. return capturelastvideotime; } return oldtime + time; } #ifdef VKQUAKE static void Media_CapturedFrame (void *data, qintptr_t bytestride, size_t width, size_t height, enum uploadfmt fmt) { if (currentcapture_funcs) currentcapture_funcs->capture_video(currentcapture_ctx, offscreen_captureframe, data, bytestride, width, height, fmt); offscreen_captureframe++; } #endif void Media_RecordFrame (void) { char *buffer; int bytestride, truewidth, trueheight; enum uploadfmt fmt; if (!currentcapture_funcs) return; /* if (*capturecutoff.string && captureframe * captureframeinterval > capturecutoff.value*60) { currentcapture_funcs->capture_end(currentcapture_ctx); currentcapture_ctx = currentcapture_funcs->capture_begin(Cmd_Argv(1), capturerate.value, vid.pixelwidth, vid.pixelheight, &sndkhz, &sndchannels, &sndbits); if (!currentcapture_ctx) { currentcapture_funcs = NULL; return; } captureframe = 0; } */ if (Media_PausedDemo(false)) { int y = vid.height -32-16; if (y < scr_con_current) y = scr_con_current; if (y > vid.height-8) y = vid.height-8; #ifdef GLQUAKE if (capturingfbo && qrenderer == QR_OPENGL) { shader_t *pic; GLBE_FBO_Pop(captureoldfbo); vid.framebuffer = NULL; GL_Set2D(false); pic = R_RegisterShader("capturdemofeedback", SUF_NONE, "{\n" "program default2d\n" "{\n" "map $diffuse\n" "}\n" "}\n"); pic->defaulttextures->base = capturetexture; //pulse green slightly, so its a bit more obvious R2D_ImageColours(1, 1+0.2*sin(realtime), 1, 1); R2D_Image(0, 0, vid.width, vid.height, 0, 1, 1, 0, pic); R2D_ImageColours(1, 1, 1, 1); Draw_FunString(0, 0, S_COLOR_RED"PAUSED"); if (R2D_Flush) R2D_Flush(); captureoldfbo = GLBE_FBO_Push(&capturefbo); vid.framebuffer = capturetexture; GL_Set2D(false); } else #endif Draw_FunString((strlen(capturemessage.string)+1)*8, y, S_COLOR_RED "PAUSED"); if (captureframeforce) capturelastvideotime += captureframeinterval; return; } //don't capture frames while we're loading. if (cl.sendprespawn || (cls.state < ca_active && COM_HasWork())) { capturelastvideotime += captureframeinterval; return; } //overlay this on the screen, so it appears in the film if (*capturemessage.string) { int y = vid.height -32-16; if (y < scr_con_current) y = scr_con_current; if (y > vid.height-8) y = vid.height-8; Draw_FunString(0, y, capturemessage.string); } //time for annother frame? if (!captureframeforce) { if (capturelastvideotime > realtime+1) capturelastvideotime = realtime; //urm, wrapped?.. if (capturelastvideotime > realtime) goto skipframe; } if (cls.findtrack) { capturelastvideotime += captureframeinterval; return; //skip until we're tracking the right player. } if (R2D_Flush) R2D_Flush(); #ifdef GLQUAKE_PBOS if (offscreen_format != TF_INVALID && qrenderer == QR_OPENGL) { int frame; //encode the frame if we're about to stomp on it while (offscreen_captureframe + countof(offscreen_queue) <= captureframe) { frame = offscreen_captureframe%countof(offscreen_queue); qglBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, offscreen_queue[frame].pbo_handle); buffer = qglMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_ONLY_ARB); if (buffer) { //FIXME: thread these (with audio too, to avoid races) currentcapture_funcs->capture_video(currentcapture_ctx, offscreen_captureframe, buffer, offscreen_queue[frame].stride, offscreen_queue[frame].width, offscreen_queue[frame].height, offscreen_queue[frame].format); qglUnmapBufferARB(GL_PIXEL_PACK_BUFFER_ARB); } offscreen_captureframe++; } frame = captureframe%countof(offscreen_queue); //if we have no pbo yet, create one. if (!offscreen_queue[frame].pbo_handle || offscreen_queue[frame].width != vid.fbpwidth || offscreen_queue[frame].height != vid.fbpheight) { int imagesize = 0; if (offscreen_queue[frame].pbo_handle) qglDeleteBuffersARB(1, &offscreen_queue[frame].pbo_handle); offscreen_queue[frame].format = offscreen_format; offscreen_queue[frame].width = vid.fbpwidth; offscreen_queue[frame].height = vid.fbpheight; switch(offscreen_queue[frame].format) { case TF_BGR24: case TF_RGB24: imagesize = 3; break; case TF_BGRA32: case TF_RGBA32: imagesize = 4; break; default: break; } offscreen_queue[frame].stride = vid.fbpwidth*-imagesize;//gl is upside down imagesize *= offscreen_queue[frame].width * offscreen_queue[frame].height; qglGenBuffersARB(1, &offscreen_queue[frame].pbo_handle); qglBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, offscreen_queue[frame].pbo_handle); qglBufferDataARB(GL_PIXEL_PACK_BUFFER_ARB, imagesize, NULL, GL_STREAM_READ_ARB); } //get the gpu to copy the texture into the pbo. the driver should pipeline this read until we actually map the pbo, hopefully avoiding stalls qglBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, offscreen_queue[frame].pbo_handle); switch(offscreen_queue[frame].format) { case TF_BGR24: qglReadPixels(0, 0, offscreen_queue[frame].width, offscreen_queue[frame].height, GL_BGR_EXT, GL_UNSIGNED_BYTE, 0); break; case TF_BGRA32: qglReadPixels(0, 0, offscreen_queue[frame].width, offscreen_queue[frame].height, GL_BGRA_EXT, GL_UNSIGNED_INT_8_8_8_8_REV, 0); break; case TF_RGB24: qglReadPixels(0, 0, offscreen_queue[frame].width, offscreen_queue[frame].height, GL_RGB, GL_UNSIGNED_BYTE, 0); break; case TF_RGBA32: qglReadPixels(0, 0, offscreen_queue[frame].width, offscreen_queue[frame].height, GL_RGBA, GL_UNSIGNED_BYTE, 0); break; default: break; } qglBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, 0); } else #endif #ifdef VKQUAKE if (qrenderer == QR_VULKAN) VKVID_QueueGetRGBData(Media_CapturedFrame); else #endif { offscreen_captureframe = captureframe+1; //submit the current video frame. audio will be mixed to match. buffer = VID_GetRGBInfo(&bytestride, &truewidth, &trueheight, &fmt); if (buffer) { qbyte *firstrow = (bytestride<0)?buffer - bytestride*(trueheight-1):buffer; currentcapture_funcs->capture_video(currentcapture_ctx, captureframe, firstrow, bytestride, truewidth, trueheight, fmt); BZ_Free (buffer); } else { Con_DPrintf("Unable to grab video image\n"); currentcapture_funcs->capture_video(currentcapture_ctx, captureframe, NULL, 0, 0, 0, TF_INVALID); } } captureframe++; capturelastvideotime += captureframeinterval; captureframeforce = false; //this is drawn to the screen and not the film skipframe: { int y = vid.height -32-16; if (y < scr_con_current) y = scr_con_current; if (y > vid.height-8) y = vid.height-8; #ifdef GLQUAKE if (capturingfbo && qrenderer == QR_OPENGL) { shader_t *pic; GLBE_FBO_Pop(captureoldfbo); vid.framebuffer = NULL; GL_Set2D(false); pic = R_RegisterShader("capturdemofeedback", SUF_NONE, "{\n" "program default2d\n" "{\n" "map $diffuse\n" "}\n" "}\n"); pic->defaulttextures->base = capturetexture; //pulse green slightly, so its a bit more obvious R2D_ImageColours(1, 1+0.2*sin(realtime), 1, 1); R2D_Image(0, 0, vid.width, vid.height, 0, 1, 1, 0, pic); R2D_ImageColours(1, 1, 1, 1); Draw_FunString(0, 0, va(S_COLOR_RED"RECORDING OFFSCREEN %g", capturelastvideotime)); if (R2D_Flush) R2D_Flush(); captureoldfbo = GLBE_FBO_Push(&capturefbo); vid.framebuffer = capturetexture; GL_Set2D(false); } else #endif Draw_FunString((strlen(capturemessage.string)+1)*8, y, S_COLOR_RED"RECORDING"); } } static void *MSD_Lock (soundcardinfo_t *sc, unsigned int *sampidx) { return sc->sn.buffer; } static void MSD_Unlock (soundcardinfo_t *sc, void *buffer) { } static unsigned int MSD_GetDMAPos(soundcardinfo_t *sc) { int s; s = captureframe*(sc->sn.speed*captureframeinterval); // s >>= sc->sn.samplebytes - 1; s *= sc->sn.numchannels; return s; } static void MSD_Submit(soundcardinfo_t *sc, int start, int end) { //Fixme: support outputting to wav //http://www.borg.com/~jglatt/tech/wave.htm int lastpos; int newpos; int framestosubmit; int offset; int bytesperframe; int minframes = 576; //mp3 requires this (which we use for vfw) int maxframes = 576; newpos = sc->paintedtime; while(1) { lastpos = sc->snd_completed; framestosubmit = newpos - lastpos; if (framestosubmit < minframes) return; if (framestosubmit > maxframes) framestosubmit = maxframes; bytesperframe = sc->sn.numchannels*sc->sn.samplebytes; offset = (lastpos % (sc->sn.samples/sc->sn.numchannels)); //we could just use a buffer size equal to the number of samples in each frame //but that isn't as robust when it comes to floating point imprecisions //namly: that it would loose a sample each frame with most framerates. if ((sc->snd_completed % (sc->sn.samples/sc->sn.numchannels)) < offset) { framestosubmit = ((sc->sn.samples/sc->sn.numchannels)) - offset; offset = 0; } currentcapture_funcs->capture_audio(currentcapture_ctx, sc->sn.buffer+offset*bytesperframe, framestosubmit*bytesperframe); sc->snd_completed += framestosubmit; } } static void MSD_Shutdown (soundcardinfo_t *sc) { Z_Free(sc->sn.buffer); capture_fakesounddevice = NULL; } void Media_InitFakeSoundDevice (int speed, int channels, int samplebits) { soundcardinfo_t *sc; if (capture_fakesounddevice) return; //when we're recording a demo, we'll be timedemoing it as it were. //this means that the actual sound devices and the fake device will be going at different rates //which really confuses any stream decoding, like music. //so just kill all actual sound devices. if (recordingdemo) { soundcardinfo_t *next; for (sc = sndcardinfo; sc; sc=next) { next = sc->next; sc->Shutdown(sc); Z_Free(sc); sndcardinfo = next; } } if (!snd_speed) snd_speed = speed; sc = Z_Malloc(sizeof(soundcardinfo_t)); sc->seat = -1; sc->snd_sent = 0; sc->snd_completed = 0; sc->sn.samples = speed*0.5; sc->sn.speed = speed; switch(samplebits) { case 32: sc->sn.samplebytes = 4; sc->sn.sampleformat = QSF_F32; break; default: case 16: sc->sn.samplebytes = 2; sc->sn.sampleformat = QSF_S16; break; case 8: sc->sn.samplebytes = 1; sc->sn.sampleformat = QSF_U8; break; } sc->sn.samplepos = 0; sc->sn.numchannels = channels; sc->inactive_sound = true; sc->sn.samples -= sc->sn.samples%1152; //truncate slightly to keep vfw happy. sc->samplequeue = -1; sc->sn.buffer = (unsigned char *) BZ_Malloc(sc->sn.samples*sc->sn.numchannels*sc->sn.samplebytes); Z_ReallocElements((void**)&sc->channel, &sc->max_chans, MAX_DYNAMIC_CHANNELS+NUM_AMBIENTS+NUM_MUSICS, sizeof(*sc->channel)); sc->Lock = MSD_Lock; sc->Unlock = MSD_Unlock; sc->Submit = MSD_Submit; sc->Shutdown = MSD_Shutdown; sc->GetDMAPos = MSD_GetDMAPos; sc->next = sndcardinfo; sndcardinfo = sc; capture_fakesounddevice = sc; S_DefaultSpeakerConfiguration(sc); } //stops capturing and destroys everything. static void Media_FlushCapture(void) { #ifdef GLQUAKE_PBOS if (offscreen_format && qrenderer == QR_OPENGL) { int frame; qbyte *buffer; while (offscreen_captureframe < captureframe) { frame = offscreen_captureframe%countof(offscreen_queue); qglBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, offscreen_queue[frame].pbo_handle); buffer = qglMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_ONLY_ARB); if (buffer) { currentcapture_funcs->capture_video(currentcapture_ctx, offscreen_captureframe, buffer, offscreen_queue[frame].stride, offscreen_queue[frame].width, offscreen_queue[frame].height, offscreen_queue[frame].format); qglUnmapBufferARB(GL_PIXEL_PACK_BUFFER_ARB); } offscreen_captureframe++; } for (frame = 0; frame < countof(offscreen_queue); frame++) { if (offscreen_queue[frame].pbo_handle) qglDeleteBuffersARB(1, &offscreen_queue[frame].pbo_handle); memset(&offscreen_queue[frame], 0, sizeof(offscreen_queue[frame])); } } #endif #if 0//def VKQUAKE if (pbo_format && qrenderer == QR_VULKAN) { int frame; while (offscreen_captureframe < captureframe) { frame = offscreen_captureframe%countof(offscreen_queue); qbyte *buffer; qglBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, offscreen_queue[frame].pbo_handle); buffer = qglMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_ONLY_ARB); if (buffer) { currentcapture_funcs->capture_video(currentcapture_ctx, buffer, offscreen_captureframe, offscreen_queue[frame].width, offscreen_queue[frame].height, offscreen_queue[frame].format); qglUnmapBufferARB(GL_PIXEL_PACK_BUFFER_ARB); } offscreen_captureframe++; } for (frame = 0; frame < countof(offscreen_queue); frame++) { if (offscreen_queue[frame].pbo_handle) qglDeleteBuffersARB(1, &offscreen_queue[frame].pbo_handle); memset(&offscreen_queue[frame], 0, sizeof(offscreen_queue[frame])); } } #endif #ifdef GLQUAKE if (capturingfbo && qrenderer == QR_OPENGL) { GLBE_FBO_Pop(captureoldfbo); GLBE_FBO_Destroy(&capturefbo); vid.framebuffer = NULL; } #endif } void Media_StopRecordFilm_f (void) { Media_FlushCapture(); capturingfbo = false; if (capture_fakesounddevice) S_ShutdownCard(capture_fakesounddevice); capture_fakesounddevice = NULL; if (recordingdemo) //start up their regular audio devices again. S_DoRestart(false); recordingdemo=false; if (currentcapture_funcs) currentcapture_funcs->capture_end(currentcapture_ctx); currentcapture_ctx = NULL; currentcapture_funcs = NULL; Cvar_ForceCallback(&vid_conautoscale); } void Media_VideoRestarting(void) { Media_FlushCapture(); } void Media_VideoRestarted(void) { #ifdef GLQUAKE if (capturingfbo && qrenderer == QR_OPENGL && gl_config.ext_framebuffer_objects) { //restore it how it was, if we can. int w = capturefbo.rb_size[0], h = capturefbo.rb_size[1]; capturingfbo = true; capturetexture = R2D_RT_Configure("$democapture", w, h, TF_BGRA32, RT_IMAGEFLAGS); captureoldfbo = GLBE_FBO_Update(&capturefbo, FBO_RB_DEPTH|(Sh_StencilShadowsActive()?FBO_RB_STENCIL:0), &capturetexture, 1, r_nulltex, capturetexture->width, capturetexture->height, 0); vid.fbpwidth = capturetexture->width; vid.fbpheight = capturetexture->height; vid.framebuffer = capturetexture; } else #endif capturingfbo = false; if (capturingfbo) Cvar_ForceCallback(&vid_conautoscale); } static void Media_RecordFilm (char *recordingname, qboolean demo) { int sndkhz, sndchannels, sndbits; int i; Media_StopRecordFilm_f(); if (capturerate.value<=0) { Con_Printf("Invalid capturerate\n"); capturerate.value = 15; } captureframeinterval = 1/capturerate.value; if (captureframeinterval < 0.001) captureframeinterval = 0.001; //no more than 1000 images per second. capturelastvideotime = realtime = 0; Con_ClearNotify(); captureframe = offscreen_captureframe = 0; for (i = 0; i < sizeof(pluginencodersfunc)/sizeof(pluginencodersfunc[0]); i++) { if (pluginencodersfunc[i]) if (!strcmp(pluginencodersfunc[i]->drivername, capturedriver.string)) currentcapture_funcs = pluginencodersfunc[i]; } if (!currentcapture_funcs) { //try to find one based upon the explicit extension given. char captext[8]; const char *outext = COM_GetFileExtension(recordingname, NULL); for (i = 0; i < countof(pluginencodersfunc); i++) { if (pluginencodersfunc[i] && pluginencodersfunc[i]->extensions) { const char *t = pluginencodersfunc[i]->extensions; while (*t) { t = COM_ParseStringSetSep (t, ';', captext, sizeof(captext)); if (wildcmp(captext, outext)) { //matches the wildcard... currentcapture_funcs = pluginencodersfunc[i]; break; } } } } } if (!currentcapture_funcs) { //otherwise just find the first valid one that's in a plugin. for (i = 0; i < countof(pluginencodersfunc); i++) { if (pluginencodersfunc[i] && pluginencodersfunc[i]->extensions && pluginencodersplugin[i]) currentcapture_funcs = pluginencodersfunc[i]; } } if (!currentcapture_funcs) { //otherwise just find the first valid one that's from anywhere... for (i = 0; i < countof(pluginencodersfunc); i++) { if (pluginencodersfunc[i] && pluginencodersfunc[i]->extensions) currentcapture_funcs = pluginencodersfunc[i]; } } if (capturesound.ival && !nosound.ival) { sndkhz = snd_speed?snd_speed:48000; sndchannels = capturesoundchannels.ival; sndbits = capturesoundbits.ival; } else { sndkhz = 0; sndchannels = 0; sndbits = 0; } vid.fbpwidth = vid.pixelwidth; vid.fbpheight = vid.pixelheight; if (demo && capturewidth.ival && captureheight.ival) { #ifdef GLQUAKE if (qrenderer == QR_OPENGL && gl_config.ext_framebuffer_objects) { capturingfbo = true; capturetexture = R2D_RT_Configure("$democapture", capturewidth.ival, captureheight.ival, TF_BGRA32, RT_IMAGEFLAGS); captureoldfbo = GLBE_FBO_Update(&capturefbo, FBO_RB_DEPTH|(Sh_StencilShadowsActive()?FBO_RB_STENCIL:0), &capturetexture, 1, r_nulltex, capturewidth.ival, captureheight.ival, 0); vid.fbpwidth = capturetexture->width; vid.fbpheight = capturetexture->height; vid.framebuffer = capturetexture; } #endif } offscreen_format = TF_INVALID; #ifdef GLQUAKE_PBOS if (qrenderer == QR_OPENGL && !gl_config.gles && gl_config.glversion >= 2.1) { //both tgas and vfw favour bgr24, so lets get the gl drivers to suffer instead of us, where possible. if (vid.fbpwidth & 3) offscreen_format = TF_BGRA32; //don't bother changing pack alignment, just use something that is guarenteed to not need anything. else offscreen_format = TF_BGR24; } #endif #ifdef VKQUAKE // if (qrenderer == QR_VULKAN) // offscreen_format = TF_BGRA32; //use the native format, the driver won't do byteswapping for us. #endif recordingdemo = demo; if (!currentcapture_funcs->capture_begin) currentcapture_ctx = NULL; else { char fname[MAX_QPATH]; char ext[8]; COM_FileExtension(recordingname, ext, sizeof(ext)); if (!strcmp(ext, "dem") || !strcmp(ext, "qwd") || !strcmp(ext, "mvd") || !strcmp(ext, "dm2") || !strcmp(ext, "gz") || !strcmp(ext, "xz") || !*ext) { //extensions are evil, but whatever. //make sure there is actually an extension there, and don't break if they try overwriting a known demo format... COM_StripExtension(recordingname, fname, sizeof(fname)); if (currentcapture_funcs->extensions) { COM_ParseStringSetSep (currentcapture_funcs->extensions, ';', ext, sizeof(ext)); Q_strncatz(fname, ext, sizeof(fname)); } recordingname = fname; } currentcapture_ctx = currentcapture_funcs->capture_begin(recordingname, capturerate.value, vid.fbpwidth, vid.fbpheight, &sndkhz, &sndchannels, &sndbits); } if (!currentcapture_ctx) { recordingdemo = false; currentcapture_funcs = NULL; Con_Printf("Unable to initialise capture driver\n"); Media_StopRecordFilm_f(); } else if (sndkhz) Media_InitFakeSoundDevice(sndkhz, sndchannels, sndbits); Cvar_ForceCallback(&vid_conautoscale); } static void Media_RecordFilm_f (void) { if (Cmd_Argc() != 2) { int i; Con_Printf("capture \nRecords video output in an avi file.\nUse capturerate and capturecodec to configure.\n\n"); for (i = 0; i < countof(pluginencodersfunc); i++) { if (pluginencodersfunc[i]) Con_Printf("%s%s^7: %s\n", !strcmp(pluginencodersfunc[i]->drivername, capturedriver.string)?"^2":"^3", pluginencodersfunc[i]->drivername, pluginencodersfunc[i]->description); } Con_Printf("\n"); Con_Printf("Current capture settings:\n"); Con_Printf(" ^[/capturedriver %s^]\n", capturedriver.string); Con_Printf(" ^[/capturecodec %s^]\n", capturecodec.string); Con_Printf(" ^[/capturedemowidth %s^]\n", capturewidth.string); Con_Printf(" ^[/capturedemoheight %s^]\n", captureheight.string); Con_Printf(" ^[/capturerate %s^]\n", capturerate.string); Con_Printf(" ^[/capturesound %s^]\n", capturesound.string); if (capturesound.value) { Con_Printf(" ^[/capturesoundchannels %s^]\n", capturesoundchannels.string); Con_Printf(" ^[/capturesoundbits %s^]\n", capturesoundbits.string); } return; } if (Cmd_IsInsecure()) //err... don't think so sonny. return; Media_RecordFilm(Cmd_Argv(1), false); } void Media_CaptureDemoEnd(void) { if (recordingdemo) Media_StopRecordFilm_f(); } void CL_PlayDemo(char *demoname, qboolean usesystempath); void Media_RecordDemo_f(void) { char *demoname = Cmd_Argv(1); char *videoname = Cmd_Argv(2); if (Cmd_FromGamecode()) return; if (Cmd_Argc()<=1) { Con_Printf( "capturedemo demoname outputname\n" "captures a demo to video frames using offline rendering for smoother results\n" "see also: apropos capture\n"); return; } if (!Renderer_Started() && !isDedicated) { Cbuf_AddText(va("wait;%s %s\n", Cmd_Argv(0), Cmd_Args()), Cmd_ExecLevel); return; } if (Cmd_Argc()<=2) videoname = demoname; CL_Stopdemo_f(); //capturing failed for some reason CL_PlayDemo(demoname, false); if (!cls.demoplayback) { Con_Printf("unable to play demo, not capturing\n"); return; } //FIXME: make sure it loaded okay Media_RecordFilm(videoname, true); scr_con_current=0; Menu_PopAll(); Key_Dest_Remove(kdm_console); if (!currentcapture_funcs) CL_Stopdemo_f(); //capturing failed for some reason } #else void Media_CaptureDemoEnd(void) {} qboolean Media_PausedDemo(qboolean fortiming) {return false;} double Media_TweekCaptureFrameTime(double oldtime, double time) { return oldtime+time ; } void Media_RecordFrame (void) {} void Media_VideoRestarting(void) {} void Media_VideoRestarted(void) {} #endif #ifdef HAVE_SPEECHTOTEXT typedef struct ISpNotifySink ISpNotifySink; typedef void *ISpNotifyCallback; typedef void __stdcall SPNOTIFYCALLBACK(WPARAM wParam, LPARAM lParam); typedef struct SPEVENT { WORD eEventId : 16; WORD elParamType : 16; ULONG ulStreamNum; ULONGLONG ullAudioStreamOffset; WPARAM wParam; LPARAM lParam; } SPEVENT; #define SPEVENTSOURCEINFO void #define ISpObjectToken void #define ISpStreamFormat void #define SPVOICESTATUS void #define SPVPRIORITY int #define SPEVENTENUM int typedef struct ISpVoice ISpVoice; typedef struct ISpVoiceVtbl { HRESULT ( STDMETHODCALLTYPE *QueryInterface )( ISpVoice * This, /* [in] */ REFIID riid, /* [iid_is][out] */ void **ppvObject); ULONG ( STDMETHODCALLTYPE *AddRef )( ISpVoice * This); ULONG ( STDMETHODCALLTYPE *Release )( ISpVoice * This); HRESULT ( STDMETHODCALLTYPE *SetNotifySink )( ISpVoice * This, /* [in] */ ISpNotifySink *pNotifySink); /* [local] */ HRESULT ( STDMETHODCALLTYPE *SetNotifyWindowMessage )( ISpVoice * This, /* [in] */ HWND hWnd, /* [in] */ UINT Msg, /* [in] */ WPARAM wParam, /* [in] */ LPARAM lParam); /* [local] */ HRESULT ( STDMETHODCALLTYPE *SetNotifyCallbackFunction )( ISpVoice * This, /* [in] */ SPNOTIFYCALLBACK *pfnCallback, /* [in] */ WPARAM wParam, /* [in] */ LPARAM lParam); /* [local] */ HRESULT ( STDMETHODCALLTYPE *SetNotifyCallbackInterface )( ISpVoice * This, /* [in] */ ISpNotifyCallback *pSpCallback, /* [in] */ WPARAM wParam, /* [in] */ LPARAM lParam); /* [local] */ HRESULT ( STDMETHODCALLTYPE *SetNotifyWin32Event )( ISpVoice * This); /* [local] */ HRESULT ( STDMETHODCALLTYPE *WaitForNotifyEvent )( ISpVoice * This, /* [in] */ DWORD dwMilliseconds); /* [local] */ HANDLE ( STDMETHODCALLTYPE *GetNotifyEventHandle )( ISpVoice * This); HRESULT ( STDMETHODCALLTYPE *SetInterest )( ISpVoice * This, /* [in] */ ULONGLONG ullEventInterest, /* [in] */ ULONGLONG ullQueuedInterest); HRESULT ( STDMETHODCALLTYPE *GetEvents )( ISpVoice * This, /* [in] */ ULONG ulCount, /* [size_is][out] */ SPEVENT *pEventArray, /* [out] */ ULONG *pulFetched); HRESULT ( STDMETHODCALLTYPE *GetInfo )( ISpVoice * This, /* [out] */ SPEVENTSOURCEINFO *pInfo); HRESULT ( STDMETHODCALLTYPE *SetOutput )( ISpVoice * This, /* [in] */ IUnknown *pUnkOutput, /* [in] */ BOOL fAllowFormatChanges); HRESULT ( STDMETHODCALLTYPE *GetOutputObjectToken )( ISpVoice * This, /* [out] */ ISpObjectToken **ppObjectToken); HRESULT ( STDMETHODCALLTYPE *GetOutputStream )( ISpVoice * This, /* [out] */ ISpStreamFormat **ppStream); HRESULT ( STDMETHODCALLTYPE *Pause )( ISpVoice * This); HRESULT ( STDMETHODCALLTYPE *Resume )( ISpVoice * This); HRESULT ( STDMETHODCALLTYPE *SetVoice )( ISpVoice * This, /* [in] */ ISpObjectToken *pToken); HRESULT ( STDMETHODCALLTYPE *GetVoice )( ISpVoice * This, /* [out] */ ISpObjectToken **ppToken); HRESULT ( STDMETHODCALLTYPE *Speak )( ISpVoice * This, /* [string][in] */ const WCHAR *pwcs, /* [in] */ DWORD dwFlags, /* [out] */ ULONG *pulStreamNumber); HRESULT ( STDMETHODCALLTYPE *SpeakStream )( ISpVoice * This, /* [in] */ IStream *pStream, /* [in] */ DWORD dwFlags, /* [out] */ ULONG *pulStreamNumber); HRESULT ( STDMETHODCALLTYPE *GetStatus )( ISpVoice * This, /* [out] */ SPVOICESTATUS *pStatus, /* [string][out] */ WCHAR **ppszLastBookmark); HRESULT ( STDMETHODCALLTYPE *Skip )( ISpVoice * This, /* [string][in] */ WCHAR *pItemType, /* [in] */ long lNumItems, /* [out] */ ULONG *pulNumSkipped); HRESULT ( STDMETHODCALLTYPE *SetPriority )( ISpVoice * This, /* [in] */ SPVPRIORITY ePriority); HRESULT ( STDMETHODCALLTYPE *GetPriority )( ISpVoice * This, /* [out] */ SPVPRIORITY *pePriority); HRESULT ( STDMETHODCALLTYPE *SetAlertBoundary )( ISpVoice * This, /* [in] */ SPEVENTENUM eBoundary); HRESULT ( STDMETHODCALLTYPE *GetAlertBoundary )( ISpVoice * This, /* [out] */ SPEVENTENUM *peBoundary); HRESULT ( STDMETHODCALLTYPE *SetRate )( ISpVoice * This, /* [in] */ long RateAdjust); HRESULT ( STDMETHODCALLTYPE *GetRate )( ISpVoice * This, /* [out] */ long *pRateAdjust); HRESULT ( STDMETHODCALLTYPE *SetVolume )( ISpVoice * This, /* [in] */ USHORT usVolume); HRESULT ( STDMETHODCALLTYPE *GetVolume )( ISpVoice * This, /* [out] */ USHORT *pusVolume); HRESULT ( STDMETHODCALLTYPE *WaitUntilDone )( ISpVoice * This, /* [in] */ ULONG msTimeout); HRESULT ( STDMETHODCALLTYPE *SetSyncSpeakTimeout )( ISpVoice * This, /* [in] */ ULONG msTimeout); HRESULT ( STDMETHODCALLTYPE *GetSyncSpeakTimeout )( ISpVoice * This, /* [out] */ ULONG *pmsTimeout); /* [local] */ HANDLE ( STDMETHODCALLTYPE *SpeakCompleteEvent )( ISpVoice * This); /* [local] */ HRESULT ( STDMETHODCALLTYPE *IsUISupported )( ISpVoice * This, /* [in] */ const WCHAR *pszTypeOfUI, /* [in] */ void *pvExtraData, /* [in] */ ULONG cbExtraData, /* [out] */ BOOL *pfSupported); /* [local] */ HRESULT ( STDMETHODCALLTYPE *DisplayUI )( ISpVoice * This, /* [in] */ HWND hwndParent, /* [in] */ const WCHAR *pszTitle, /* [in] */ const WCHAR *pszTypeOfUI, /* [in] */ void *pvExtraData, /* [in] */ ULONG cbExtraData); END_INTERFACE } ISpVoiceVtbl; struct ISpVoice { struct ISpVoiceVtbl *lpVtbl; }; void TTS_SayUnicodeString(wchar_t *stringtosay) { static CLSID CLSID_SpVoice = {0x96749377, 0x3391, 0x11D2, {0x9E,0xE3,0x00,0xC0,0x4F,0x79,0x73,0x96}}; static GUID IID_ISpVoice = {0x6C44DF74,0x72B9,0x4992, {0xA1,0xEC,0xEF,0x99,0x6E,0x04,0x22,0xD4}}; static ISpVoice *sp = NULL; if (!sp) CoCreateInstance( &CLSID_SpVoice, NULL, CLSCTX_SERVER, &IID_ISpVoice, (void*)&sp); if (sp) { sp->lpVtbl->Speak(sp, stringtosay, 1, NULL); } } void TTS_SayAsciiString(char *stringtosay) { wchar_t bigbuffer[8192]; mbstowcs(bigbuffer, stringtosay, sizeof(bigbuffer)/sizeof(bigbuffer[0]) - 1); bigbuffer[sizeof(bigbuffer)/sizeof(bigbuffer[0]) - 1] = 0; TTS_SayUnicodeString(bigbuffer); } cvar_t tts_mode = CVARD("tts_mode", "1", "Text to speech\n0: off\n1: Read only chat messages with a leading 'tts ' prefix.\n2: Read all chat messages\n3: Read every single console print."); void TTS_SayChatString(char **stringtosay) { if (!strncmp(*stringtosay, "tts ", 4)) { *stringtosay += 4; if (tts_mode.ival != 1 && tts_mode.ival != 2) return; } else { if (tts_mode.ival != 2) return; } TTS_SayAsciiString(*stringtosay); } void TTS_SayConString(conchar_t *stringtosay) { wchar_t bigbuffer[8192]; int i; if (tts_mode.ival < 3) return; for (i = 0; i < 8192-1 && *stringtosay; i++, stringtosay++) { if ((*stringtosay & 0xff00) == 0xe000) bigbuffer[i] = *stringtosay & 0x7f; else bigbuffer[i] = *stringtosay & CON_CHARMASK; } bigbuffer[i] = 0; if (i) TTS_SayUnicodeString(bigbuffer); } void TTS_Say_f(void) { TTS_SayAsciiString(Cmd_Args()); } #define ISpRecognizer void #define SPPHRASE void #define SPSERIALIZEDPHRASE void #define SPSTATEHANDLE void* #define SPGRAMMARWORDTYPE int #define SPPROPERTYINFO void #define SPLOADOPTIONS void* #define SPBINARYGRAMMAR void* #define SPRULESTATE int #define SPTEXTSELECTIONINFO void #define SPWORDPRONOUNCEABLE void #define SPGRAMMARSTATE int typedef struct ISpRecoResult ISpRecoResult; typedef struct ISpRecoContext ISpRecoContext; typedef struct ISpRecoGrammar ISpRecoGrammar; typedef struct ISpRecoContextVtbl { HRESULT ( STDMETHODCALLTYPE *QueryInterface )( ISpRecoContext * This, /* [in] */ REFIID riid, /* [iid_is][out] */ void **ppvObject); ULONG ( STDMETHODCALLTYPE *AddRef )( ISpRecoContext * This); ULONG ( STDMETHODCALLTYPE *Release )( ISpRecoContext * This); HRESULT ( STDMETHODCALLTYPE *SetNotifySink )( ISpRecoContext * This, /* [in] */ ISpNotifySink *pNotifySink); /* [local] */ HRESULT ( STDMETHODCALLTYPE *SetNotifyWindowMessage )( ISpRecoContext * This, /* [in] */ HWND hWnd, /* [in] */ UINT Msg, /* [in] */ WPARAM wParam, /* [in] */ LPARAM lParam); /* [local] */ HRESULT ( STDMETHODCALLTYPE *SetNotifyCallbackFunction )( ISpRecoContext * This, /* [in] */ SPNOTIFYCALLBACK *pfnCallback, /* [in] */ WPARAM wParam, /* [in] */ LPARAM lParam); /* [local] */ HRESULT ( STDMETHODCALLTYPE *SetNotifyCallbackInterface )( ISpRecoContext * This, /* [in] */ ISpNotifyCallback *pSpCallback, /* [in] */ WPARAM wParam, /* [in] */ LPARAM lParam); /* [local] */ HRESULT ( STDMETHODCALLTYPE *SetNotifyWin32Event )( ISpRecoContext * This); /* [local] */ HRESULT ( STDMETHODCALLTYPE *WaitForNotifyEvent )( ISpRecoContext * This, /* [in] */ DWORD dwMilliseconds); /* [local] */ HANDLE ( STDMETHODCALLTYPE *GetNotifyEventHandle )( ISpRecoContext * This); HRESULT ( STDMETHODCALLTYPE *SetInterest )( ISpRecoContext * This, /* [in] */ ULONGLONG ullEventInterest, /* [in] */ ULONGLONG ullQueuedInterest); HRESULT ( STDMETHODCALLTYPE *GetEvents )( ISpRecoContext * This, /* [in] */ ULONG ulCount, /* [size_is][out] */ SPEVENT *pEventArray, /* [out] */ ULONG *pulFetched); HRESULT ( STDMETHODCALLTYPE *GetInfo )( ISpRecoContext * This, /* [out] */ SPEVENTSOURCEINFO *pInfo); HRESULT ( STDMETHODCALLTYPE *GetRecognizer )( ISpRecoContext * This, /* [out] */ ISpRecognizer **ppRecognizer); HRESULT ( STDMETHODCALLTYPE *CreateGrammar )( ISpRecoContext * This, /* [in] */ ULONGLONG ullGrammarId, /* [out] */ ISpRecoGrammar **ppGrammar); } ISpRecoContextVtbl; struct ISpRecoContext { struct ISpRecoContextVtbl *lpVtbl; }; typedef struct ISpRecoResultVtbl { HRESULT ( STDMETHODCALLTYPE *QueryInterface )( ISpRecoResult * This, /* [in] */ REFIID riid, /* [iid_is][out] */ void **ppvObject); ULONG ( STDMETHODCALLTYPE *AddRef )( ISpRecoResult * This); ULONG ( STDMETHODCALLTYPE *Release )( ISpRecoResult * This); HRESULT ( STDMETHODCALLTYPE *GetPhrase )( ISpRecoResult * This, /* [out] */ SPPHRASE **ppCoMemPhrase); HRESULT ( STDMETHODCALLTYPE *GetSerializedPhrase )( ISpRecoResult * This, /* [out] */ SPSERIALIZEDPHRASE **ppCoMemPhrase); HRESULT ( STDMETHODCALLTYPE *GetText )( ISpRecoResult * This, /* [in] */ ULONG ulStart, /* [in] */ ULONG ulCount, /* [in] */ BOOL fUseTextReplacements, /* [out] */ WCHAR **ppszCoMemText, /* [out] */ BYTE *pbDisplayAttributes); HRESULT ( STDMETHODCALLTYPE *Discard )( ISpRecoResult * This, /* [in] */ DWORD dwValueTypes); #if 0 HRESULT ( STDMETHODCALLTYPE *GetResultTimes )( ISpRecoResult * This, /* [out] */ SPRECORESULTTIMES *pTimes); HRESULT ( STDMETHODCALLTYPE *GetAlternates )( ISpRecoResult * This, /* [in] */ ULONG ulStartElement, /* [in] */ ULONG cElements, /* [in] */ ULONG ulRequestCount, /* [out] */ ISpPhraseAlt **ppPhrases, /* [out] */ ULONG *pcPhrasesReturned); HRESULT ( STDMETHODCALLTYPE *GetAudio )( ISpRecoResult * This, /* [in] */ ULONG ulStartElement, /* [in] */ ULONG cElements, /* [out] */ ISpStreamFormat **ppStream); HRESULT ( STDMETHODCALLTYPE *SpeakAudio )( ISpRecoResult * This, /* [in] */ ULONG ulStartElement, /* [in] */ ULONG cElements, /* [in] */ DWORD dwFlags, /* [out] */ ULONG *pulStreamNumber); HRESULT ( STDMETHODCALLTYPE *Serialize )( ISpRecoResult * This, /* [out] */ SPSERIALIZEDRESULT **ppCoMemSerializedResult); HRESULT ( STDMETHODCALLTYPE *ScaleAudio )( ISpRecoResult * This, /* [in] */ const GUID *pAudioFormatId, /* [in] */ const WAVEFORMATEX *pWaveFormatEx); HRESULT ( STDMETHODCALLTYPE *GetRecoContext )( ISpRecoResult * This, /* [out] */ ISpRecoContext **ppRecoContext); #endif } ISpRecoResultVtbl; struct ISpRecoResult { struct ISpRecoResultVtbl *lpVtbl; }; typedef struct ISpRecoGrammarVtbl { HRESULT ( STDMETHODCALLTYPE *QueryInterface )( ISpRecoGrammar * This, /* [in] */ REFIID riid, /* [iid_is][out] */ void **ppvObject); ULONG ( STDMETHODCALLTYPE *AddRef )( ISpRecoGrammar * This); ULONG ( STDMETHODCALLTYPE *Release )( ISpRecoGrammar * This); HRESULT ( STDMETHODCALLTYPE *ResetGrammar )( ISpRecoGrammar * This, /* [in] */ WORD NewLanguage); HRESULT ( STDMETHODCALLTYPE *GetRule )( ISpRecoGrammar * This, /* [in] */ const WCHAR *pszRuleName, /* [in] */ DWORD dwRuleId, /* [in] */ DWORD dwAttributes, /* [in] */ BOOL fCreateIfNotExist, /* [out] */ SPSTATEHANDLE *phInitialState); HRESULT ( STDMETHODCALLTYPE *ClearRule )( ISpRecoGrammar * This, SPSTATEHANDLE hState); HRESULT ( STDMETHODCALLTYPE *CreateNewState )( ISpRecoGrammar * This, SPSTATEHANDLE hState, SPSTATEHANDLE *phState); HRESULT ( STDMETHODCALLTYPE *AddWordTransition )( ISpRecoGrammar * This, SPSTATEHANDLE hFromState, SPSTATEHANDLE hToState, const WCHAR *psz, const WCHAR *pszSeparators, SPGRAMMARWORDTYPE eWordType, float Weight, const SPPROPERTYINFO *pPropInfo); HRESULT ( STDMETHODCALLTYPE *AddRuleTransition )( ISpRecoGrammar * This, SPSTATEHANDLE hFromState, SPSTATEHANDLE hToState, SPSTATEHANDLE hRule, float Weight, const SPPROPERTYINFO *pPropInfo); HRESULT ( STDMETHODCALLTYPE *AddResource )( ISpRecoGrammar * This, /* [in] */ SPSTATEHANDLE hRuleState, /* [in] */ const WCHAR *pszResourceName, /* [in] */ const WCHAR *pszResourceValue); HRESULT ( STDMETHODCALLTYPE *Commit )( ISpRecoGrammar * This, DWORD dwReserved); HRESULT ( STDMETHODCALLTYPE *GetGrammarId )( ISpRecoGrammar * This, /* [out] */ ULONGLONG *pullGrammarId); HRESULT ( STDMETHODCALLTYPE *GetRecoContext )( ISpRecoGrammar * This, /* [out] */ ISpRecoContext **ppRecoCtxt); HRESULT ( STDMETHODCALLTYPE *LoadCmdFromFile )( ISpRecoGrammar * This, /* [string][in] */ const WCHAR *pszFileName, /* [in] */ SPLOADOPTIONS Options); HRESULT ( STDMETHODCALLTYPE *LoadCmdFromObject )( ISpRecoGrammar * This, /* [in] */ REFCLSID rcid, /* [string][in] */ const WCHAR *pszGrammarName, /* [in] */ SPLOADOPTIONS Options); HRESULT ( STDMETHODCALLTYPE *LoadCmdFromResource )( ISpRecoGrammar * This, /* [in] */ HMODULE hModule, /* [string][in] */ const WCHAR *pszResourceName, /* [string][in] */ const WCHAR *pszResourceType, /* [in] */ WORD wLanguage, /* [in] */ SPLOADOPTIONS Options); HRESULT ( STDMETHODCALLTYPE *LoadCmdFromMemory )( ISpRecoGrammar * This, /* [in] */ const SPBINARYGRAMMAR *pGrammar, /* [in] */ SPLOADOPTIONS Options); HRESULT ( STDMETHODCALLTYPE *LoadCmdFromProprietaryGrammar )( ISpRecoGrammar * This, /* [in] */ REFGUID rguidParam, /* [string][in] */ const WCHAR *pszStringParam, /* [in] */ const void *pvDataPrarm, /* [in] */ ULONG cbDataSize, /* [in] */ SPLOADOPTIONS Options); HRESULT ( STDMETHODCALLTYPE *SetRuleState )( ISpRecoGrammar * This, /* [string][in] */ const WCHAR *pszName, void *pReserved, /* [in] */ SPRULESTATE NewState); HRESULT ( STDMETHODCALLTYPE *SetRuleIdState )( ISpRecoGrammar * This, /* [in] */ ULONG ulRuleId, /* [in] */ SPRULESTATE NewState); HRESULT ( STDMETHODCALLTYPE *LoadDictation )( ISpRecoGrammar * This, /* [string][in] */ const WCHAR *pszTopicName, /* [in] */ SPLOADOPTIONS Options); HRESULT ( STDMETHODCALLTYPE *UnloadDictation )( ISpRecoGrammar * This); HRESULT ( STDMETHODCALLTYPE *SetDictationState )( ISpRecoGrammar * This, /* [in] */ SPRULESTATE NewState); HRESULT ( STDMETHODCALLTYPE *SetWordSequenceData )( ISpRecoGrammar * This, /* [in] */ const WCHAR *pText, /* [in] */ ULONG cchText, /* [in] */ const SPTEXTSELECTIONINFO *pInfo); HRESULT ( STDMETHODCALLTYPE *SetTextSelection )( ISpRecoGrammar * This, /* [in] */ const SPTEXTSELECTIONINFO *pInfo); HRESULT ( STDMETHODCALLTYPE *IsPronounceable )( ISpRecoGrammar * This, /* [string][in] */ const WCHAR *pszWord, /* [out] */ SPWORDPRONOUNCEABLE *pWordPronounceable); HRESULT ( STDMETHODCALLTYPE *SetGrammarState )( ISpRecoGrammar * This, /* [in] */ SPGRAMMARSTATE eGrammarState); HRESULT ( STDMETHODCALLTYPE *SaveCmd )( ISpRecoGrammar * This, /* [in] */ IStream *pStream, /* [optional][out] */ WCHAR **ppszCoMemErrorText); HRESULT ( STDMETHODCALLTYPE *GetGrammarState )( ISpRecoGrammar * This, /* [out] */ SPGRAMMARSTATE *peGrammarState); } ISpRecoGrammarVtbl; struct ISpRecoGrammar { struct ISpRecoGrammarVtbl *lpVtbl; }; static ISpRecoContext *stt_recctx = NULL; static ISpRecoGrammar *stt_gram = NULL; void STT_Event(void) { WCHAR *wstring, *i; struct SPEVENT ev; ISpRecoResult *rr; HRESULT hr; char asc[2048], *o; int l; unsigned short c; char *nib = "0123456789abcdef"; if (!stt_gram) return; while (SUCCEEDED(hr = stt_recctx->lpVtbl->GetEvents(stt_recctx, 1, &ev, NULL)) && hr != S_FALSE) { rr = (ISpRecoResult*)ev.lParam; rr->lpVtbl->GetText(rr, -1, -1, TRUE, &wstring, NULL); for (l = sizeof(asc)-1, o = asc, i = wstring; l > 0 && *i; ) { c = *i++; if (c == '\n' || c == ';') { } else if (c < 128) { *o++ = c; l--; } else if (l > 6) { *o++ = '^'; *o++ = 'U'; *o++ = nib[(c>>12)&0xf]; *o++ = nib[(c>>8)&0xf]; *o++ = nib[(c>>4)&0xf]; *o++ = nib[(c>>0)&0xf]; } else break; } *o = 0; CoTaskMemFree(wstring); Cbuf_AddText("say tts ", RESTRICT_LOCAL); Cbuf_AddText(asc, RESTRICT_LOCAL); Cbuf_AddText("\n", RESTRICT_LOCAL); rr->lpVtbl->Release(rr); } } void STT_Init_f(void) { static CLSID CLSID_SpSharedRecoContext = {0x47206204, 0x5ECA, 0x11D2, {0x96, 0x0F, 0x00, 0xC0, 0x4F, 0x8E, 0xE6, 0x28}}; static CLSID IID_SpRecoContext = {0xF740A62F, 0x7C15, 0x489E, {0x82, 0x34, 0x94, 0x0A, 0x33, 0xD9, 0x27, 0x2D}}; if (stt_gram) { stt_gram->lpVtbl->Release(stt_gram); stt_recctx->lpVtbl->Release(stt_recctx); stt_gram = NULL; stt_recctx = NULL; Con_Printf("Speech-to-text disabled\n"); return; } if (SUCCEEDED(CoCreateInstance(&CLSID_SpSharedRecoContext, NULL, CLSCTX_SERVER, &IID_SpRecoContext, (void*)&stt_recctx))) { ULONGLONG ev = (((ULONGLONG)1) << 38) | (((ULONGLONG)1) << 30) | (((ULONGLONG)1) << 33); if (SUCCEEDED(stt_recctx->lpVtbl->SetNotifyWindowMessage(stt_recctx, mainwindow, WM_USER_SPEECHTOTEXT, 0, 0))) if (SUCCEEDED(stt_recctx->lpVtbl->SetInterest(stt_recctx, ev, ev))) if (SUCCEEDED(stt_recctx->lpVtbl->CreateGrammar(stt_recctx, 0, &stt_gram))) { if (SUCCEEDED(stt_gram->lpVtbl->LoadDictation(stt_gram, NULL, 0))) if (SUCCEEDED(stt_gram->lpVtbl->SetDictationState(stt_gram, 1))) { //success! Con_Printf("Speech-to-text active\n"); return; } stt_gram->lpVtbl->Release(stt_gram); } stt_recctx->lpVtbl->Release(stt_recctx); } stt_gram = NULL; stt_recctx = NULL; Con_Printf("Speech-to-text unavailable\n"); } #endif #ifdef AVAIL_MP3_ACM typedef struct { HACMSTREAM acm; unsigned int dstbuffer; /*in frames*/ unsigned int dstcount; /*in frames*/ unsigned int dststart; /*in frames*/ qbyte *dstdata; unsigned int srcspeed; qaudiofmt_t srcformat; unsigned int srcchannels; unsigned int srcoffset; /*in bytes*/ unsigned int srclen; /*in bytes*/ qbyte srcdata[1]; char title[256]; } mp3decoder_t; static void QDECL S_MP3_Purge(sfx_t *sfx) { mp3decoder_t *dec = sfx->decoder.buf; sfx->decoder.buf = NULL; sfx->decoder.ended = NULL; sfx->decoder.purge = NULL; sfx->decoder.decodedata = NULL; qacmStreamClose(dec->acm, 0); if (dec->dstdata) BZ_Free(dec->dstdata); BZ_Free(dec); sfx->loadstate = SLS_NOTLOADED; } float QDECL S_MP3_Query(sfx_t *sfx, sfxcache_t *buf, char *title, size_t titlesize) { mp3decoder_t *dec = sfx->decoder.buf; //we don't know unless we decode it all if (buf) { } if (titlesize && dec->srclen >= 128) { //id3v1 is a 128 byte blob at the end of the file. char trimartist[31]; char trimtitle[31]; char *p; struct { char tag[3]; //TAG char title[30]; char artist[30]; char album[30]; char year[4]; char comment[30];//[28]+null+track qbyte genre; } *id3v1 = (void*)(dec->srcdata + dec->srclen-128); if (id3v1->tag[0] == 'T' && id3v1->tag[1] == 'A' && id3v1->tag[2] == 'G') { //yup, there's an id3v1 tag there memcpy(trimartist, id3v1->artist, 30); for(p = trimartist+30; p>trimartist && p[-1] == ' '; ) p--; *p = 0; memcpy(trimtitle, id3v1->title, 30); for(p = trimtitle+30; p>trimtitle && p[-1] == ' '; ) p--; *p = 0; if (*trimartist && *trimtitle) Q_snprintfz(title, titlesize, "%.30s - %.30s", trimartist, trimtitle); else if (*id3v1->title) Q_snprintfz(title, titlesize, "%.30s", trimtitle); } return 1;//no real idea. } return 0; } /*must be thread safe*/ sfxcache_t *QDECL S_MP3_Locate(sfx_t *sfx, sfxcache_t *buf, ssamplepos_t start, int length) { int newlen; if (buf) { mp3decoder_t *dec = sfx->decoder.buf; ACMSTREAMHEADER strhdr; char buffer[8192]; extern cvar_t snd_linearresample_stream; int framesz = (QAF_BYTES(dec->srcformat) * dec->srcchannels); if (length) { if (dec->dststart > start) { /*I don't know where the compressed data is for each sample. acm doesn't have a seek. so reset to start, for music this should be the most common rewind anyway*/ dec->dststart = 0; dec->dstcount = 0; dec->srcoffset = 0; } if (dec->dstcount > snd_speed*6) { int trim = dec->dstcount - snd_speed; //retain a second of buffer in case we have multiple sound devices if (dec->dststart + trim > start) { trim = start - dec->dststart; if (trim < 0) trim = 0; } // if (trim < 0) // trim = 0; /// if (trim > dec->dstcount) // trim = dec->dstcount; memmove(dec->dstdata, dec->dstdata + trim*framesz, (dec->dstcount - trim)*framesz); dec->dststart += trim; dec->dstcount -= trim; } while(start+length >= dec->dststart+dec->dstcount) { memset(&strhdr, 0, sizeof(strhdr)); strhdr.cbStruct = sizeof(strhdr); strhdr.pbSrc = dec->srcdata + dec->srcoffset; strhdr.cbSrcLength = dec->srclen - dec->srcoffset; if (!strhdr.cbSrcLength) break; strhdr.pbDst = buffer; strhdr.cbDstLength = sizeof(buffer); qacmStreamPrepareHeader(dec->acm, &strhdr, 0); qacmStreamConvert(dec->acm, &strhdr, ACM_STREAMCONVERTF_BLOCKALIGN); qacmStreamUnprepareHeader(dec->acm, &strhdr, 0); dec->srcoffset += strhdr.cbSrcLengthUsed; if (!strhdr.cbDstLengthUsed) { if (strhdr.cbSrcLengthUsed) continue; break; } newlen = dec->dstcount + (strhdr.cbDstLengthUsed * ((float)snd_speed / dec->srcspeed))/framesz; if (dec->dstbuffer < newlen+64) { dec->dstbuffer = newlen+64 + snd_speed; dec->dstdata = BZ_Realloc(dec->dstdata, dec->dstbuffer*framesz); } SND_ResampleStream(strhdr.pbDst, dec->srcspeed, dec->srcformat, dec->srcchannels, strhdr.cbDstLengthUsed / framesz, dec->dstdata+dec->dstcount*framesz, snd_speed, dec->srcformat, dec->srcchannels, snd_linearresample_stream.ival); dec->dstcount = newlen; } } buf->data = dec->dstdata; buf->length = dec->dstcount; buf->numchannels = dec->srcchannels; buf->soundoffset = dec->dststart; buf->speed = snd_speed; buf->format = dec->srcformat; if (dec->srclen == dec->srcoffset && start >= dec->dststart+dec->dstcount) return NULL; //once we reach the EOF, start reporting errors. } return buf; } #ifndef WAVE_FORMAT_MPEGLAYER3 #define WAVE_FORMAT_MPEGLAYER3 0x0055 typedef struct { WAVEFORMATEX wfx; WORD wID; DWORD fdwFlags; WORD nBlockSize; WORD nFramesPerBlock; WORD nCodecDelay; } MPEGLAYER3WAVEFORMAT; #endif #ifndef MPEGLAYER3_ID_MPEG #define MPEGLAYER3_WFX_EXTRA_BYTES 12 #define MPEGLAYER3_FLAG_PADDING_OFF 2 #define MPEGLAYER3_ID_MPEG 1 #endif static qboolean QDECL S_LoadMP3Sound (sfx_t *s, qbyte *data, size_t datalen, int sndspeed, qboolean forcedecode) { WAVEFORMATEX pcm_format; MPEGLAYER3WAVEFORMAT mp3format; HACMDRIVER drv = NULL; mp3decoder_t *dec; char ext[8]; COM_FileExtension(s->name, ext, sizeof(ext)); if (stricmp(ext, "mp3")) return false; dec = BZF_Malloc(sizeof(*dec) + datalen); if (!dec) return false; memcpy(dec->srcdata, data, datalen); dec->srclen = datalen; s->decoder.buf = dec; s->decoder.ended = S_MP3_Purge; s->decoder.purge = S_MP3_Purge; s->decoder.decodedata = S_MP3_Locate; s->decoder.querydata = S_MP3_Query; s->loopstart = -1; dec->dstdata = NULL; dec->dstcount = 0; dec->dststart = 0; dec->dstbuffer = 0; dec->srcoffset = 0; dec->srcspeed = 44100; dec->srcchannels = 2; dec->srcformat = QAF_S16; memset (&pcm_format, 0, sizeof(pcm_format)); pcm_format.wFormatTag = WAVE_FORMAT_PCM; pcm_format.nChannels = dec->srcchannels; pcm_format.nSamplesPerSec = dec->srcspeed; pcm_format.nBlockAlign = QAF_BYTES(dec->srcformat)*dec->srcchannels; pcm_format.nAvgBytesPerSec = pcm_format.nSamplesPerSec*QAF_BYTES(dec->srcformat)*dec->srcchannels; pcm_format.wBitsPerSample = QAF_BYTES(dec->srcformat)*8; pcm_format.cbSize = 0; mp3format.wfx.cbSize = MPEGLAYER3_WFX_EXTRA_BYTES; mp3format.wfx.wFormatTag = WAVE_FORMAT_MPEGLAYER3; mp3format.wfx.nChannels = dec->srcchannels; mp3format.wfx.nAvgBytesPerSec = 128 * (1024 / 8); // not really used but must be one of 64, 96, 112, 128, 160kbps mp3format.wfx.wBitsPerSample = 0; // MUST BE ZERO mp3format.wfx.nBlockAlign = 1; // MUST BE ONE mp3format.wfx.nSamplesPerSec = dec->srcspeed; // 44.1kHz mp3format.fdwFlags = MPEGLAYER3_FLAG_PADDING_OFF; mp3format.nBlockSize = 522; // voodoo value #1 - 144 x (bitrate / sample rate) + padding mp3format.nFramesPerBlock = 1; // MUST BE ONE mp3format.nCodecDelay = 0;//1393; // voodoo value #2 mp3format.wID = MPEGLAYER3_ID_MPEG; if (!qacmStartup() || 0!=qacmStreamOpen(&dec->acm, drv, (WAVEFORMATEX*)&mp3format, &pcm_format, NULL, 0, 0, 0)) { Con_Printf("Couldn't init decoder\n"); return false; } S_MP3_Locate(s, NULL, 0, 100); return true; } #endif void Media_Init(void) { #ifdef HAVE_JUKEBOX int i; Cmd_AddCommand("music_fforward", Media_FForward_f); Cmd_AddCommand("music_rewind", Media_Rewind_f); Cmd_AddCommand("music_next", Media_Next_f); Cmd_AddCommand("media_next", Media_Next_f); Cvar_Register(&music_playlist_index, "compat"); for (i = 0; i < 6; i++) { Cvar_Get(va("music_playlist_list%i", i), "", 0, "compat"); Cvar_Get(va("music_playlist_sampleposition%i", i), "-1", 0, "compat"); } music_playlist_last = -1; media_playlisttypes = MEDIA_PLAYLIST | MEDIA_GAMEMUSIC | MEDIA_CVARLIST; #ifdef WINAMP Cvar_Register(&media_hijackwinamp, "Media player things"); #endif Cvar_Register(&media_shuffle, "Media player things"); Cvar_Register(&media_repeat, "Media player things"); #if !defined(NOMEDIAMENU) && !defined(NOBUILTINMENUS) Cmd_AddCommand ("media_add", M_Media_Add_f); Cmd_AddCommand ("media_remove", M_Media_Remove_f); Cmd_AddCommand ("menu_media", M_Menu_Media_f); #endif #endif Cvar_Register(&music_fade, "Media player things"); #ifdef HAVE_SPEECHTOTEXT Cmd_AddCommand("tts", TTS_Say_f); Cmd_AddCommand("stt", STT_Init_f); Cvar_Register(&tts_mode, "Gimmicks"); #endif #ifdef AVAIL_MP3_ACM S_RegisterSoundInputPlugin(NULL, S_LoadMP3Sound); #endif #ifdef HAVE_CDPLAYER Cmd_AddCommand("cd", CD_f); cdenabled = false; if (COM_CheckParm("-nocdaudio")) cdenabled = false; if (COM_CheckParm("-cdaudio")) cdenabled = true; #endif #ifdef HAVE_MEDIA_ENCODER #ifdef _DEBUG Media_RegisterEncoder(NULL, &capture_null); #endif Media_RegisterEncoder(NULL, &capture_raw); #if defined(HAVE_API_VFW) Media_RegisterEncoder(NULL, &capture_avi); #endif Cmd_AddCommandD("capture", Media_RecordFilm_f, "Captures realtime action to a named video file. Check the capture* cvars to control driver/codecs/rates."); Cmd_AddCommandD("capturedemo", Media_RecordDemo_f, "capuuredemo foo.dem foo.avi - Captures a named demo to a named video file.\nDemo capturing is performed offscreen when possible, allowing arbitrary video sizes or smooth captures on underpowered hardware."); Cmd_AddCommandD("capturestop", Media_StopRecordFilm_f, "Aborts the current video capture."); Cmd_AddCommandD("capturepause", Media_CapturePause_f, "Pauses the video capture, allowing you to avoid capturing uninteresting parts. This is a toggle, so reuse the same command to resume capturing again."); Cvar_Register(&capturemessage, "Video Capture Controls"); Cvar_Register(&capturesound, "Video Capture Controls"); Cvar_Register(&capturerate, "Video Capture Controls"); Cvar_Register(&capturewidth, "Video Capture Controls"); Cvar_Register(&captureheight, "Video Capture Controls"); Cvar_Register(&capturedriver, "Video Capture Controls"); Cvar_Register(&capturecodec, "Video Capture Controls"); Cvar_Register(&capturethrottlesize, "Video Capture Controls"); Cvar_Register(&capturesoundbits, "Video Capture Controls"); Cvar_Register(&capturesoundchannels, "Video Capture Controls"); #endif #ifdef HAVE_MEDIA_DECODER Cmd_AddCommand("playclip", Media_PlayVideoWindowed_f); Cmd_AddCommand("playvideo", Media_PlayFilm_f); Cmd_AddCommand("playfilm", Media_PlayFilm_f); Cmd_AddCommand("cinematic", Media_PlayFilm_f); //q3: name <1:hold, 2:loop> #endif Cmd_AddCommand("music", Media_NamedTrack_f); Cmd_AddCommand("stopmusic", Media_StopTrack_f); }