ciderpress/app/CassetteDialog.cpp
Andy McFadden d8223dbcfd Relocate method comments
This moves method comments from the .cpp file to the .h file,
where users of the methods can find them.  This also makes it
possible for the IDE to show the comments when you mouse-hover over
the method name, though Visual Studio is a bit weak in this regard.

Also, added "override" keywords on overridden methods.  Reasonably
current versions of popular compilers seem to support this.

Also, don't have the return type on a separate line in the .cpp file.
The motivation for the practice -- quickly finding a method definition
with "^name" -- is less useful in C++ than C, and modern IDEs provide
more convenient ways to do the same thing.

Also, do some more conversion from unsigned types to uintXX_t.

This commit is primarily for the "app" directory.
2014-11-21 22:33:39 -08:00

1108 lines
41 KiB
C++

/*
* 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()
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::OnHelp(void)
{
WinHelp(HELP_TOPIC_IMPORT_CASSETTE, HELP_CONTEXT);
}
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::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(NULL);
GenericArchive::UNIXTimeToDateTime(&now, &details.createWhen);
GenericArchive::UNIXTimeToDateTime(&now, &details.archiveWhen);
CString errMsg;
fDirty = true;
if (!MainWindow::SaveToArchive(&details, fDataArray[idx].GetDataBuf(),
fDataArray[idx].GetDataLen(), NULL, -1, /*ref*/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;
}