HD-PVR Post-production

As already posted, last year I bought the Hauppauge HD-PVR. Since I’m a geek (am I?!) and I like doing things my way, using all the software provided with the device wasn’t an option I was going to choose. Also, since I always want maximum picture quality I always set the HD-PVR to record at full bitrate (13.5Mpbs CBR).

As you can imagine recording at 13.5Mpbs will produce some quite big files, but thats all good as I have plenty of disk space to waste (temporarily). For example, a 1 1/2 hour recording will take approx. 9.7GB. That’s big, even with lots of disk space to waste. So, I convert the recording to at least an MKV (AVC/AAC) as an archive/computer playable version and sometimes a DVD (to watch on the TV set, which only has a DVD player, for now).

Now since I’m a command line freak (did I say I was a geek?!), here’s how I do it. In order to do the same you will need at least the following software (all free):

1. The first step is is to load the .TS (or M2TS) file in DGAVCIndex:

DGAVCDec

And save the whole stream into a DGA project (File -> Save Project or F4). This will also demux the audio and create a [filename] PID 1100 DELAY [delay]ms.aac file.

2. Then I create an Avisynth script to load the DGA project in VirtualDub to make a stream selection (strip the beginning/end and commercials). The script looks like this:

LoadPlugin("DGAVCDecode.dll")
V=AVCSource("Project.dga")
A=DirectShowSource("Project PID 1100 DELAY -180ms.aac", video=false, seek=false)
AudioDub(V, A)
DelayAudio(-0.180)

[UPDATE 1/3/2010: You may want to use seekzero=true instead of seek=false as some seeking may be required depending on the context.]

Make sure to match the DelayAudio() with the delay from the AAC file then load the AVS into VirtualDub. This part can be time consuming as this is where I seek through the whole stream to find commercials and other parts I want to strip. For every part to strip I write down the first frame and last frame numbers to insert into the AVS. Once this is done you simply trim all the frames you want to keep like this:

Trim(29, 13187)+Trim(19031, 57183)+Trim(60138, 62091)

This strips the following frame ranges: 0-28, 13188-19030, 57184-60137 and 62092 to the end. Basically, Trim tells Avisynth to keep the specified frame range.

3. Now is the time to start encoding. First you will need to calculate your bitrate to make sure the newly encoded stream will fit your target media. Open the Bitrate Calculator and input your stream length and audio bitrate (I use 128Kpbs) and select your target media. Keep in mind the target bitrate is an average bitrate therefore you may want to bring it down by 1-2% to make sure you won’t have to re-encode the stream a second time.

For encoding the video, I use the following 2-pass encode:

x264.exe --profile high --pass 1 --bitrate 16000 --threads auto --thread-input --output Project.ts.avs.264 Project.ts.avs
x264.exe --profile high --pass 2 --bitrate 16000 --threads auto --thread-input --output Project.ts.avs.264 Project.ts.avs

For the audio, I use the following 1-pass encode:

wavi Project.ts.avs - | faac.exe -b 128 -o Project.ts.avs.aac -

Then I end up with 2 files, Project.ts.avs.264 and Project.ts.avs.aac which I multiplex in MKVToolnix to produce an MKV file. Make sure to select your video framerate in mmg.exe as .264 files do not store that information (you should get a warning after adding the .264 into mmg). Optionally, you can also select the language for both streams. That’s it for the high quality archive copy!

4. To create a DVD compliant structure, I use mencoder (lavcodec) for encoding both the video and audio. Here’s the command line I use for a high quality encode:

