/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Apple II cassette I/O functions.
 */
#include "StdAfx.h"
#include "CassetteDialog.h"
#include "CassImpTargetDialog.h"
#include "HelpTopics.h"
#include "GenericArchive.h"
#include "Main.h"
#include "../diskimg/DiskImg.h"		// need kStorageSeedling
#include <math.h>

/*
 * Tape layout:
 *	10.6 seconds of 770Hz (8192 cycles * 1300 usec/cycle)
 *	1/2 cycle at 400 usec/cycle, followed by 1/2 cycle at 500 usec/cycle
 *	Data, using 500 usec/cycle for '0' and 1000 usec/cycle for '1'
 *	There is no "end" marker, except perhaps for the absence of data
 *
 * The last byte of data is an XOR checksum (seeded with 0xff).
 *
 * BASIC uses two sections, each with the full 10-second lead-in and a
 * checksum byte).  Integer BASIC writes a two-byte section with the length
 * of the program, while Applesoft BASIC writes a three-byte section with
 * the length followed by a one-byte "run" flag (seen: 0x55 and 0xd5).
 *
 * Applesoft arrays, loaded with "RECALL", have a three-byte header, and
 * may be confused with BASIC programs.  Shape tables, loaded with "SHLOAD",
 * have a two-byte header and may be confused with Integer programs.
 *
 * The monitor ROM routine uses a detection threshold of 700 usec to tell
 * the difference between 0s and 1s.  When reading, it *outputs* a tone for
 * 3.5 seconds before listening.  It doesn't try to detect the 770Hz tone,
 * just waits for something under (40*12=)440 usec.
 *
 * The Apple II hardware changes the high bit read from $c060 every time it
 * detects a zero-crossing on the cassette input.  I assume the polarity
 * of the input signal is reflected by the polarity of the high bit, but
 * I'm not sure, and in the end it doesn't really matter.
 *
 * Typical instructions for loading data from tape look like this:
 *	- Type "LOAD" or "xxxx.xxxxR", but don't hit <return>.
 *	- Play tape until you here the tone.
 *	- Immediately hit stop.
 *	- Plug the cable from the Apple II into the tape player.
 *	- Hit "play" on the recorder, then immediately hit <return>.
 *	- When the Apple II beeps, it's done.  Stop the tape.
 *
 * How quickly do we need to sample?  The highest frequency we expect to
 * find is 2KHz, so anything over 4KHz should be sufficient.  However, we
 * need to be able to resolve the time between zero transitions to some
 * reasonable resolution.  We need to tell the difference between a 650usec
 * half-cycle and a 200usec half-cycle for the start, and 250/500usec for
 * the data section.  Our measurements can comfortably be off by 200 usec
 * with no ill effects on the lead-in, assuming a perfect signal.  (Sampling
 * every 200 usec would be 5Hz.)  The data itself needs to be +/- 125usec
 * for half-cycles, though we can get a little sloppier if we average the
 * error out by combining half-cycles.
 *
 * The signal is less than perfect, sometimes far less, so we need better
 * sampling to avoid magnifying distortions in the signal.  If we sample
 * at 22.05KHz, we could see a 650usec gap as 590, 635, or 680, depending
 * on when we sample and where we think the peaks lie.  We're off by 15usec
 * before we even start.  We can reasonably expect to be off +/- twice the
 * "usecPerSample" value.  At 8KHz, that's +/- 250usec, which isn't
 * acceptable.  At 11KHz we're at +/- 191usec, which is scraping along.
 *
 * We can get mitigate some problems by doing an interpolation of the
 * two points nearest the zero-crossing, which should give us a more
 * accurate fix on the zero point than simply choosing the closest point.
 * This does potentially increase our risk of errors due to noise spikes at
 * points near the zero.  Since we're reading from cassette, any noise spikes
 * are likely to be pretty wide, so averaging the data or interpolating
 * across multiple points isn't likely to help us.
 *
 * Some tapes seem to have a low-frequency distortion that amounts to a DC
 * bias when examining a single sample.  Timing the gaps between zero
 * crossings is therefore not sufficient unless we also correct for the
 * local DC bias.  In some cases the recorder or media was unable to
 * respond quickly enough, and as a result 0s have less amplitude
 * than 1s.  This throws off some simple correction schemes.
 *
 * The easiest approach is to figure out where one cycle starts and stops, and
 * use the timing of the full cycle.  This gets a little ugly because the
 * original output was a square wave, so there's a bit of ringing in the
 * peaks, especially the 1s.  Of course, we have to look at half-cycles
 * initially, because we need to identify the first "short 0" part.  Once
 * we have that, we can use full cycles, which distributes any error over
 * a larger set of samples.
 *
 * In some cases the positive half-cycle is longer than the negative
 * half-cycle (e.g. reliably 33 samples vs. 29 samples at 48KHz, when
 * 31.2 is expected for 650us).  Slight variations can lead to even
 * greater distortion, even though the timing for the full signal is
 * within tolerances.  This means we need to accumulate the timing for
 * a full cycle before making an evaluation, though we still need to
 * examine the half-cycle timing during the lead-in to catch the "short 0".
 *
 * Because of these distortions, 8-bit 8KHz audio is probably not a good
 * idea.  16-bit 22.05KHz sampling is a better choice for tapes that have
 * been sitting around for 25-30 years.
 */
