/*
 * 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 "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()


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 != NULL);
    int defaultAlg = pPreferences->GetPrefLong(kPrCassetteAlgorithm);
    if (defaultAlg > CassetteData::kAlgorithmMIN &&
        defaultAlg < CassetteData::kAlgorithmMAX)
    {
        pCombo->SetCurSel(defaultAlg);
    } else {
        LOGI("GLITCH: invalid defaultAlg in prefs (%d)", 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 != NULL);
    ListView_SetExtendedListViewStyleEx(pListView->m_hWnd,
        LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT);

    int width0, width1, width2, width3, width4;

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

    pListView->InsertColumn(0, L"Index", LVCFMT_LEFT, width0);
    pListView->InsertColumn(1, L"Format", LVCFMT_LEFT, width1);
    pListView->InsertColumn(2, L"Length", LVCFMT_LEFT, width2);
    pListView->InsertColumn(3, L"Checksum", LVCFMT_LEFT, width3);
    pListView->InsertColumn(4, L"Start sample", LVCFMT_LEFT, width4);
    pListView->InsertColumn(5, L"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


void CassetteDialog::OnListChange(NMHDR*, LRESULT* pResult)
{
    LOGI("List change");
    CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST);
    CButton* pButton = (CButton*) GetDlgItem(IDC_IMPORT_CHUNK);
    pButton->EnableWindow(pListView->GetSelectedCount() != 0);

    *pResult = 0;
}


void CassetteDialog::OnListDblClick(NMHDR* pNotifyStruct, LRESULT* pResult)
{
    LOGI("Double click!");
    CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST);

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

    *pResult = 0;
}

void CassetteDialog::OnAlgorithmChange(void)
{
    CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_CASSETTE_ALG);
    ASSERT(pCombo != NULL);
    LOGI("+++ SELECTION IS NOW %d", pCombo->GetCurSel());
    fAlgorithm = (CassetteData::Algorithm) pCombo->GetCurSel();
    AnalyzeWAV();
}

void CassetteDialog::OnImport(void)
{
    /*
     * Figure out which item they have selected.
     */
    CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST);
    ASSERT(pListView != NULL);
    assert(pListView->GetSelectedCount() == 1);

    POSITION posn;
    posn = pListView->GetFirstSelectedItemPosition();
    if (posn == NULL) {
        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::LocalFileDetails details;

    details.SetEntryKind(GenericArchive::LocalFileDetails::kFileKindDataFork);
    details.SetLocalPathName(L"Cassette WAV");
    details.SetStrippedLocalPathName(impDialog.fFileName);
    details.SetAccess(0xe3);    // unlocked, backup bit set
    details.SetFileType(impDialog.GetFileType());
    if (details.GetFileType() == kFileTypeBIN) {
        details.SetExtraType(impDialog.fStartAddr);
    } else if (details.GetFileType() == kFileTypeBAS) {
        details.SetExtraType(0x0801);
    } else {
        details.SetExtraType(0x0000);
    }
    details.SetStorageType(DiskFS::kStorageSeedling);
    time_t now = time(NULL);
    NuDateTime ndt;
    GenericArchive::UNIXTimeToDateTime(&now, &ndt);
    details.SetCreateWhen(ndt);
    details.SetArchiveWhen(ndt);
    details.SetModWhen(ndt);

    CString errMsg;

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


bail:
    if (!errMsg.IsEmpty()) {
        CString msg;
        msg.Format(L"Unable to import file: %ls.", (LPCWSTR) errMsg);
        ShowFailureMsg(this, msg, IDS_FAILED);
        return;
    }
}

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(L"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(L"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) {
        LOGI("No Apple II files found");
        /* that's okay, just show the empty list */
    }

    return true;
}

void CassetteDialog::AddEntry(int idx, CListCtrl* pListCtrl, long* pFileType)
{
    CString tmpStr;
    const CassetteData* pData = &fDataArray[idx];
    const unsigned char* pDataBuf = pData->GetDataBuf();

    ASSERT(pDataBuf != NULL);

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

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


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

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 = NULL;
    float* sampleBuf = NULL;
    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;
    LOGI("CassetteData::Scan(off=%ld / %ld) len=%ld  alg=%d",
        byteOffset, sampleStartIndex, dataLen, alg);

    pFormat = pSoundFile->GetWaveFormat();

    buf = new unsigned char[kSampleChunkSize];
    sampleBuf = new float[kSampleChunkSize/bytesPerSample];
    if (fOutputBuf == NULL)  // alloc on first use
        fOutputBuf = new unsigned char[kMaxFileLen];
    if (buf == NULL || sampleBuf == NULL || fOutputBuf == NULL) {
        LOGI("Buffer alloc failed");
        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) {
            LOGI("ReadData(%d) failed", 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) {
                    LOGI("Cassette data overflow");
                    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
        LOGI("Scan ended while searching for 770");
        goto bail;
    case kPhaseScanForShort0:
    case kPhaseShort0B:
        LOGI("Scan ended while searching for short 0/0B");
        //DebugBreak(); // unusual
        goto bail;
    case kPhaseReadData:
        LOGI("Scan ended while reading data");
        //DebugBreak(); // truncated WAV file?
        goto bail;
    case kPhaseEndReached:
        LOGI("Scan found end");
        // winner!
        break;
    default:
        LOGI("Unknown phase %d", scanState.phase);
        assert(false);
        goto bail;
    }

    LOGI("*** Output %d bytes (bitAcc=0x%02x, checkSum=0x%02x)",
        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;
}

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;
            //LOGI("Sample8(%5d)=%d float=%.3f", 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;
            //LOGI("Sample16(%5d)=%d float=%.3f", offset, sample, *(sampleBuf-1));
            //offset++;
            buf += bps;
            chunkLen -= bps;
        }
    } else {
        assert(false);
    }

    //LOGI("Conv %d", 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)


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;
    }
}

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;
        //LOGI("Zero %6ld: half=%.1fusec full=%.1fusec",
        //  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;
}

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) {
        //  LOGI("Peak %6ld: amp=%.3f height=%.3f peakWidth=%.1fusec",
        //      sampleIndex-1, pScanState->prevSample, ampDelta,
        //      halfCycleUsec);
        //  ::Sleep(10);
        //}
        if (sampleIndex == 32739)
            LOGI("whee");

        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;
}

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)
        {
            //LOGI("  scanning 770 at %ld", 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;
                LOGI("  looking for short 0");
            }
        } else if (fullCycleUsec != 0.0f) {
            /* pattern lost, reset */
            if (pScanState->num770 > 5) {
                LOGI("  lost 770 at %ld width=%.1f (count=%ld)",
                    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)
        {
            LOGI("  found short zero (half=%.1f) at %ld after %ld 770s",
                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 */
            LOGI("  Lost 770 at %ld width=%.1f (count=%ld)",
                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 */
            LOGI("  Found 0B %.1f (total %.1f), advancing to 'read data' phase",
                halfCycleUsec, fullCycleUsec);
            pScanState->dataStart = sampleIndex;
            pScanState->phase = kPhaseReadData;
        } else {
            /* must be a false-positive at end of tone */
            LOGI("  Didn't find post-short-0 value (half=%.1f + %.1f)",
                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 */
                LOGI("  Bad full cycle time %.1f in data at %ld, bailing",
                    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;
}
