Skip to content

Commit

Permalink
Audio recording support, part 3
Browse files Browse the repository at this point in the history
Synthesize archive record entries for the tape chunks.  Integer and
Applesoft BASIC programs, and Applesoft shape tables, are
automatically identified and given appropriate file types.  The
2-byte/3-byte header before these is excluded from view.
  • Loading branch information
fadden committed Oct 14, 2024
1 parent 9da222f commit 61f8754
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 26 deletions.
28 changes: 28 additions & 0 deletions CommonUtil/AppHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,34 @@ public void SetOptionInt(string key, int value) {
//}
}

public T GetOptionEnum<T>(string name, T defaultValue) {
if (!mOptions.TryGetValue(name, out string? valueStr)) {
return defaultValue;
}
try {
object o = Enum.Parse(typeof(T), valueStr);
return (T)o;
} catch (ArgumentException ae) {
Debug.WriteLine("Failed to parse '" + valueStr + "' (enum " + typeof(T) + "): " +
ae.Message);
return defaultValue;
}
}

public void SetOptionEnum<T>(string name, T value) {
if (value == null) {
throw new NotImplementedException("Can't handle a null-valued enum type");
}
string? newVal = Enum.GetName(typeof(T), value);
if (newVal == null) {
Debug.WriteLine("Unable to get enum name type=" + typeof(T) + " value=" + value);
return;
}
if (!mOptions.TryGetValue(name, out string? oldValue) || oldValue != newVal) {
mOptions[name] = newVal;
}
}