mencoder.exe -mc 0 -noskip -ovc lavc -nosound -lavcopts threads=2:vcodec=mpeg2video:vrc_buf_size=1835:vrc_maxrate=9800:vbitrate=9668:keyint=15:vpass=1:trell:mbd=2:precmp=2:subcmp=2:cmp=2:dia=-10:predia=-10:cbp:mv0:vqmin=1:lmin=1:dc=10:vstrict=0:aspect=16/9 Project.ts.avs -o Project.ts.avs.mpg -ofps 30000/1001 -of mpeg -mpegopts format=dvd:tsaf
mencoder.exe -mc 0 -noskip -ovc lavc -nosound -lavcopts threads=2:vcodec=mpeg2video:vrc_buf_size=1835:vrc_maxrate=9800:vbitrate=9668:keyint=15:vpass=3:trell:mbd=2:precmp=2:subcmp=2:cmp=2:dia=-10:predia=-10:cbp:mv0:vqmin=1:lmin=1:dc=10:vstrict=0:aspect=16/9 Project.ts.avs -o Project.ts.avs.mpg -ofps 30000/1001 -of mpeg -mpegopts format=dvd:tsaf
mencoder.exe -mc 0 -noskip -ovc lavc -oac lavc -lavcopts acodec=ac3:abitrate=128:threads=2:vcodec=mpeg2video:vrc_buf_size=1835:vrc_maxrate=9800:vbitrate=9668:keyint=15:vpass=3:trell:mbd=2:precmp=2:subcmp=2:cmp=2:dia=-10:predia=-10:cbp:mv0:vqmin=1:lmin=1:dc=10:vstrict=0:aspect=16/9 -srate 48000 -af lavcresample=48000 Project.ts.avs -o Project.ts.avs.mpg -ofps 30000/1001 -of mpeg -mpegopts format=dvd:tsaf

Here you could tweak the first and 2nd pass for better performance (remove CPU intensive switches). Also, vpass=3 isn’t a mistake for pass no. 2 (you can read mencoder’s manpage for more details). Now at this point if everything went fine with the encode you should end up with a Project.ts.avs.mpg file.

5. Time to create the DVD structure with DVDAuthor. This is as simple as this:

dvdauthor.exe -t -o PROJECT Project.ts.avs.mpg
dvdauthor.exe -T -o PROJECT

Here, PROJECT is an output folder containing the DVD structure (AUDIO_TS and VIDEO_TS folders). The -t switch creates the titleset and the -T switch creates the table of content (which contains only the single titleset created from -t).

6. The last step to create the DVD is to burn the disc. Just fire up ImgBurn and select Create image file from files/folders and under the Output menu select Device to burn the result to disc.

Et voil√†. Of course there is always a thousand ways of re-encoding a stream so keep in mind this is only one way of doing it… ūüôā

PPA Update

Yes, I have found some time (sorry, I meant “took some time”) to update my PPA. I’ve updated my x264 package to core 76 for now. You’ll find my PPA on launchpad.

I also want to update my other packages but first I need to better understand the process of updating an existing source package with a new upstream version using version control (git). Launchpad has lots of good resources on packaging software for Ubuntu (or any Debian based distro).

I found interesting links on packaging for Debian. However they all seem to be using their “own” way and there seems to be no “standard/official” way of doing it. There is also the vcs-pkg project that aims to further investigate the use of version control for packaging. Interesting.

Any good links aside those ones you would like to suggest?

Reference Counting Garbage Collection

One nice feature from the .NET world is garbage collection. You simply don’t need to worry (99% of the time) about memory allocation resulting from creating new objects. The .NET garbage collector detects when an object is no longer needed based on a few heuristics but mainly based on reference count.

Using the same technique while using C++ is possible, and even easy. Since programming is about reuse I opted to create a class that wraps existing objects to add the wanted feature. It looks like this:

CRefObject<TExisting>* p = CRefObject<TExisting>::CreateInstance(/* TExisting ctor args */);

CRefObject basically derives from TExisting to keep all the same functionality:

template<typename TBase, typename TActivator = CRefActivatorTraits<TBase>>
class CRefObject : public TBase
{

...

};

Keeping the reference count within the object itself is important as one objective is to pass objects between threads without having to worry about memory allocation. There are many smart pointers out there and many implement reference counting however I haven’t found one that keeps the reference count as an part of an object identity.

Implementing CRefObject<T> is quite straightforward: manage reference counting.

	ULONG AddRef()
	{
		return ::InterlockedIncrement(&m_lRefCount);
	}

	ULONG Release()
	{
		LONG lCount = ::InterlockedDecrement(&m_lRefCount);
		ATLASSERT(lCount >= 0);
		if (lCount == 0)
			TActivator::Destroy(this);

		return lCount;
	}

Now that we have reference counting we can upgrade existing objects and manage reference counting like this:

CRefObject<TExisting>* p = CRefObject<TExisting>::CreateInstance(/* TExisting ctor args */);
...
// When done with the object, release it
p->Release();