/*
; Monitor ROM dump, with memory locations rearranged for easier reading.

; Increment 16-bit value at 0x3c (A1) and compare it to 16-bit value at
;  0x3e (A2). Returns with carry set if A1 >= A2.
; Requires 26 cycles in common case, 30 cycles in rare case.
FCBA: A5 3C     709  NXTA1    LDA   A1L        ;INCR 2-BYTE A1.
FCBC: C5 3E     710           CMP   A2L
FCBE: A5 3D     711           LDA   A1H        ;  AND COMPARE TO A2
FCC0: E5 3F     712           SBC   A2H
FCC2: E6 3C     713           INC   A1L        ;  (CARRY SET IF >=)
FCC4: D0 02     714           BNE   RTS4B
FCC6: E6 3D     715           INC   A1H
FCC8: 60        716  RTS4B    RTS

; Write data from location in A1L up to location in A2L.
FECD: A9 40     975  WRITE    LDA   #$40
FECF: 20 C9 FC  976           JSR   HEADR      ;WRITE 10-SEC HEADER
; Write loop.  Continue until A1 reaches A2.
FED2: A0 27     977           LDY   #$27
FED4: A2 00     978  WR1      LDX   #$00
FED6: 41 3C     979           EOR   (A1L,X)
FED8: 48        980           PHA
FED9: A1 3C     981           LDA   (A1L,X)
FEDB: 20 ED FE  982           JSR   WRBYTE
FEDE: 20 BA FC  983           JSR   NXTA1
FEE1: A0 1D     984           LDY   #$1D
FEE3: 68        985           PLA
FEE4: 90 EE     986           BCC   WR1
; Write checksum byte, then beep the speaker.
FEE6: A0 22     987           LDY   #$22
FEE8: 20 ED FE  988           JSR   WRBYTE
FEEB: F0 4D     989           BEQ   BELL

; Write one byte (8 bits, or 16 half-cycles).
; On exit, Z-flag is set.
FEED: A2 10     990  WRBYTE   LDX   #$10
FEEF: 0A        991  WRBYT2   ASL
FEF0: 20 D6 FC  992           JSR   WRBIT
FEF3: D0 FA     993           BNE   WRBYT2
FEF5: 60        994           RTS

; Write tape header.  Called by WRITE with A=$40, READ with A=$16.
; On exit, A holds $FF.
; First time through, X is undefined, so we may get slightly less than
;  A*256 half-cycles (i.e. A*255 + X).  If the carry is clear on entry,
;  the first ADC will subtract two (yielding A*254+X), and the first X
;  cycles will be "long 0s" instead of "long 1s".  Doesn't really matter.
FCC9: A0 4B     717  HEADR    LDY   #$4B       ;WRITE A*256 'LONG 1'
FCCB: 20 DB FC  718           JSR   ZERDLY     ;  HALF CYCLES
FCCE: D0 F9     719           BNE   HEADR      ;  (650 USEC EACH)
FCD0: 69 FE     720           ADC   #$FE
FCD2: B0 F5     721           BCS   HEADR      ;THEN A 'SHORT 0'
; Fall through to write bit.  Note carry is clear, so we'll use the zero
;  delay.  We've initialized Y to $21 instead of $32 to get a short '0'
;  (165usec) for the first half and a normal '0' for the second half;
FCD4: A0 21     722           LDY   #$21       ;  (400 USEC)
; Write one bit.  Called from WRITE with Y=$27.
FCD6: 20 DB FC  723  WRBIT    JSR   ZERDLY     ;WRITE TWO HALF CYCLES
FCD9: C8        724           INY              ;  OF 250 USEC ('0')
FCDA: C8        725           INY              ;  OR 500 USEC ('0')
; Delay for '0'.  X typically holds a bit count or half-cycle count.
; Y holds delay period in 5-usec increments:
;   (carry clear) $21=165us  $27=195us  $2C=220 $4B=375us
;   (carry set) $21=165+250=415us  $27=195+250=445us  $4B=375+250=625us
;   Remember that TOTAL delay, with all other instructions, must equal target
; On exit, Y=$2C, Z-flag is set if X decremented to zero.  The 2C in Y
;  is for WRBYTE, which is in a tight loop and doesn't need much padding.
FCDB: 88        726  ZERDLY   DEY
FCDC: D0 FD     727           BNE   ZERDLY
FCDE: 90 05     728           BCC   WRTAPE     ;Y IS COUNT FOR
; Additional delay for '1' (always 250us).
FCE0: A0 32     729           LDY   #$32       ;  TIMING LOOP
FCE2: 88        730  ONEDLY   DEY
FCE3: D0 FD     731           BNE   ONEDLY
; Write a transition to the tape.
FCE5: AC 20 C0  732  WRTAPE   LDY   TAPEOUT
FCE8: A0 2C     733           LDY   #$2C
FCEA: CA        734           DEX
FCEB: 60        735           RTS

; Read data from location in A1L up to location in A2L.
FEFD: 20 FA FC  999  READ     JSR   RD2BIT     ;FIND TAPEIN EDGE
FF00: A9 16     1000          LDA   #$16
FF02: 20 C9 FC  1001          JSR   HEADR      ;DELAY 3.5 SECONDS
FF05: 85 2E     1002          STA   CHKSUM     ;INIT CHKSUM=$FF
FF07: 20 FA FC  1003          JSR   RD2BIT     ;FIND TAPEIN EDGE
; Loop, waiting for edge.  11 cycles/iteration, plus 432+14 = 457usec.
FF0A: A0 24     1004 RD2      LDY   #$24       ;LOOK FOR SYNC BIT
FF0C: 20 FD FC  1005          JSR   RDBIT      ;  (SHORT 0)
FF0F: B0 F9     1006          BCS   RD2        ;  LOOP UNTIL FOUND
; Timing of next transition, a normal '0' half-cycle, doesn't matter.
FF11: 20 FD FC  1007          JSR   RDBIT      ;SKIP SECOND SYNC H-CYCLE
; Main byte read loop.  Continue until A1 reaches A2.
FF14: A0 3B     1008          LDY   #$3B       ;INDEX FOR 0/1 TEST
FF16: 20 EC FC  1009 RD3      JSR   RDBYTE     ;READ A BYTE
FF19: 81 3C     1010          STA   (A1L,X)    ;STORE AT (A1)
FF1B: 45 2E     1011          EOR   CHKSUM
FF1D: 85 2E     1012          STA   CHKSUM     ;UPDATE RUNNING CHKSUM
FF1F: 20 BA FC  1013          JSR   NXTA1      ;INC A1, COMPARE TO A2
FF22: A0 35     1014          LDY   #$35       ;COMPENSATE 0/1 INDEX
FF24: 90 F0     1015          BCC   RD3        ;LOOP UNTIL DONE
; Read checksum byte and check it.
FF26: 20 EC FC  1016          JSR   RDBYTE     ;READ CHKSUM BYTE
FF29: C5 2E     1017          CMP   CHKSUM
FF2B: F0 0D     1018          BEQ   BELL       ;GOOD, SOUND BELL AND RETURN

; Print "ERR", beep speaker.
FF2D: A9 C5     1019 PRERR    LDA   #$C5
FF2F: 20 ED FD  1020          JSR   COUT       ;PRINT "ERR", THEN BELL
FF32: A9 D2     1021          LDA   #$D2
FF34: 20 ED FD  1022          JSR   COUT
FF37: 20 ED FD  1023          JSR   COUT
FF3A: A9 87     1024 BELL     LDA   #$87       ;OUTPUT BELL AND RETURN
FF3C: 4C ED FD  1025          JMP   COUT

; Read a byte from the tape.  Y is $3B on first call, $35 on subsequent
;  calls.  The bits are shifted left, meaning that the high bit is read
;  first.
FCEC: A2 08     736  RDBYTE   LDX   #$08       ;8 BITS TO READ
FCEE: 48        737  RDBYT2   PHA              ;READ TWO TRANSITIONS
FCEF: 20 FA FC  738           JSR   RD2BIT     ;  (FIND EDGE)
FCF2: 68        739           PLA
FCF3: 2A        740           ROL              ;NEXT BIT
FCF4: A0 3A     741           LDY   #$3A       ;COUNT FOR SAMPLES
FCF6: CA        742           DEX
FCF7: D0 F5     743           BNE   RDBYT2
FCF9: 60        744           RTS

; Read two bits from the tape.
FCFA: 20 FD FC  745  RD2BIT   JSR   RDBIT
; Read one bit from the tape.  On entry, Y is the expected transition time:
;   $3A=696usec  $35=636usec  $24=432usec
; Returns with the carry set if the transition time exceeds the Y value.
FCFD: 88        746  RDBIT    DEY              ;DECR Y UNTIL
FCFE: AD 60 C0  747           LDA   TAPEIN     ; TAPE TRANSITION
FD01: 45 2F     748           EOR   LASTIN
FD03: 10 F8     749           BPL   RDBIT
; the above loop takes 12 usec per iteration, what follows takes 14.
FD05: 45 2F     750           EOR   LASTIN
FD07: 85 2F     751           STA   LASTIN
FD09: C0 80     752           CPY   #$80       ;SET CARRY ON Y
FD0B: 60        753           RTS

*/


