#include <windows.h>
#include <vfw.h>
#include <stdio.h>

#define VERSION "1.06"
/*
1.06: option to write extended WAV header with channel mask (thanks to tebasuna51)
1.05: performance optimization 
1.04: added support for floating-point samples (thanks to tebasuna51)
1.03: fixed size field in data chunk header (thanks to tebasuna51)
1.03: don't expect that AVIStreamRead stops at the end of the stream (thanks to tebasuna51)
*/

#define WAVE_FORMAT_IEEE_FLOAT 3

DWORD WINAPI FileWriterThread(LPVOID param);
VOID CleanupAndExit(UINT exitCode);
BOOL CtrlHandler(DWORD ctrlType);

#define BUFFER_SIZE 0x20000
#define DEFAULT_MASK_COUNT 9
static DWORD DEFAULT_MASK[9] = {0, 4, 3, 259, 51, 55, 63, 319, 255};

LPSTR         inputFile     = NULL;
LPSTR         outputFile    = NULL;
BOOL          rawOutput     = FALSE;
BOOL          wavexOutput   = FALSE;
DWORD         channelMask   = 0xFFFFFFFF;
PAVIFILE      aviFile       = NULL;
BOOL          lookForStream = TRUE;
LONG          streamID      = 0;
PAVISTREAM    aviStream     = NULL;
AVISTREAMINFO streamInfo;
WAVEFORMATEX  waveFormat;
LONG          formatSize    = sizeof(WAVEFORMATEX);
DWORD         sampleSize;
BOOL          hasSomeAudio  = FALSE;
DWORDLONG     fileSize;
DWORD         samplesInBuffer;
BYTE          wavHeader[68];
DWORD         headerPos = 0;
BOOL          toStdOut;
HANDLE        outputHandle;
HANDLE        readEvent = NULL;
HANDLE        writeEvent = NULL;
HANDLE        writerThread = NULL;
DWORD         writerThreadID;
int           readBuffer = 0;
int           writeBuffer = 0;
LPBYTE        buffer[2][BUFFER_SIZE];
DWORD         bytesToWrite[2];
DWORD         bytesWritten;
DWORD         samplesRead;
DWORD         nextSample = 0;
BOOL          userBreak = FALSE;