That looks like COM doesn’t it? Yes it does. And there is one more feature we can add to make it look even more like COM using ATL: using smart pointers. This makes it much easier to track the reference count. Welcome CRefAutoPtr<T>:

template<typename T>
class CRefAutoPtr
{
public:
	CRefAutoPtr() throw() :
		m_pT(NULL)
	{ }

	CRefAutoPtr(const CRefAutoPtr& sp) throw() :
		m_pT(sp.m_pT)
	{
		_AddRef();
	}

	explicit CRefAutoPtr(CRefObject* pT) throw() :
		m_pT(NULL)
	{
		_Attach(pT);
	}

	virtual ~CRefAutoPtr() throw()
	{
		_Release();
	}

	CRefAutoPtr& operator =(const CRefAutoPtr& sp) throw()
	{
		_Release();
		m_pT = sp.m_pT;
		_AddRef();
		return *this;
	}

	CRefObject* operator &() throw()
	{
		_AddRef();
		return m_pT;
	}

	operator CRefObject*() throw()
	{
		return operator&();
	}

	CRefObject* operator ->() const throw()
	{
		return m_pT;
	}

	bool operator !=(const CRefAutoPtr& sp) const throw()
	{
		return !operator==(sp);
	}

	bool operator ==(const CRefAutoPtr& sp) const throw()
	{
		return m_pT == sp.m_pT;
	}

	CRefObject* Detach() throw()
	{
		CRefObject* pT = m_pT;
		m_pT = NULL;
		return pT;
	}

protected:
	VOID _Attach(CRefObject* pT) throw()
	{
		_Release();
		m_pT = pT;
	}

	VOID _AddRef() throw()
	{
		CRefObject* pT = m_pT;
		if (pT)
			pT->AddRef();
	}

	VOID _Release() throw()
	{
		CRefObject* pT = m_pT;
		if (pT)
		{
			m_pT = NULL;
			pT->Release();
		}
	}

public:
	CRefObject* m_pT;
};

The whole implementation isn’t 100% complete, but its quite a good start.

Byte Formatting Revisited

If you read my last post you may have noticed both ConvertBytes and FormatBytes could benefit a few improvements. After thinking the implementation over again I made the following improvements.

  1. The biggest change is the order of the parameters in both functions; in order to allow for default parameters I moved the out parameters (pointers) to the beginning of the parameter list
  2. In ConvertBytes instead of linking the binary prefix to the exponent I now use a different variable for less confusion
  3. If the provided size is already below the base then the output binary prefix is set to BP_NONE
  4. I renamed iPrefix to iBinaryPrefix in both functions for consistency
  5. In FormatBytes I now use the new BP_NONE output prefix
  6. Formatting has been changed to 2 decimals instead of 3 (can be specified otherwise)
  7. An extra parameter has been added to FormatBytes for specifying whether the binary prefix is to appear in the fomatting (i.e. useful when formatting a range of bytes; e.g. 5.6-9.1 KB)
  8. Finally, FormatBytes now handles formatting provided bytes if below the base (i.e. using B)

Here’s what both functions now look like:

enum PREFIXNORM
{
	PN_SI,					// Base 10 (i.e. 1000)
	PN_IEC,					// Base 2 (i.e. 1024)
	PN_DECIMAL = PN_SI,
	PN_BINARY = PN_IEC
};

const INT iPrefixBase[] = { 1000, 1024 };

enum BINARYPREFIX
{
	BP_NONE = -2,
	BP_AUTO,
//	SI				IEC 60027	Exp (b10/b2)	Exp (b1000/b1024)
	BP_KILO,		// KIBI		3				1
	BP_MEGA,		// MEBI		6				2
	BP_GIGA,		// GIBI		9				3
	BP_TERA,		// TEBI		12				4
	BP_PETA,		// PEBI		15				5
	BP_EXA,			// EXBI		18				6
	BP_ZETTA,		// ZEBI		21				7
	BP_YOTTA		// YOBI		24				8
};

const LPTSTR pszPrefixes[][8] = {
	{ _T("KB"),  _T("MB"),  _T("GB"),  _T("TB"),  _T("PB"),  _T("EB"),  _T("ZB"),  _T("YB") },
	{ _T("KiB"), _T("MiB"), _T("GiB"), _T("TiB"), _T("PiB"), _T("EiB"), _T("ZiB"), _T("YiB") },
};