/*
 * ==========================================================================
 *		CassetteDialog
 * ==========================================================================
 */

BEGIN_MESSAGE_MAP(CassetteDialog, CDialog)
	ON_NOTIFY(LVN_ITEMCHANGED, IDC_CASSETTE_LIST, OnListChange)
	ON_NOTIFY(NM_DBLCLK, IDC_CASSETTE_LIST, OnListDblClick)
	//ON_MESSAGE(WMU_DIALOG_READY, OnDialogReady)
	ON_COMMAND(IDC_IMPORT_CHUNK, OnImport)
	ON_COMMAND(IDHELP, OnHelp)
	ON_CBN_SELCHANGE(IDC_CASSETTE_ALG, OnAlgorithmChange)
END_MESSAGE_MAP()


/*
 * Set up the dialog.
 */
BOOL
CassetteDialog::OnInitDialog(void)
{
	CRect rect;
	const Preferences* pPreferences = GET_PREFERENCES();

	CDialog::OnInitDialog();		// does DDX init

	CWnd* pWnd;
	pWnd = GetDlgItem(IDC_IMPORT_CHUNK);
	pWnd->EnableWindow(FALSE);

	pWnd = GetDlgItem(IDC_CASSETTE_INPUT);
	pWnd->SetWindowText(fFileName);

	/* prep the combo box */
	CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_CASSETTE_ALG);
	ASSERT(pCombo != nil);
	int defaultAlg = pPreferences->GetPrefLong(kPrCassetteAlgorithm);
	if (defaultAlg > CassetteData::kAlgorithmMIN &&
		defaultAlg < CassetteData::kAlgorithmMAX)
	{
		pCombo->SetCurSel(defaultAlg);
	} else {
		WMSG1("GLITCH: invalid defaultAlg in prefs (%d)\n", defaultAlg);
		pCombo->SetCurSel(CassetteData::kAlgorithmZero);
	}
	fAlgorithm = (CassetteData::Algorithm) defaultAlg;

	/*
	 * Prep the listview control.
	 *
	 * Columns:
	 *	[icon] Index | Format | Length | Checksum OK
	 */
	CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST);
	ASSERT(pListView != nil);
	ListView_SetExtendedListViewStyleEx(pListView->m_hWnd,
		LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT);

	int width0, width1, width2, width3, width4;

	pListView->GetClientRect(&rect);
	width0 = pListView->GetStringWidth("XXIndexX");
	width1 = pListView->GetStringWidth("XXFormatXmmmmmmmmmmmmmm");
	width2 = pListView->GetStringWidth("XXLengthXm");
	width3 = pListView->GetStringWidth("XXChecksumXm");
	width4 = pListView->GetStringWidth("XXStart sampleX");
	//width5 = pListView->GetStringWidth("XXEnd sampleX");

	pListView->InsertColumn(0, "Index", LVCFMT_LEFT, width0);
	pListView->InsertColumn(1, "Format", LVCFMT_LEFT, width1);
	pListView->InsertColumn(2, "Length", LVCFMT_LEFT, width2);
	pListView->InsertColumn(3, "Checksum", LVCFMT_LEFT, width3);
	pListView->InsertColumn(4, "Start sample", LVCFMT_LEFT, width4);
	pListView->InsertColumn(5, "End sample", LVCFMT_LEFT,
		rect.Width() - (width0+width1+width2+width3+width4)
		/*- ::GetSystemMetrics(SM_CXVSCROLL)*/ );

	/* add images for list; this MUST be loaded before header images */
