mirror of
https://github.com/fadden/ciderpress.git
synced 2024-12-22 20:29:51 +00:00
b79498da50
Most of this change is a conversion of the old FileDetails struct into a new LocalFileDetails class. The new class keeps the members private, and keeps the Unicode and MOR representations of the string separate. The NuFX and DiskImg libraries don't support UTF-16 filenames, so we stil can't add files with non-CP-1252 filenames, but we're a step closer. Also, update NufxLib with a couple of fixes from the main project. Also, fix handling of "%00" when adding files. Also, mark most of the A2FileDOS fields private. Not sure why they weren't.
1106 lines
41 KiB
C++
1106 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 "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;
|
|
}
|