typedef DOUBLE *LPDOUBLE;
typedef BINARYPREFIX *LPBINARYPREFIX;

BOOL ConvertBytes(
	LPDOUBLE pdResult,
	LPBINARYPREFIX piBinaryPrefix,
	ULONGLONG ullBytes,
	BINARYPREFIX iBinaryPrefix = BP_AUTO,
	PREFIXNORM iPrefixNorm = PN_DECIMAL)
{
	if (!pdResult || !piBinaryPrefix)
		return FALSE;

	DOUBLE dSize = (DOUBLE)ullBytes;
	DOUBLE dBase = (FLOAT)iPrefixBase[iPrefixNorm];

	*pdResult = dSize;
	*piBinaryPrefix = iBinaryPrefix;

	if (iBinaryPrefix == BP_NONE)	// No conversion required
		return TRUE;

	DOUBLE dExponent = 0;

	if (iBinaryPrefix == BP_AUTO) // Try to find a suitable prefix
	{
		if (dSize < dBase)
		{
			*piBinaryPrefix = BP_NONE;
		}
		else
		{
			dExponent = ::floor(::log(dSize) / ::log(dBase));
			*piBinaryPrefix = (BINARYPREFIX)((INT)dExponent - 1);
		}
	}
	else					// Use provided prefix
	{
		dExponent = iBinaryPrefix + 1;
	}

	*pdResult = dSize / ::pow(dBase, dExponent);

	return TRUE;
}

CString FormatBytes(
	LPBINARYPREFIX piBinaryPrefix,
	ULONGLONG ullBytes,
	BINARYPREFIX iBinaryPrefix = BP_AUTO,
	PREFIXNORM iPrefixNorm = PN_SI,
	INT iDecimals = 2,
	BOOL bFormatPrefix = TRUE)
{
	DOUBLE dResult;
	BOOL bRes = ::ConvertBytes(&dResult, piBinaryPrefix, ullBytes, iBinaryPrefix, iPrefixNorm);
	ATLASSERT(bRes);

	if (*piBinaryPrefix == BP_NONE || ullBytes < iPrefixBase[iPrefixNorm])
		iDecimals = 0;

	CString szFormat, szResult;
	szFormat.Format(_T("%%.%df"), iDecimals);

	if (bFormatPrefix)
	{
		if (*piBinaryPrefix == BP_NONE)
			szFormat.Append(_T(" B"));
		else
			szFormat.AppendFormat(_T(" %s"), pszPrefixes[iPrefixNorm][*piBinaryPrefix]);
	}

	szResult.Format(szFormat, dResult);

	return szResult;
}

If you ever find good use of these functions please let me know. I personally use them quite often, hopefully they will be as useful for someone else. Oh, if you wonder how to format a range of bytes this is how to do it:

                DOUBLE dMinSize = 123801, dMaxSize = 92873429;
		BINARYPREFIX ibp = BP_AUTO;
		CString szMaxSize(::FormatBytes(&ibp, dMaxSize, ibp, PN_DECIMAL, 1, TRUE));
		CString szMinSize(::FormatBytes(&ibp, dMinSize, ibp, PN_DECIMAL, 1, FALSE));
		CString szSize;
		szSize.Format(_T("%s-%s"), szMinSize, szMaxSize);
                ...

It is important to note that you might be tempted to do:

                DOUBLE dMinSize = 123801, dMaxSize = 92873429;
                BINARYPREFIX ibp = BP_AUTO;
                CString szSize;
                szSize.Format(_T("%s-%s"),
                                ::FormatBytes(&ibp, dMaxSize, ibp, PN_DECIMAL, 1, TRUE),
                                ::FormatBytes(&ibp, dMinSize, ibp, PN_DECIMAL, 1, FALSE));
                ...

However doing so will most likely result in a wrong output because of the parameter evaluation strategy which might differ from one compiler to another.

Byte Formatting

Now that we know all about the binary prefix origins the next question is how do we format bytes? The answer is not as straightforward as it should be.

This proposed implementation offers 2 options. One is to auto-select the prefix based on the number of bytes and the other is to use a specific prefix. There are two core operations involved in formatting bytes, one is to convert the byte size to the requested unit and the other is to format the converted size into a string, managing decimals.