//	LoadListImages();
//	pListView->SetImageList(&fListImageList, LVSIL_SMALL);

//	LoadList();

	CenterWindow();

	//int cc = PostMessage(WMU_DIALOG_READY, 0, 0);
	//ASSERT(cc != 0);

	if (!AnalyzeWAV())
		OnCancel();

	return TRUE;
}

#if 0
/*
 * Dialog construction has completed.  Start the WAV analysis.
 */
LONG
CassetteDialog::OnDialogReady(UINT, LONG)
{
	//AnalyzeWAV();
	return 0;
}
#endif


/*
 * Something changed in the list.  Update the "OK" button.
 */
void
CassetteDialog::OnListChange(NMHDR*, LRESULT* pResult)
{
	WMSG0("List change\n");
	CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST);
	CButton* pButton = (CButton*) GetDlgItem(IDC_IMPORT_CHUNK);
	pButton->EnableWindow(pListView->GetSelectedCount() != 0);

	*pResult = 0;
}


/*
 * Double click.
 */
void
CassetteDialog::OnListDblClick(NMHDR* pNotifyStruct, LRESULT* pResult)
{
	WMSG0("Double click!\n");
	CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST);

	if (pListView->GetSelectedCount() == 1)
		OnImport();

	*pResult = 0;
}

/*
 * The volume filter drop-down box has changed.
 */
void
CassetteDialog::OnAlgorithmChange(void)
{
	CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_CASSETTE_ALG);
	ASSERT(pCombo != nil);
	WMSG1("+++ SELECTION IS NOW %d\n", pCombo->GetCurSel());
	fAlgorithm = (CassetteData::Algorithm) pCombo->GetCurSel();
	AnalyzeWAV();
}

/*
 * User pressed the "Help" button.
 */
void
CassetteDialog::OnHelp(void)
{
	WinHelp(HELP_TOPIC_IMPORT_CASSETTE, HELP_CONTEXT);
}

/*
 * User pressed "import" button.  Add the selected item to the current
 * archive or disk image.
 */
void
CassetteDialog::OnImport(void)
{
	/*
	 * Figure out which item they have selected.
	 */
	CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST);
	ASSERT(pListView != nil);
	assert(pListView->GetSelectedCount() == 1);

	POSITION posn;
	posn = pListView->GetFirstSelectedItemPosition();
	if (posn == nil) {
		ASSERT(false);
		return;
	}
	int idx = pListView->GetNextSelectedItem(posn);

	/*
	 * Set up the import dialog.
	 */
	CassImpTargetDialog impDialog(this);

	impDialog.fFileName = "From.Tape";
	impDialog.fFileLength = fDataArray[idx].GetDataLen();
	impDialog.SetFileType(fDataArray[idx].GetFileType());

	if (impDialog.DoModal() != IDOK)
		return;

	/*
	 * Write the file to the currently-open archive.
	 */
	GenericArchive::FileDetails details;

	details.entryKind = GenericArchive::FileDetails::kFileKindDataFork;
	details.origName = "Cassette WAV";
	details.storageName = impDialog.fFileName;
	details.access = 0xe3;	// unlocked, backup bit set
	details.fileType = impDialog.GetFileType();
	if (details.fileType == kFileTypeBIN)
		details.extraType = impDialog.fStartAddr;
	else if (details.fileType == kFileTypeBAS)
		details.extraType = 0x0801;
	else
		details.extraType = 0x0000;
	details.storageType = DiskFS::kStorageSeedling;
	time_t now = time(nil);
	GenericArchive::UNIXTimeToDateTime(&now, &details.createWhen);
	GenericArchive::UNIXTimeToDateTime(&now, &details.archiveWhen);

	CString errMsg;

	fDirty = true;
	if (!MainWindow::SaveToArchive(&details, fDataArray[idx].GetDataBuf(),
		fDataArray[idx].GetDataLen(), nil, -1, /*ref*/errMsg, this))
	{
		goto bail;
	}


bail:
	if (!errMsg.IsEmpty()) {
		CString msg;
		msg.Format("Unable to import file: %s.", (const char *) errMsg);
		ShowFailureMsg(this, msg, IDS_FAILED);
		return;
	}
}


/*
 * Analyze the contents of a WAV file.
 *
 * Returns "true" if it found anything at all, "false" if not.
 */
bool
CassetteDialog::AnalyzeWAV(void)
{
	SoundFile soundFile;
	CWaitCursor waitc;
	CListCtrl* pListCtrl = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST);
	CString errMsg;
	long sampleOffset;
	int idx;

	if (soundFile.Create(fFileName, &errMsg) != 0) {
		ShowFailureMsg(this, errMsg, IDS_FAILED);
		return false;
	}

	const WAVEFORMATEX* pFormat = soundFile.GetWaveFormat();
	if (pFormat->nChannels < 1 || pFormat->nChannels > 2 ||
		(pFormat->wBitsPerSample != 8 && pFormat->wBitsPerSample != 16))
	{
		errMsg.Format("Unexpected PCM format (%d channels, %d bits/sample)",
			pFormat->nChannels, pFormat->wBitsPerSample);
		ShowFailureMsg(this, errMsg, IDS_FAILED);
		return false;
	}
	if (soundFile.GetDataLen() % soundFile.GetBPS() != 0) {
		errMsg.Format("Unexpected sound data length (%ld, samples are %d bytes)",
			soundFile.GetDataLen(), soundFile.GetBPS());
		ShowFailureMsg(this, errMsg, IDS_FAILED);
		return false;
	}

	pListCtrl->DeleteAllItems();

	sampleOffset = 0;
	for (idx = 0; idx < kMaxRecordings; idx++) {
		long fileType;
		bool result;

		result = fDataArray[idx].Scan(&soundFile, fAlgorithm, &sampleOffset);
		if (!result)
			break;

		AddEntry(idx, pListCtrl, &fileType);
		fDataArray[idx].SetFileType(fileType);
	}

	if (idx == 0) {
		WMSG0("No Apple II files found\n");
		/* that's okay, just show the empty list */
	}

	return true;
}