public void LogD(string msg) {
Debug.WriteLine("MsgD: " + msg);
Expand Down
16 changes: 12 additions & 4 deletions CommonUtil/CassetteDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,17 +190,18 @@ private CassetteDecoder(WAVFile waveFile, Algorithm alg) {
/// Decodes a stream of audio samples into data chunks.
/// </summary>
/// <param name="wavFile">Processed RIFF file with WAVE data (.wav).</param>
/// <param name="firstOnly">If true, stop when the first good chunk is found.</param>
/// <returns>List of chunks, in the order in which they appear in the sound file.</returns>
public static List<Chunk> DecodeFile(WAVFile wavFile, Algorithm alg) {
public static List<Chunk> DecodeFile(WAVFile wavFile, Algorithm alg, bool firstOnly) {
CassetteDecoder decoder = new CassetteDecoder(wavFile, alg);
decoder.Scan();
decoder.Scan(firstOnly);
return decoder.mChunks;
}

/// <summary>
/// Scans the contents of the audio file, generating file chunks as it goes.
/// </summary>
private void Scan() {
private void Scan(bool firstOnly) {
Debug.Assert(mWavFile.FormatTag == WAVFile.WAVE_FORMAT_PCM);
Debug.WriteLine("Scanning file: " + mWavFile.GetInfoString());

Expand Down Expand Up @@ -284,13 +285,20 @@ private void Scan() {
chunk = new Chunk(fileData, (byte)readChecksum, checksum,
bitAcc != 1, mDataStart, mDataEnd);
}
Debug.WriteLine("READ: " + chunk.ToString());
Debug.WriteLine("Found: " + chunk.ToString());
mChunks.Add(chunk);
doInit = true;
if (firstOnly) {
break;
}
}
}

curSampleIndex += count;

if (firstOnly && mChunks.Count > 0) {
break;
}
}

switch (mState) {
Expand Down
2 changes: 1 addition & 1 deletion DiskArc/Arc/AudioRecording-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- CiderPress implementation
(https://github.com/fadden/ciderpress/blob/master/app/CassetteDialog.cpp)

There is a substantial library of Apple II cassette tapes at
There is a substantial library of Apple II cassette tape recordings at
https://brutaldeluxe.fr/projects/cassettes/.

## Background ##
Expand Down
187 changes: 183 additions & 4 deletions DiskArc/Arc/AudioRecording.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,14 @@ IEnumerator IEnumerable.GetEnumerator() {
public static bool TestKind(Stream stream, AppHook appHook) {
stream.Position = 0;
WAVFile? wav = WAVFile.Prepare(stream);
return (wav != null);
if (wav == null) {
return false;
}
// Scan for the first good entry. No need to process the entire file.
CassetteDecoder.Algorithm alg = appHook.GetOptionEnum(DAAppHook.AUDIO_REC_ALG,
CassetteDecoder.Algorithm.Zero);
List<CassetteDecoder.Chunk> chunks = CassetteDecoder.DecodeFile(wav, alg, true);
return chunks.Count != 0;
}

/// <summary>
Expand All @@ -123,12 +130,19 @@ public static AudioRecording OpenArchive(Stream stream, AppHook appHook) {
stream.Position = 0;
WAVFile? wav = WAVFile.Prepare(stream);
if (wav == null) {
throw new NotSupportedException("Not a supported WAV file format");
throw new NotSupportedException("not a supported WAV file format");
}
AudioRecording archive = new AudioRecording(stream, wav, appHook);
List<CassetteDecoder.Chunk> chunks = CassetteDecoder.DecodeFile(wav,

// Allow the default algorithm to be overridden.
CassetteDecoder.Algorithm alg = appHook.GetOptionEnum(DAAppHook.AUDIO_REC_ALG,
CassetteDecoder.Algorithm.Zero);
// TODO - analyze contents
List<CassetteDecoder.Chunk> chunks = CassetteDecoder.DecodeFile(wav, alg, false);
if (chunks.Count == 0) {
// Doesn't appear to be an Apple II recording.
throw new NotSupportedException("no Apple II recordings found");
}
archive.GenerateRecords(chunks);
return archive;
}

Expand All @@ -155,6 +169,171 @@ protected virtual void Dispose(bool disposing) {
IsValid = false;
}

/// <summary>
/// Generates file records from the decoded audio data chunks. Files will be treated
/// as binary blobs unless we identify what looks like a paired entry.
/// </summary>
private void GenerateRecords(List<CassetteDecoder.Chunk> chunks) {
// Generate info for all chunks, for debugging.
for (int i = 0; i < chunks.Count; i++) {
CassetteDecoder.Chunk chunk = chunks[i];
Notes.AddI("#" + i + ": " + chunk.ToString());
}

for (int i = 0; i < chunks.Count; i++) {
CassetteDecoder.Chunk chunk = chunks[i];
byte fileType;
if (i == chunks.Count - 1) {
// Last chunk.
fileType = FileAttribs.FILE_TYPE_NON;
} else if (chunk.Data.Length == 2) {
// Could be followed by Integer BASIC program or Applesoft shape table.
ushort len = RawData.GetU16LE(chunk.Data, 0);
if (CheckINT(len, chunks[i + 1])) {
fileType = FileAttribs.FILE_TYPE_INT;
} else if (CheckShape(len, chunks[i + 1])) {
fileType = FileAttribs.FILE_TYPE_BIN;
} else {
fileType = FileAttribs.FILE_TYPE_NON;
}
} else if (chunk.Data.Length == 3) {
// Could be followed by Applesoft BASIC program or stored array.
ushort len = RawData.GetU16LE(chunk.Data, 0);
byte xtra = chunk.Data[2];
if (CheckBAS(len, xtra, chunks[i + 1])) {
fileType = FileAttribs.FILE_TYPE_BAS;
} else {
// TODO: find or create examples of VAR tapes, and figure out how to
// determine validity.
fileType = FileAttribs.FILE_TYPE_NON;
}
} else {
fileType = FileAttribs.FILE_TYPE_NON;
}
ushort auxType;
switch (fileType) {
case FileAttribs.FILE_TYPE_BIN:
auxType = 0x1000;
i++;
break;
case FileAttribs.FILE_TYPE_INT:
auxType = 0x0000;
i++;
break;
case FileAttribs.FILE_TYPE_BAS:
auxType = 0x0801;
i++;
break;
case FileAttribs.FILE_TYPE_VAR:
auxType = 0x0000;
i++;
break;
case FileAttribs.FILE_TYPE_NON:
default:
fileType = FileAttribs.FILE_TYPE_BIN;
auxType = 0x1000;
break;
}
AudioRecording_FileEntry newEntry = new AudioRecording_FileEntry(this,
"File" + RecordList.Count.ToString("D2"), fileType, auxType, chunks[i].Data);
if (chunk.CalcChecksum != 0x00 || chunk.BadEnd) {
newEntry.IsDubious = true;
}
RecordList.Add(newEntry);
}
}

/// <summary>
/// Detects whether a chunk holds an Integer BASIC program.
/// </summary>
private static bool CheckINT(ushort len, CassetteDecoder.Chunk chunk) {
Debug.WriteLine("CheckINT: decLen=" + len + " chunkLen=" + chunk.Data.Length);
if (chunk.Data.Length < 4) {
// Too short to be BASIC.
return false;
}

// Check to see if the start looks like the first line of a BASIC program.
byte lineLen = chunk.Data[0];
int lineNum = RawData.GetU16LE(chunk.Data, 1);
if (lineNum > 32767 || lineLen > chunk.Data.Length || chunk.Data[lineLen - 1] != 0x01) {
// Doesn't look like INT.
return false;
}
return true;
}

/// <summary>
/// Detects whether a chunk holds an Applesoft shape table.
/// This test is fairly weak, and should only be made after the INT test fails.
/// </summary>
private static bool CheckShape(ushort len, CassetteDecoder.Chunk chunk) {
// Shape tables are difficult to identify. Do a few simple tests.
if (chunk.Data.Length < 2) {
return false;
}
int shapeCount = chunk.Data[0];
if (shapeCount == 0 || shapeCount * 2 >= chunk.Data.Length) {
// Empty table, or offset table exceeds length of file.
return false;
}

// It's not totally wrong, it has a two-byte header, it's not INT, and we're
// going to call it BIN either way. Declare success.
return true;
}

/// <summary>
/// Detects whether a chunk holds an Applesoft BASIC program.
/// </summary>
private static bool CheckBAS(ushort len, byte xtra, CassetteDecoder.Chunk chunk) {
// Declared length in header chunk should be one shorter than actual length.
Debug.WriteLine("CheckBAS: decLen=" + len + " chunkLen=" + chunk.Data.Length +
" xtra=$" + xtra.ToString("x2"));

if (chunk.Data.Length < 4) {
// Too short to be valid or useful.
return false;
}

//
// We do a quick test where we look at the first two lines of the program, and see
// if they look coherent.
//
// Start with trivial checks on the next-address pointer and line number.
//
ushort nextAddr1 = RawData.GetU16LE(chunk.Data, 0);
ushort lineNum1 = RawData.GetU16LE(chunk.Data, 2);
if (nextAddr1 == 0 || lineNum1 > 63999) {
// Empty or invalid. Assume not BAS.
return false;
}
// Find the end of the line.
int idx;
for (idx = 4; idx < chunk.Data.Length; idx++) {
if (chunk.Data[idx] == 0x00) {
break;
}
}
idx++;
if (idx + 2 == chunk.Data.Length) {
// At end of the file... is this a one-liner?
return false; // TODO - make a test case
} else if (idx + 4 < chunk.Data.Length) {
ushort nextAddr2 = RawData.GetU16LE(chunk.Data, idx);
ushort lineNum2 = RawData.GetU16LE(chunk.Data, idx + 2);
if (lineNum2 <= lineNum1 || lineNum2 > 63999) {
// Invalid line structure.
return false;
}
if (nextAddr2 <= nextAddr1) {
// Pointers moving backward.
return false;
}
}
return true;
}

// IArchive
public ArcReadStream OpenPart(IFileEntry ientry, FilePart part) {
if (!IsValid) {
Expand Down
Loading

0 comments on commit 61f8754

Please sign in to comment.