int main(int argc, char * argv[])
{
	AVIFileInit();
	SetConsoleCtrlHandler((PHANDLER_ROUTINE) CtrlHandler, TRUE);
	
	if (argc < 2) {
		fprintf(stderr, 
			"WAVI v" VERSION " - (c) 2007 Tamas Kurucsai, with lots of help from tebasuna51\n"
			"Licensed under the terms of the GNU General Public License.\n"
			"\n"
			"This utility extracts the first uncompressed PCM audio track\n"
			"from an AVI file and saves it to a WAV file. This is not quite\n"
			"useful for most AVI files since they usually contain some kind\n"
			"of compressed audio, but it can come handy when it's needed to\n"
			"save an audio track from AviSynth.\n"
			"\n"
			"Usage: WAVI <avi-file> [ <wav-file> [ /R | /X | /M <mask> ] ]\n"
			"\n"
			"If <avi-file> is a valid AVI file which contains PCM audio,\n"
			"the audio track will be written to <wav-file> as a WAV file.\n"
			"If '-' is passed as <wav-file>, the WAV file will be written\n"
			"to the standard output.\n"
			"If <wav-file> is not given, only information will be printed\n"
			"about the audio track.\n"
			"\n"
			"On success, the exit code will be 0 and the first line printed\n"
			"to the standard error will look like the following:\n"
			"\n"
			"Found PCM audio: <c> channels, <r> Hz, <b> bits, <l> seconds.\n"
			"\n"
			"where <c> is the number of audio channels, <r> is the sampling\n"
			"rate, <b> is the number of bits per sample and <l> is the length\n"
			"of the track in seconds.\n"
			"\n"
			"If the audio track contains floating-point samples, the next line\n"
			"printed to the standard error will be:\n"
			"Audio track contains floating-point samples.\n"
			"\n"
			"If an error occurs, the exit code will be 1 and some useful error\n"
			"message will be printed to the standard error.\n"
			"\n"
			"WAV files larger than 4 GB may be created. However, such WAV files\n"
			"are non-standard and may not be handled correctly by some players\n"
			"and encoders. A warning will be printed to the standard error when\n"
			"such a WAV file is created.\n"
			"\n"
			"WAVI accepts the following options:\n"
			"\n"
			"/R - Write a raw file of samples without the WAV header.\n"
			"\n"
			"/X - Write an extended WAV header containing the default\n"
			"     channel mask for multi-channel audio.\n"
			"\n"
			"/M <mask> - Write an extended WAV header containing the\n"
			"     specified channel mask for multi-channel audio.\n"
			"\n"
			"The default channel masks are:\n"
			"\n"
			"Mask  Chan. MS channels                Description\n"
			"----  ----- -------------------------  ----------------\n"
			"   4   1    FC                         Mono\n"
			"   3   2    FL FR                      Stereo\n"
			" 259   3    FL FR BC                   First Surround\n"
			"  51   4    FL FR BL BR                Quadro\n"
			"  55   5    FL FR FC BL BR             like Dpl II (without LFE)\n"
			"  63   6    FL FR FC LF BL BR          Standard Surround\n"
			" 319   7    FL FR FC LF BL BR BC       With back center\n"
			" 255   8    FL FR FC LF BL BR FLC FRC  With front center left/right\n"
			"\n"
			"Some other common channel masks:\n"
			"\n"
			"Mask  Chan. MS channels                Description\n"
			"----  ----- -------------------------  ----------------\n"
			"   7   3    FL FR FC\n"
			" 263   4    FL FR FC BC                like Dpl I\n"
			" 271   5    FL FR FC BC LF\n"
			"  59   5    FL FR BL BR LF\n"
			);
		CleanupAndExit(1);
        } else {
        	int actarg = 0;
        	BOOL maskComing = FALSE;
        	while (argv[++actarg]) {
        		if ((argv[actarg][0] == '-' || argv[actarg][0] == '/') && argv[actarg][1]) {
        			if (maskComing) {
					fprintf(stderr, "Error: Channel mask expected after %s.\n", argv[actarg - 1]);
					CleanupAndExit(1);
				}
        			switch (argv[actarg][1]) {
        			case 'r':
        			case 'R':
        				rawOutput = TRUE;
        				break;
        			case 'm':
        			case 'M':
        				maskComing = TRUE;
        			case 'x':
        			case 'X':
        				wavexOutput = TRUE;
        				break;
        			default:
        				fprintf(stderr, "Error: Invalid option %s.\n", argv[actarg]);
					CleanupAndExit(1);
					break;
				}
        		} else {
        			if (maskComing) {
        				channelMask = atoi(argv[actarg]);
        				if (! channelMask) {
						fprintf(stderr, "Error: Channel mask expected after %s.\n", argv[actarg - 1]);
						CleanupAndExit(1);
					}
        				maskComing = FALSE;
        			} else if (! inputFile) {
        				inputFile = argv[actarg];
        			} else if (! outputFile) {
        				outputFile = argv[actarg];
        			} else {
					fprintf(stderr, "Error: Too many parameters.\n");
					CleanupAndExit(1);
        			}
        		}
        	}
		if (maskComing) {
			fprintf(stderr, "Error: Channel mask expected after %s.\n", argv[actarg - 1]);
			CleanupAndExit(1);
		} else if (! inputFile) {
			fprintf(stderr, "Error: Missing input file.\n");
			CleanupAndExit(1);
		}
	}
	if (userBreak) CleanupAndExit(1);
	
	if (AVIFileOpen(& aviFile, inputFile, OF_READ | OF_SHARE_DENY_WRITE, NULL)) {
		fprintf(stderr, "Error: Could not open AVI file \"%s\".\n", inputFile);
		CleanupAndExit(1);
	}
	if (userBreak) CleanupAndExit(1);
	while (lookForStream) {
		if (! AVIFileGetStream(aviFile, & aviStream, 0, streamID)) {
			if (! AVIStreamInfo(aviStream, & streamInfo, sizeof(AVISTREAMINFO))) {
				if (streamInfo.fccType == streamtypeAUDIO) {
					hasSomeAudio = TRUE;
					if (! AVIStreamReadFormat(aviStream, 0, & waveFormat, & formatSize)) {
						if (waveFormat.wFormatTag == WAVE_FORMAT_PCM || waveFormat.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
							if (! AVIStreamSampleSize(aviStream, 0, & sampleSize)) {
								lookForStream = FALSE;
							}
						}
					}
				}
			}
			if (lookForStream) {
				AVIStreamRelease(aviStream);
				aviStream = NULL;
				streamID++;
			}
		} else {
			lookForStream = FALSE;
		}
		if (userBreak) CleanupAndExit(1);
	}
	if (! aviStream) {
		fprintf(stderr, "Error: Could not find PCM audio track in \"%s\".\n", inputFile);
		if (hasSomeAudio) {
			fprintf(stderr, "(However, the file contains an audio track of another format.)\n");
		}
		CleanupAndExit(1);
	} 
    	fprintf(stderr, "Found PCM audio: %d channels, %d Hz, %d bits, %f seconds.\n", 
		waveFormat.nChannels, waveFormat.nSamplesPerSec, waveFormat.wBitsPerSample,
		1.0 * streamInfo.dwLength * streamInfo.dwScale / streamInfo.dwRate);
	if (waveFormat.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
		fprintf(stderr, "Audio track contains floating-point samples.\n");
	}
		
	if (outputFile) {
		fileSize = UInt32x32To64(sampleSize, streamInfo.dwLength);
		if (fileSize > 0xFFFFFFFF - (wavexOutput ? 60 : 36)) {
			fprintf(stderr, "Warning: WAV file will be larger than 4 GB.\n");
			fileSize = sampleSize * ((0xFFFFFFFF - (wavexOutput ? 60 : 36)) / sampleSize);
		}
		samplesInBuffer = BUFFER_SIZE / sampleSize;
		if (! samplesInBuffer) {
			fprintf(stderr, "Error: Too large samples; audio track is definitely invalid.\n");
			CleanupAndExit(1);
		}
		if (wavexOutput && (channelMask == 0xFFFFFFFF)) {
			if (waveFormat.nChannels < DEFAULT_MASK_COUNT) {
				channelMask = DEFAULT_MASK[waveFormat.nChannels];
			} else {
				channelMask = (1 << waveFormat.nChannels) - 1;
			}
		}

		if (! rawOutput) {
			wavHeader[headerPos++] = 'R';
			wavHeader[headerPos++] = 'I';
			wavHeader[headerPos++] = 'F';
			wavHeader[headerPos++] = 'F';
			*(LPDWORD)(wavHeader + headerPos) = (DWORD) fileSize + (wavexOutput ? 60 : 36);
			headerPos += 4;
			wavHeader[headerPos++] = 'W';
			wavHeader[headerPos++] = 'A';
			wavHeader[headerPos++] = 'V';
			wavHeader[headerPos++] = 'E';
			wavHeader[headerPos++] = 'f';
			wavHeader[headerPos++] = 'm';
			wavHeader[headerPos++] = 't';
			wavHeader[headerPos++] = ' ';
			*(LPDWORD)(wavHeader + headerPos) = (wavexOutput ? 40 : 16);
			headerPos += 4;
			*(LPWORD)(wavHeader + headerPos)  = (wavexOutput ? 0xFFFE : waveFormat.wFormatTag);
			headerPos += 2;
			*(LPWORD)(wavHeader + headerPos)  = waveFormat.nChannels;
			headerPos += 2;
			*(LPDWORD)(wavHeader + headerPos) = waveFormat.nSamplesPerSec;
			headerPos += 4;
			*(LPDWORD)(wavHeader + headerPos) = waveFormat.nAvgBytesPerSec;
			headerPos += 4;
			*(LPWORD)(wavHeader + headerPos)  = waveFormat.nBlockAlign;
			headerPos += 2;
			*(LPWORD)(wavHeader + headerPos)  = waveFormat.wBitsPerSample;
			headerPos += 2;
                        if (wavexOutput) {
                             *(LPWORD)(wavHeader + headerPos)  = 22;
                             headerPos += 2;
                             *(LPWORD)(wavHeader + headerPos)  = waveFormat.wBitsPerSample;
                             headerPos += 2;
                             *(LPDWORD)(wavHeader + headerPos) = channelMask;
                             headerPos += 4;
                             *(LPWORD)(wavHeader + headerPos)  = waveFormat.wFormatTag;
                             headerPos += 2;
                             *(LPWORD)(wavHeader + headerPos)  = 0x0000;
                             headerPos += 2;
                             *(LPDWORD)(wavHeader + headerPos) = 0x00100000;
                             headerPos += 4;
                             *(LPDWORD)(wavHeader + headerPos) = 0x00AA8000;
                             headerPos += 4;
                             *(LPDWORD)(wavHeader + headerPos) = 0x9B713800;
                             headerPos += 4;
                        }
			wavHeader[headerPos++] = 'd';
			wavHeader[headerPos++] = 'a';
			wavHeader[headerPos++] = 't';
			wavHeader[headerPos++] = 'a';
			*(LPDWORD)(wavHeader + headerPos) = (DWORD) fileSize;
			headerPos += 4;
		}

		toStdOut = ! strcmp(outputFile, "-");
		if (toStdOut) {
			outputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
			fprintf(stderr, "Writing WAV file...\n");
		} else {
			outputHandle = CreateFile(outputFile, GENERIC_WRITE, FILE_SHARE_READ, 
				NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
			if (outputHandle == INVALID_HANDLE_VALUE) {
				fprintf(stderr, "Error: Could not create file \"%s\".\n", outputFile);
				CleanupAndExit(1);
			}
			fprintf(stderr, "Writing WAV file \"%s\"...\n", outputFile);
		}
			
		if (headerPos) {
			if (! WriteFile(outputHandle, wavHeader, headerPos, & bytesWritten, NULL) || (bytesWritten != headerPos)) {
				if (toStdOut) {
					fprintf(stderr, "Error: Could not write to the standard output.\n");
				} else {
					fprintf(stderr, "Error: Could not write file \"%s\".\n", outputFile);
					CloseHandle(outputHandle);
				}
				CleanupAndExit(1);
			}
		}
		
		readEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
		writeEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
		writerThread = CreateThread(NULL, 0, FileWriterThread, NULL, CREATE_SUSPENDED, & writerThreadID);
		if (! readEvent || ! writeEvent || ! writerThread) {
			fprintf(stderr, "Error: An internal error occured.\n");
			if (! toStdOut) CloseHandle(outputHandle);
			CleanupAndExit(1);
		}
		ResumeThread(writerThread);
		
		while (nextSample < streamInfo.dwLength) {
			if (userBreak) CleanupAndExit(1);
			WaitForSingleObject(readEvent, INFINITE);
			if (userBreak) CleanupAndExit(1);
			if (streamInfo.dwLength - nextSample < samplesInBuffer) {
				samplesInBuffer = streamInfo.dwLength - nextSample;
			}
			bytesToWrite[readBuffer] = 0;
			if (AVIStreamRead(aviStream, nextSample, samplesInBuffer, & buffer[readBuffer], BUFFER_SIZE, 
			                  & bytesToWrite[readBuffer], & samplesRead) || (! bytesToWrite[readBuffer])) {
				fprintf(stderr, "Error: Could not read audio data from \"%s\".\n", inputFile);
				if (! toStdOut) CloseHandle(outputHandle);
				CleanupAndExit(1);
			}
			SetEvent(writeEvent);
			nextSample += samplesRead;
			readBuffer = 1 - readBuffer;
		}
		if (userBreak) CleanupAndExit(1);
		WaitForSingleObject(readEvent, INFINITE);
		if (userBreak) CleanupAndExit(1);
		bytesToWrite[readBuffer] = 0;
		SetEvent(writeEvent);
		WaitForSingleObject(writerThread, INFINITE);
		
		if (! toStdOut && ! CloseHandle(outputHandle)) {
			fprintf(stderr, "Error: Could not write file \"%s\".\n", outputFile);
			CleanupAndExit(1);
		}
		
		fprintf(stderr, "WAV file written successfully.\n");
	}
	
	CleanupAndExit(0);
	return 0;
}

DWORD WINAPI FileWriterThread(LPVOID Param)
{
	while (42) {
		WaitForSingleObject(writeEvent, INFINITE);
		if (bytesToWrite[writeBuffer]) {
			if (! WriteFile(outputHandle, buffer[writeBuffer], bytesToWrite[writeBuffer], & bytesWritten, NULL) 
			    || (bytesWritten != bytesToWrite[writeBuffer])) {
				if (toStdOut) {
					fprintf(stderr, "Error: Could not write to the standard output.\n");
				} else {
					fprintf(stderr, "Error: Could not write file \"%s\".\n", outputFile);
					CloseHandle(outputHandle);
				}
				CleanupAndExit(1);
			}
			bytesToWrite[writeBuffer] = 0;
			SetEvent(readEvent);
			writeBuffer = 1 - writeBuffer;
		} else {
			return 0;
		}
	} 
}

VOID CleanupAndExit(UINT exitCode)
{
	if (writerThread) CloseHandle(writerThread);
	if (writeEvent)   CloseHandle(writeEvent);
	if (readEvent)    CloseHandle(readEvent);
	if (buffer)       LocalFree((HLOCAL) buffer);
	if (aviStream)    AVIStreamRelease(aviStream);
	if (aviFile)      AVIFileRelease(aviFile);
	AVIFileExit();
	ExitProcess(exitCode);
}

BOOL CtrlHandler(DWORD ctrlType)
{
	switch (ctrlType) 
	{
	case CTRL_C_EVENT:
	case CTRL_BREAK_EVENT:
		if (! userBreak) {
			fprintf(stderr, "User break.\n");
			userBreak = TRUE;
		} else {
			fprintf(stderr, "Please wait a bit until everything is cleaned up.\n");
		}
		return TRUE;
	default:
		return TRUE;
	}
}