/*
 * Add an entry to the list.
 *
 * Layout: index format length checksum start-offset
 */
void
CassetteDialog::AddEntry(int idx, CListCtrl* pListCtrl, long* pFileType)
{
	CString tmpStr;
	const CassetteData* pData = &fDataArray[idx];
	const unsigned char* pDataBuf = pData->GetDataBuf();

	ASSERT(pDataBuf != nil);

	tmpStr.Format("%d", idx);
	pListCtrl->InsertItem(idx, tmpStr);

	*pFileType = kFileTypeBIN;
	if (pData->GetDataLen() == 2) {
		tmpStr.Format("Integer header ($%04X)",
			pDataBuf[0] | pDataBuf[1] << 8);
	} else if (pData->GetDataLen() == 3) {
		tmpStr.Format("Applesoft header ($%04X $%02x)",
			pDataBuf[0] | pDataBuf[1] << 8, pDataBuf[2]);
	} else if (pData->GetDataLen() > 3 && idx > 0 &&
		fDataArray[idx-1].GetDataLen() == 2)
	{
		tmpStr = "Integer BASIC";
		*pFileType = kFileTypeINT;
	} else if (pData->GetDataLen() > 3 && idx > 0 &&
		fDataArray[idx-1].GetDataLen() == 3)
	{
		tmpStr = "Applesoft BASIC";
		*pFileType = kFileTypeBAS;
	} else {
		tmpStr = "Binary";
	}
	pListCtrl->SetItemText(idx, 1, tmpStr);
	
	tmpStr.Format("%d", pData->GetDataLen());
	pListCtrl->SetItemText(idx, 2, tmpStr);
	if (pData->GetDataChkGood())
		tmpStr.Format("Good (0x%02x)", pData->GetDataChecksum());
	else
		tmpStr.Format("BAD (0x%02x)", pData->GetDataChecksum());
	pListCtrl->SetItemText(idx, 3, tmpStr);
	tmpStr.Format("%ld", pData->GetDataOffset());
	pListCtrl->SetItemText(idx, 4, tmpStr);
	tmpStr.Format("%ld", pData->GetDataEndOffset());
	pListCtrl->SetItemText(idx, 5, tmpStr);
}


/*
 * ==========================================================================
 *		CassetteData
 * ==========================================================================
 */

/*
 * Scan the WAV file, starting from the specified byte offset.
 *
 * Returns "true" if we found a file, "false" if not (indicating that the
 * end of the input has been reached).  Updates "*pStartOffset" to point
 * past the end of the data we've read.
 */
bool
CassetteDialog::CassetteData::Scan(SoundFile* pSoundFile, Algorithm alg,
	long* pStartOffset)
{
	const int kSampleChunkSize = 65536;		// should be multiple of 4
	const WAVEFORMATEX* pFormat;
	ScanState scanState;
	long initialLen, dataLen, chunkLen, byteOffset;
	long sampleStartIndex;
	unsigned char* buf = nil;
	float* sampleBuf = nil;
	int bytesPerSample;
	bool result = false;
	unsigned char checkSum;
	int outByteIndex, bitAcc;

	bytesPerSample = pSoundFile->GetBPS();
	assert(bytesPerSample >= 1 && bytesPerSample <= 4);
	assert(kSampleChunkSize % bytesPerSample == 0);
	byteOffset = *pStartOffset;
	initialLen = dataLen = pSoundFile->GetDataLen() - byteOffset;
	sampleStartIndex = byteOffset/bytesPerSample;
	WMSG4("CassetteData::Scan(off=%ld / %ld) len=%ld  alg=%d\n",
		byteOffset, sampleStartIndex, dataLen, alg);

	pFormat = pSoundFile->GetWaveFormat();

	buf = new unsigned char[kSampleChunkSize];
	sampleBuf = new float[kSampleChunkSize/bytesPerSample];
	if (fOutputBuf == nil)	// alloc on first use
		fOutputBuf = new unsigned char[kMaxFileLen];
	if (buf == nil || sampleBuf == nil || fOutputBuf == nil) {
		WMSG0("Buffer alloc failed\n");
		goto bail;
	}

	memset(&scanState, 0, sizeof(scanState));
	scanState.algorithm = alg;
	scanState.phase = kPhaseScanFor770Start;
	scanState.mode = kModeInitial0;
	scanState.positive = false;
	scanState.usecPerSample = 1000000.0f / (float) pFormat->nSamplesPerSec;

	checkSum = 0xff;
	outByteIndex = 0;
	bitAcc = 1;

	/*
	 * Loop until done or out of data.
	 */
	while (dataLen > 0) {
		int cc;

		chunkLen = dataLen;
		if (chunkLen > kSampleChunkSize)
			chunkLen = kSampleChunkSize;

		cc = pSoundFile->ReadData(buf, byteOffset, chunkLen);
		if (cc < 0) {
			WMSG1("ReadData(%d) failed\n", chunkLen);
			goto bail;
		}

		ConvertSamplesToReal(pFormat, buf, chunkLen, sampleBuf);

		for (int i = 0; i < chunkLen / bytesPerSample; i++) {
			int bitVal;
			if (ProcessSample(sampleBuf[i], sampleStartIndex + i,
				&scanState, &bitVal))
			{
				if (outByteIndex >= kMaxFileLen) {
					WMSG0("Cassette data overflow\n");
					scanState.phase = kPhaseEndReached;
				} else {
					/* output a bit, shifting until bit 8 lights up */
					assert(bitVal == 0 || bitVal == 1);
					bitAcc = (bitAcc << 1) | bitVal;
					if (bitAcc > 0xff) {
						fOutputBuf[outByteIndex++] = (unsigned char) bitAcc;
						checkSum ^= (unsigned char) bitAcc;
						bitAcc = 1;
					}
				}
			}
			if (scanState.phase == kPhaseEndReached) {
				dataLen -= i * bytesPerSample;
				break;
			}
		}
		if (scanState.phase == kPhaseEndReached)
			break;

		dataLen -= chunkLen;
		byteOffset += chunkLen;
		sampleStartIndex += chunkLen / bytesPerSample;
	}

	switch (scanState.phase) {
	case kPhaseScanFor770Start:
	case kPhaseScanning770:
		// expected case for trailing part of file
		WMSG0("Scan ended while searching for 770\n");
		goto bail;
	case kPhaseScanForShort0:
	case kPhaseShort0B:
		WMSG0("Scan ended while searching for short 0/0B\n");
		//DebugBreak();	// unusual
		goto bail;
	case kPhaseReadData:
		WMSG0("Scan ended while reading data\n");
		//DebugBreak();	// truncated WAV file?
		goto bail;
	case kPhaseEndReached:
		WMSG0("Scan found end\n");
		// winner!
		break;
	default:
		WMSG1("Unknown phase %d\n", scanState.phase);
		assert(false);
		goto bail;
	}

	WMSG3("*** Output %d bytes (bitAcc=0x%02x, checkSum=0x%02x)\n",
		outByteIndex, bitAcc, checkSum);

	if (outByteIndex == 0) {
		fOutputLen = 0;
		fChecksum = 0x00;
		fChecksumGood = false;
	} else {
		fOutputLen = outByteIndex-1;
		fChecksum = fOutputBuf[outByteIndex-1];
		fChecksumGood = (checkSum == 0x00);
	}
	fStartSample = scanState.dataStart;
	fEndSample = scanState.dataEnd;

	/* we're done with this file; advance the start offset */
	*pStartOffset = *pStartOffset + (initialLen - dataLen);

	result = true;

bail:
	delete[] buf;
	delete[] sampleBuf;
	return result;
}