To help the implementation we need to define a few things. First, will the conversion follow the SI or IEC standard? Then which unit are we making the conversion to? Finally, we need to define the prefixes themselves. To make this all fit together we make the prefix array match the definition of the used norm and the selected unit:

enum PREFIXNORM
{
   PN_SI,                    // Base 10 (i.e. 1000)
   PN_IEC,                   // Base 2 (i.e. 1024)
   PN_DECIMAL = PN_SI,
   PN_BINARY = PN_IEC
};

const INT iPrefixBase[] = { 1000, 1024 };

enum BINARYPREFIX
{
   BP_NONE = -2,
   BP_AUTO,
// SI              IEC 60027      Exp (b10/b2)     Exp (b1000/b1024)
   BP_KILO,        // KIBI        3                1
   BP_MEGA,        // MEBI        6                2
   BP_GIGA,        // GIBI        9                3
   BP_TERA,        // TEBI        12               4
   BP_PETA,        // PEBI        15               5
   BP_EXA,         // EXBI        18               6
   BP_ZETTA,       // ZEBI        21               7
   BP_YOTTA        // YOBI        24               8
};

typedef BINARYPREFIX *LPBINARYPREFIX;

const LPTSTR pszPrefixes[][8] = {
   { _T("KB"),  _T("MB"),  _T("GB"),  _T("TB"),  _T("PB"),  _T("EB"),  _T("ZB"),  _T("YB") },
   { _T("KiB"), _T("MiB"), _T("GiB"), _T("TiB"), _T("PiB"), _T("EiB"), _T("ZiB"), _T("YiB") }
};

To do the actual conversion we need the byte size, the norm to use and the unit to convert to. The function returns the converted byte size and the unit used (if BP_AUTO is specified):

BOOL ConvertBytes(
   ULONGLONG ullSize,
   PREFIXNORM iPrefixNorm,
   BINARYPREFIX iPrefix,
   LPDOUBLE pdResult,
   LPBINARYPREFIX piPrefix)
{
   if (!pdResult || !piPrefix)
       return FALSE;

   DOUBLE dSize = (DOUBLE)ullSize;
   DOUBLE dBase = (FLOAT)iPrefixBase[iPrefixNorm];

   *pdResult = dSize;
   *piPrefix = BP_NONE;

   if (dSize < dBase)    // Already in bytes?
       return TRUE;

   if (iPrefix == BP_AUTO)
       *piPrefix = (BINARYPREFIX)((INT)::floor(::log(dSize) / ::log(dBase)) - 1);
   else
       *piPrefix = iPrefix;

   *pdResult = dSize / ::pow(dBase, (DOUBLE)(*piPrefix) + 1);

   return TRUE;
}

As you may have noticed the auto conversion is flawed. Indeed the maximum precision of a double allows to convert reliably only up to a terabyte. The conversion is still valid with higher values however the auto unit selection will diverge away from the multiple of the base as the number of bytes increases. For example converting the following values:

ULONGLONG ullSizes[] = {
   999ULL,
   1000ULL,                // 1K
   999999ULL,
   1000000ULL,             // 1M
   999999999ULL,
   1000000000ULL,          // 1G
   999999999999ULL,
   1000000000000ULL,       // 1T
   999999999999999ULL,
   1000000000000000ULL,    // 1P
   999999999999999999ULL,
   1000000000000000000ULL, // 1E
   // ...
};

Yields to these results (formatted with %.9f):

999.000000000 B
1.000000000 KB
999.999000000 KB
1.000000000 MB
999.999999000 MB
1.000000000 GB
999.999999999 GB
1.000000000 TB
1.000000000 PB // Should be 999­.999999999 TB but rounded to 1 PB
1.000000000 PB
1.000000000 EB // Should be 999.999999999 PB but rounded to 1 EB
1.000000000 EB

Now formatting the converted size into a readable string is a matter of using the prefix array:

CString FormatBytes(
    ULONGLONG ullBytes,
    INT iDecimals = 3,
    BINARYPREFIX iPrefix = BP_AUTO,
    PREFIXNORM iPrefixNorm = PN_SI)
{
    CString szResult;

    if (iPrefix == BP_NONE)
    {
        szResult.Format(_T("%I64u B"), ullBytes);
    }
    else
    {
        DOUBLE dResult;
        BOOL bRes = ::ConvertBytes(ullBytes, iPrefixNorm, iPrefix, &dResult, &iPrefix);
        ATLASSERT(bRes);

        CString szFormat;
        szFormat.Format(_T("%%.%df %%s"), iDecimals);
        szResult.Format(szFormat, dResult, pszPrefixes[iPrefixNorm][iPrefix]);
    }

    return szResult;
}

