/* * 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 /* * 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 . * - 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 . * - 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(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 /* * 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(L"Unable to import file: %ls.", (LPCWSTR) 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(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) { 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(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 * ========================================================================== */ /* * 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; }