/*
 * Convert a block of samples from PCM to float.
 *
 * Only the first (left) channel is converted in multi-channel formats.
 */
void
CassetteDialog::CassetteData::ConvertSamplesToReal(const WAVEFORMATEX* pFormat,
	const unsigned char* buf, long chunkLen, float* sampleBuf)
{
	int bps = ((pFormat->wBitsPerSample+7)/8) * pFormat->nChannels;
	int bitsPerSample = pFormat->wBitsPerSample;
	int offset = 0;

	assert(chunkLen % bps == 0);

	if (bitsPerSample == 8) {
		while (chunkLen > 0) {
			*sampleBuf++ = (*buf - 128) / 128.0f;
			//WMSG3("Sample8(%5d)=%d float=%.3f\n", offset, *buf, *(sampleBuf-1));
			//offset++;
			buf += bps;
			chunkLen -= bps;
		}
	} else if (bitsPerSample == 16) {
		while (chunkLen > 0) {
			short sample = *buf | *(buf+1) << 8;
			*sampleBuf++ = sample / 32768.0f;
			//WMSG3("Sample16(%5d)=%d float=%.3f\n", offset, sample, *(sampleBuf-1));
			//offset++;
			buf += bps;
			chunkLen -= bps;
		}
	} else {
		assert(false);
	}

	//WMSG1("Conv %d\n", bitsPerSample);
}

/* width of 1/2 cycle in 770Hz lead-in */
const float kLeadInHalfWidth = 650.0f;		// usec
/* max error when detecting 770Hz lead-in, in usec */
const float kLeadInMaxError = 108.0f;		// usec (542 - 758)
/* width of 1/2 cycle of "short 0" */
const float kShortZeroHalfWidth = 200.0f;	// usec
/* max error when detection short 0 */
const float kShortZeroMaxError = 150.0f;	// usec (50 - 350)
/* width of 1/2 cycle of '0' */
const float kZeroHalfWidth = 250.0f;		// usec
/* max error when detecting '0' */
const float kZeroMaxError = 94.0f;			// usec
/* width of 1/2 cycle of '1' */
const float kOneHalfWidth = 500.0f;			// usec
/* max error when detecting '1' */
const float kOneMaxError = 94.0f;			// usec
/* after this many 770Hz half-cycles, start looking for short 0 */
const long kLeadInHalfCycThreshold = 1540;	// 1 full second

/* amplitude must change by this much before we switch out of "peak" mode */
const float kPeakThreshold = 0.2f;			// 10%
/* amplitude must change by at least this much to stay in "transition" mode */
const float kTransMinDelta = 0.02f;			// 1%
/* kTransMinDelta happens over this range */
const float kTransDeltaBase = 45.35f;		// usec (1 sample at 22.05KHz)


/*
 * Process one audio sample.  Updates "pScanState" appropriately.
 *
 * If we think we found a bit, this returns "true" with 0 or 1 in "*pBitVal".
 */
bool
CassetteDialog::CassetteData::ProcessSample(float sample, long sampleIndex,
	ScanState* pScanState, int* pBitVal)
{
	if (pScanState->algorithm == kAlgorithmZero)
		return ProcessSampleZero(sample, sampleIndex, pScanState, pBitVal);
	else if (pScanState->algorithm == kAlgorithmRoundPeak ||
			 pScanState->algorithm == kAlgorithmSharpPeak ||
			 pScanState->algorithm == kAlgorithmShallowPeak)
		return ProcessSamplePeak(sample, sampleIndex, pScanState, pBitVal);
	else {
		assert(false);
		return false;
	}
}

/*
 * Process the data by measuring the distance between zero crossings.
 *
 * This is very similar to the way the Apple II does it, though
 * we have to scan for the 770Hz lead-in instead of simply assuming the
 * the user has queued up the tape.
 *
 * To offset the effects of DC bias, we examine full cycles instead of
 * half cycles.
 */