Finally, using the formatting is as simple as this:

::_tprintf(_T("%s\n"), ::FormatBytes(357913941));

To give this output:

357.914 MB

Binary Prefix Origins

How about having fun facts about the binary prefix mess? But why, you might wonder. Well it all started when I wanted to convert 1 megabyte into bytes. Easy you might say, simply multiply by 1000000 and you’re done! Yes, it should be that easy however its not. Why? Ahh the big question. Before answering that question, we need some background info.

Find 3 problems with this appellation: 1.44MB 3,5 inch floppy disk.

  1. The capacity, originally described as 1¬†440 kB (kilobytes) before being ‚Äútranslated‚ÄĚ to 1,44 MB, is in fact a little over 2 % inaccurate because of the double misuse of a decimal prefix
  2. The physical size is 90 mm, not 3,5 inches
  3. The word floppy no longer really applies as it did to the 5,25 inch predecessor

Considering how perfectionist computer scientists should be its quite amazing to realize how they can screw up that bad in such a short statement. Be reassured, the blame is to be shared by many other parties.

So, now that we know how bad things are, how can we convert 1 megabyte into bytes? Well, it all depends (heard that answer before? If not, then ask that question to your nearest geek friend).

According to the SI standard everything is 10 base so 1MB is 1000000 bytes. If you want things to be 2 base then you should follow the IEC 60027-2 standard and say 1MiB for 1048576 bytes.¬† The capital “B” is not part of the standard, its referred to as bytes while lower case “b” is referred to as bits (8 bits for 1 byte). Easy enough?

Fore more info, check out Wikipedia.

The ::MessageBox Case

So lets say you’re popping up a message box in your application like this:

::MessageBox(m_hWnd, _T("Text"), _T("Caption"), MB_ICONINFORMATION | MB_OK);

Fair enough. However there is one problem, where does the window popup?

Well, Windows defaults to popping it up in the center of the default screen. This means if your application window was moved to a corner, or even worst, to another screen and your application happens to pop a message while the focus is on the other screen you’ll get the popup on wrong screen! Talk about confusing a user.

So, whats our solution? Well, Microsoft’s Support KB as an article to provide a workaround in Visual Basic using a Windows CBT hook. You basically create a one time hook to position the window before it appears then remove the hook right away. That works. However, you have to install that hook everytime before calling the MessageBox function.

To ease the process someone on The Code Project also posted an article to streamline the process using C#.

Here’s what it could look like in C++:


class CCenterWindow
{
public:
	CCenterWindow(HWND hWnd)
	{
		m_hWnd = hWnd;
		m_hHook = ::SetWindowsHookEx(
			WH_CBT,
			&CCenterWindow::HookProc,
			(HINSTANCE)::GetWindowLong(m_hWnd, GWL_HINSTANCE),
			::GetCurrentThreadId());
	}

	static LRESULT CALLBACK HookProc(INT nCode, WPARAM wParam, LPARAM lParam)
	{
		if (nCode == HCBT_ACTIVATE)
		{
			RECT r1, r2;
			LONG x, y;
			::GetWindowRect(m_hWnd, &r1);
			::GetWindowRect((HWND)wParam, &r2);
			x = (r1.left + (r1.right - r1.left) / 2) - ((r2.right - r2.left) / 2);
			y = (r1.top + (r1.bottom - r1.top) / 2) - ((r2.bottom - r2.top) / 2);
			::SetWindowPos((HWND)wParam, NULL, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
			::UnhookWindowsHookEx(m_hHook);
		}

		return 0;
	}

private:
	static HWND m_hWnd;
	static HHOOK m_hHook;
};

So now you can center a message box by doing this:

CCenterWindow cw(m_hWnd);
::MessageBox(m_hWnd, _T("Text"), _T("Caption"), MB_ICONINFORMATION | MB_OK);

And the hook being installed/uninstalled will do the work of centering the window. Of course, you can compact things even more by creating a macro.