bool
CassetteDialog::CassetteData::ProcessSampleZero(float sample, long sampleIndex,
	ScanState* pScanState, int* pBitVal)
{
	long timeDelta;
	bool crossedZero = false;
	bool emitBit = false;

	/*
	 * Analyze the mode, changing to a new one when appropriate.
	 */
	switch (pScanState->mode) {
	case kModeInitial0:
		assert(pScanState->phase == kPhaseScanFor770Start);
		pScanState->mode = kModeRunning;
		break;
	case kModeRunning:
		if (pScanState->prevSample < 0.0f && sample >= 0.0f ||
			pScanState->prevSample >= 0.0f && sample < 0.0f)
		{
			crossedZero = true;
		}
		break;
	default:
		assert(false);
		break;
	}

	/*
	 * Deal with a zero crossing.
	 *
	 * We currently just grab the first point after we cross.  We should
	 * be grabbing the closest point or interpolating across.
	 */
	if (crossedZero) {
		float halfCycleUsec;
		int bias;

		if (fabs(pScanState->prevSample) < fabs(sample))
			bias = -1;		// previous sample was closer to zero point
		else
			bias = 0;		// current sample is closer

		/* delta time for zero-to-zero (half cycle) */
		timeDelta = (sampleIndex+bias) - pScanState->lastZeroIndex;

		halfCycleUsec = timeDelta * pScanState->usecPerSample;
		//WMSG3("Zero %6ld: half=%.1fusec full=%.1fusec\n",
		//	sampleIndex, halfCycleUsec,
		//	halfCycleUsec + pScanState->halfCycleWidth);

		emitBit = UpdatePhase(pScanState, sampleIndex+bias, halfCycleUsec,
			pBitVal);

		pScanState->lastZeroIndex = sampleIndex + bias;
	}

	/* record this sample for the next go-round */
	pScanState->prevSample = sample;

	return emitBit;
}

/*
 * Process the data by finding and measuring the distance between peaks.
 */
bool
CassetteDialog::CassetteData::ProcessSamplePeak(float sample, long sampleIndex,
	ScanState* pScanState, int* pBitVal)
{
	/* values range from [-1.0,1.0), so range is 2.0 total */
	long timeDelta;
	float ampDelta;
	float transitionLimit;
	bool hitPeak = false;
	bool emitBit = false;

	/*
	 * Analyze the mode, changing to a new one when appropriate.
	 */
	switch (pScanState->mode) {
	case kModeInitial0:
		assert(pScanState->phase == kPhaseScanFor770Start);
		pScanState->mode = kModeInitial1;
		break;
	case kModeInitial1:
		assert(pScanState->phase == kPhaseScanFor770Start);
		if (sample >= pScanState->prevSample)
			pScanState->positive = true;
		else
			pScanState->positive = false;
		pScanState->mode = kModeInTransition;
		/* set these up with something reasonable */
		pScanState->lastPeakStartIndex = sampleIndex;
		pScanState->lastPeakStartValue = sample;
		break;

	case kModeInTransition:
		/*
		 * Stay here until two adjacent samples are very close in amplitude
		 * (or we change direction).  We need to adjust our amplitude
		 * threshold based on sampling frequency, or at higher sample
		 * rates we're going to think everything is a transition.
		 *
		 * The approach here is overly simplistic, and is prone to failure
		 * when the sampling rate is high, especially with 8-bit samples
		 * or sound cards that don't really have 16-bit resolution.  The
		 * proper way to do this is to keep a short history, and evaluate
		 * the delta amplitude over longer periods.  [At this point I'd
		 * rather just tell people to record at 22.05KHz.]
		 *
		 * Set the "hitPeak" flag and handle the consequences below.
		 */
		if (pScanState->algorithm == kAlgorithmRoundPeak)
			transitionLimit = kTransMinDelta *
					(pScanState->usecPerSample / kTransDeltaBase);
		else
			transitionLimit = 0.0f;

		if (pScanState->positive) {
			if (sample < pScanState->prevSample + transitionLimit) {
				pScanState->mode = kModeAtPeak;
				hitPeak = true;
			}
		} else {
			if (sample > pScanState->prevSample - transitionLimit) {
				pScanState->mode = kModeAtPeak;
				hitPeak = true;
			}
		}
		break;
	case kModeAtPeak:
		/*
		 * Stay here until we're a certain distance above or below the
		 * previous peak.  This also keeps us in a holding pattern for
		 * large flat areas.
		 */
		transitionLimit = kPeakThreshold;
		if (pScanState->algorithm == kAlgorithmShallowPeak)
			transitionLimit /= 4.0f;

		ampDelta = pScanState->lastPeakStartValue - sample;
		if (ampDelta < 0)
			ampDelta = -ampDelta;
		if (ampDelta > transitionLimit) {
			if (sample >= pScanState->lastPeakStartValue)
				pScanState->positive = true;		// going up
			else
				pScanState->positive = false;		// going down

			/* mark the end of the peak; could be same as start of peak */
			pScanState->mode = kModeInTransition;
		}
		break;
	default:
		assert(false);
		break;
	}

	/*
	 * If we hit "peak" criteria, we regard the *previous* sample as the
	 * peak.  This is very important for lower sampling rates (e.g. 8KHz).
	 */
	if (hitPeak) {
		/* compute half-cycle amplitude and time */
		float halfCycleUsec; //, fullCycleUsec;

		/* delta time for peak-to-peak (half cycle) */
		timeDelta = (sampleIndex-1) - pScanState->lastPeakStartIndex;
		/* amplitude peak-to-peak */
		ampDelta = pScanState->lastPeakStartValue - pScanState->prevSample;
		if (ampDelta < 0)
			ampDelta = -ampDelta;

		halfCycleUsec = timeDelta * pScanState->usecPerSample;
		//if (sampleIndex > 584327 && sampleIndex < 590000) {
		//	WMSG4("Peak %6ld: amp=%.3f height=%.3f peakWidth=%.1fusec\n",
		//		sampleIndex-1, pScanState->prevSample, ampDelta,
		//		halfCycleUsec);
		//	::Sleep(10);
		//}
		if (sampleIndex == 32739)
			WMSG0("whee\n");

		emitBit = UpdatePhase(pScanState, sampleIndex-1, halfCycleUsec, pBitVal);

		/* set the "peak start" values */
		pScanState->lastPeakStartIndex = sampleIndex-1;
		pScanState->lastPeakStartValue = pScanState->prevSample;
	}

	/* record this sample for the next go-round */
	pScanState->prevSample = sample;

	return emitBit;
}


/*
 * Given the width of a half-cycle, update "phase" and decide whether or not
 * it's time to emit a bit.
 *
 * Updates "halfCycleWidth" too, alternating between 0.0 and a value.
 *
 * The "sampleIndex" parameter is largely just for display.  We use it to
 * set the "start" and "end" pointers, but those are also ultimately just
 * for display to the user.
 */
bool
CassetteDialog::CassetteData::UpdatePhase(ScanState* pScanState, long sampleIndex,
	float halfCycleUsec, int* pBitVal)
{
	float fullCycleUsec;
	bool emitBit = false;

	if (pScanState->halfCycleWidth != 0.0f)
		fullCycleUsec = halfCycleUsec + pScanState->halfCycleWidth;
	else
		fullCycleUsec = 0.0f;	// only have first half

	switch (pScanState->phase) {
	case kPhaseScanFor770Start:
		/* watch for a cycle of the appropriate length */
		if (fullCycleUsec != 0.0f &&
			fullCycleUsec > kLeadInHalfWidth*2.0f - kLeadInMaxError*2.0f &&
			fullCycleUsec < kLeadInHalfWidth*2.0f + kLeadInMaxError*2.0f)
		{
			//WMSG1("  scanning 770 at %ld\n", sampleIndex);
			pScanState->phase = kPhaseScanning770;
			pScanState->num770 = 1;
		}
		break;
	case kPhaseScanning770:
		/* count up the 770Hz cycles */
		if (fullCycleUsec != 0.0f &&
			fullCycleUsec > kLeadInHalfWidth*2.0f - kLeadInMaxError*2.0f &&
			fullCycleUsec < kLeadInHalfWidth*2.0f + kLeadInMaxError*2.0f)
		{
			pScanState->num770++;
			if (pScanState->num770 > kLeadInHalfCycThreshold/2) {
				/* looks like a solid tone, advance to next phase */
				pScanState->phase = kPhaseScanForShort0;
				WMSG0("  looking for short 0\n");
			}
		} else if (fullCycleUsec != 0.0f) {
			/* pattern lost, reset */
			if (pScanState->num770 > 5) {
				WMSG3("  lost 770 at %ld width=%.1f (count=%ld)\n",
					sampleIndex, fullCycleUsec, pScanState->num770);
			}
			pScanState->phase = kPhaseScanFor770Start;
		}
		/* else we only have a half cycle, so do nothing */
		break;
	case kPhaseScanForShort0:
		/* found what looks like a 770Hz field, find the short 0 */
		if (halfCycleUsec > kShortZeroHalfWidth - kShortZeroMaxError &&
			halfCycleUsec < kShortZeroHalfWidth + kShortZeroMaxError)
		{
			WMSG3("  found short zero (half=%.1f) at %ld after %ld 770s\n",
				halfCycleUsec, sampleIndex, pScanState->num770);
			pScanState->phase = kPhaseShort0B;
			/* make sure we treat current sample as first half */
			pScanState->halfCycleWidth = 0.0f;
		} else
		if (fullCycleUsec != 0.0f &&
			fullCycleUsec > kLeadInHalfWidth*2.0f - kLeadInMaxError*2.0f &&
			fullCycleUsec < kLeadInHalfWidth*2.0f + kLeadInMaxError*2.0f)
		{
			/* found another 770Hz cycle */
			pScanState->num770++;
		} else if (fullCycleUsec != 0.0f) {
			/* full cycle of the wrong size, we've lost it */
			WMSG3("  Lost 770 at %ld width=%.1f (count=%ld)\n",
				sampleIndex, fullCycleUsec, pScanState->num770);
			pScanState->phase = kPhaseScanFor770Start;
		}
		break;
	case kPhaseShort0B:
		/* pick up the second half of the start cycle */
		assert(fullCycleUsec != 0.0f);
		if (fullCycleUsec > (kShortZeroHalfWidth + kZeroHalfWidth) - kZeroMaxError*2.0f &&
			fullCycleUsec < (kShortZeroHalfWidth + kZeroHalfWidth) + kZeroMaxError*2.0f)
		{
			/* as expected */
			WMSG2("  Found 0B %.1f (total %.1f), advancing to 'read data' phase\n",
				halfCycleUsec, fullCycleUsec);
			pScanState->dataStart = sampleIndex;
			pScanState->phase = kPhaseReadData;
		} else {
			/* must be a false-positive at end of tone */
			WMSG2("  Didn't find post-short-0 value (half=%.1f + %.1f)\n",
				pScanState->halfCycleWidth, halfCycleUsec);
			pScanState->phase = kPhaseScanFor770Start;
		}
		break;

	case kPhaseReadData:
		/* check width of full cycle; don't double error allowance */
		if (fullCycleUsec != 0.0f) {
			if (fullCycleUsec > kZeroHalfWidth*2 - kZeroMaxError*2 &&
				fullCycleUsec < kZeroHalfWidth*2 + kZeroMaxError*2)
			{
				*pBitVal = 0;
				emitBit = true;
			} else
			if (fullCycleUsec > kOneHalfWidth*2 - kOneMaxError*2 &&
				fullCycleUsec < kOneHalfWidth*2 + kOneMaxError*2)
			{
				*pBitVal = 1;
				emitBit = true;
			} else {
				/* bad cycle, assume end reached */
				WMSG2("  Bad full cycle time %.1f in data at %ld, bailing\n",
					fullCycleUsec, sampleIndex);
				pScanState->dataEnd = sampleIndex;
				pScanState->phase = kPhaseEndReached;
			}
		}
		break;
	default:
		assert(false);
		break;
	}

	/* save the half-cycle stats */
	if (pScanState->halfCycleWidth == 0.0f)
		pScanState->halfCycleWidth = halfCycleUsec;
	else
		pScanState->halfCycleWidth = 0.0f;

	return emitBit;
}
