Sfoglia il codice sorgente

Merge pull request #207 from saybur/audio-cdrom

Experimental audio playback support from BIN/CUE files, courtesy of @saybur
Alex Perez 2 anni fa
parent
commit
20ed645528

+ 53 - 0
lib/SCSI2SD/src/firmware/mode.c

@@ -220,6 +220,33 @@ static const uint8_t ControlModePage[] =
 0x00, 0x00 // AEN holdoff period.
 };
 
+#ifdef ENABLE_AUDIO_OUTPUT
+static const uint8_t CDROMCDParametersPage[] =
+{
+0x0D, // page code
+0x06, // page length
+0x00, // reserved
+0x0D, // reserved, inactivity time 8 min
+0x00, 0x3C, // 60 seconds per MSF M unit
+0x00, 0x4B  // 75 frames per MSF S unit
+};
+
+static const uint8_t CDROMAudioControlParametersPage[] =
+{
+0x0E, // page code
+0x0E, // page length
+0x04, // 'Immed' bit set, 'SOTC' bit not set
+0x00, // reserved
+0x00, // reserved
+0x80, // 1 LBAs/sec multip
+0x00, 0x4B, // 75 LBAs/sec
+0x03, 0xFF, // output port 0 active, max volume
+0x03, 0xFF, // output port 1 active, max volume
+0x00, 0x00, // output port 2 inactive
+0x00, 0x00 // output port 3 inactive
+};
+#endif
+
 static const uint8_t SequentialDeviceConfigPage[] =
 {
 0x10, // page code
@@ -496,6 +523,32 @@ static void doModeSense(
 		idx += sizeof(ControlModePage);
 	}
 
+#ifdef ENABLE_AUDIO_OUTPUT
+	if ((scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
+		&& (pageCode == 0x0D || pageCode == 0x3F))
+	{
+		pageFound = 1;
+		pageIn(
+			pc,
+			idx,
+			CDROMCDParametersPage,
+			sizeof(CDROMCDParametersPage));
+		idx += sizeof(CDROMCDParametersPage);
+	}
+
+	if ((scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
+		&& (pageCode == 0x0E || pageCode == 0x3F))
+	{
+		pageFound = 1;
+		pageIn(
+			pc,
+			idx,
+			CDROMAudioControlParametersPage,
+			sizeof(CDROMAudioControlParametersPage));
+		idx += sizeof(CDROMAudioControlParametersPage);
+	}
+#endif
+
 	if ((scsiDev.target->cfg->deviceType == S2S_CFG_SEQUENTIAL) &&
 		(pageCode == 0x10 || pageCode == 0x3F))
 	{

+ 89 - 42
lib/ZuluSCSI_platform_RP2040/audio.cpp

@@ -24,14 +24,11 @@
 #include <hardware/spi.h>
 #include <pico/multicore.h>
 #include "audio.h"
+#include "ZuluSCSI_audio.h"
 #include "ZuluSCSI_config.h"
 #include "ZuluSCSI_log.h"
 #include "ZuluSCSI_platform.h"
 
-#ifdef __cplusplus
-extern "C" {
-#endif
-
 extern SdFs SD;
 
 // Table with the number of '1' bits for each index.
@@ -134,11 +131,18 @@ static uint16_t wire_buf_a[WIRE_BUFFER_SIZE];
 static uint16_t wire_buf_b[WIRE_BUFFER_SIZE];
 
 // tracking for audio playback
-static bool audio_active = false;
-static volatile bool audio_stopping = false;
-static FsFile audio_file;
+static uint8_t audio_owner; // SCSI ID or 0xFF when idle
+static volatile bool audio_paused = false;
+static ImageBackingStore* audio_file;
+static uint64_t fpos;
 static uint32_t fleft;
 
+// historical playback status information
+static audio_status_code audio_last_status[8] = {ASC_NO_STATUS};
+
+// mechanism for cleanly stopping DMA units
+static volatile bool audio_stopping = false;
+
 // trackers for the below function call
 static uint16_t sfcnt = 0; // sub-frame count; 2 per frame, 192 frames/block
 static uint8_t invert = 0; // biphase encode help: set if last wire bit was '1'
@@ -318,7 +322,11 @@ void audio_dma_irq() {
 }
 
 bool audio_is_active() {
-    return audio_active;
+    return audio_owner != 0xFF;
+}
+
+bool audio_is_playing(uint8_t id) {
+    return audio_owner == (id & 7);
 }
 
 void audio_setup() {
@@ -338,14 +346,20 @@ void audio_setup() {
 }
 
 void audio_poll() {
-    if (!audio_active) return;
+    if (!audio_is_active()) return;
+    if (audio_paused) return;
     if (fleft == 0 && sbufst_a == STALE && sbufst_b == STALE) {
         // out of data and ready to stop
-        audio_stop();
+        audio_stop(audio_owner);
         return;
     } else if (fleft == 0) {
         // out of data to read but still working on remainder
         return;
+    } else if (!audio_file->isOpen()) {
+        // closed elsewhere, maybe disk ejected?
+        dbgmsg("------ Playback stop due to closed file");
+        audio_stop(audio_owner);
+        return;
     }
 
     // are new audio samples needed from the memory card?
@@ -364,9 +378,19 @@ void audio_poll() {
     platform_set_sd_callback(NULL, NULL);
     uint16_t toRead = AUDIO_BUFFER_SIZE;
     if (fleft < toRead) toRead = fleft;
-    if (audio_file.read(audiobuf, toRead) != toRead) {
+    if (audio_file->position() != fpos) {
+        // should be uncommon due to SCSI command restrictions on devices
+        // playing audio; if this is showing up in logs a different approach
+        // will be needed to avoid seek performance issues on FAT32 vols
+        dbgmsg("------ Audio seek required on ", audio_owner);
+        if (!audio_file->seek(fpos)) {
+            logmsg("Audio error, unable to seek to ", fpos, ", ID:", audio_owner);
+        }
+    }
+    if (audio_file->read(audiobuf, toRead) != toRead) {
         logmsg("Audio sample data underrun");
     }
+    fpos += toRead;
     fleft -= toRead;
 
     if (sbufst_a == FILLING) {
@@ -376,62 +400,69 @@ void audio_poll() {
     }
 }
 
-bool audio_play(const char* file, uint64_t start, uint64_t end, bool swap) {
+bool audio_play(uint8_t owner, ImageBackingStore* img, uint64_t start, uint64_t end, bool swap) {
     // stop any existing playback first
-    if (audio_active) audio_stop();
+    if (audio_is_active()) audio_stop(audio_owner);
 
     // dbgmsg("Request to play ('", file, "':", start, ":", end, ")");
 
     // verify audio file is present and inputs are (somewhat) sane
+    if (owner == 0xFF) {
+        logmsg("Illegal audio owner");
+        return false;
+    }
     if (start >= end) {
         logmsg("Invalid range for audio (", start, ":", end, ")");
         return false;
     }
     platform_set_sd_callback(NULL, NULL);
-    audio_file = SD.open(file, O_RDONLY);
-    if (!audio_file.isOpen()) {
-        logmsg("Unable to open file for audio playback: ", file);
+    audio_file = img;
+    if (!audio_file->isOpen()) {
+        logmsg("File not open for audio playback, ", owner);
         return false;
     }
-    uint64_t len = audio_file.size();
-    if (start > len || end > len) {
-        logmsg("File '", file, "' playback request (",
-                start, ":", end, ":", len, ") outside bounds");
-        audio_file.close();
+    uint64_t len = audio_file->size();
+    if (start > len) {
+        logmsg("File playback request start (", start, ":", len, ") outside file bounds");
         return false;
     }
+    // truncate playback end to end of file
+    // we will not consider this to be an error at the moment
+    if (end > len) {
+        dbgmsg("------ Truncate audio play request end ", end, " to file size ", len);
+        end = len;
+    }
     fleft = end - start;
     if (fleft <= 2 * AUDIO_BUFFER_SIZE) {
-        logmsg("File '", file, "' playback request (",
-                start, ":", end, ") too short");
-        audio_file.close();
+        logmsg("File playback request (", start, ":", end, ") too short");
         return false;
     }
 
     // read in initial sample buffers
-    if (!audio_file.seek(start)) {
-        logmsg("Sample file (", file, ") failed start seek to ", start);
-        audio_file.close();
+    if (!audio_file->seek(start)) {
+        logmsg("Sample file failed start seek to ", start);
         return false;
     }
-    if (audio_file.read(sample_buf_a, AUDIO_BUFFER_SIZE) != AUDIO_BUFFER_SIZE) {
-        logmsg("File '", file, "' playback start returned fewer bytes than allowed");
-        audio_file.close();
+    if (audio_file->read(sample_buf_a, AUDIO_BUFFER_SIZE) != AUDIO_BUFFER_SIZE) {
+        logmsg("File playback start returned fewer bytes than allowed");
         return false;
     }
-    if (audio_file.read(sample_buf_b, AUDIO_BUFFER_SIZE) != AUDIO_BUFFER_SIZE) {
-        logmsg("File '", file, "' playback start returned fewer bytes than allowed");
-        audio_file.close();
+    if (audio_file->read(sample_buf_b, AUDIO_BUFFER_SIZE) != AUDIO_BUFFER_SIZE) {
+        logmsg("File playback start returned fewer bytes than allowed");
         return false;
     }
 
     // prepare initial tracking state
+    fpos = audio_file->position();
     fleft -= AUDIO_BUFFER_SIZE * 2;
     sbufsel = A;
     sbufpos = 0;
     sbufswap = swap;
     sbufst_a = READY;
     sbufst_b = READY;
+    audio_owner = owner & 7;
+    audio_last_status[audio_owner] = ASC_PLAYING;
+    audio_paused = false;
 
     // prepare the wire buffers
     for (uint16_t i = 0; i < WIRE_BUFFER_SIZE; i++) {
@@ -465,12 +496,25 @@ bool audio_play(const char* file, uint64_t start, uint64_t end, bool swap) {
 
     // ready to go
     dma_channel_start(SOUND_DMA_CHA);
-    audio_active = true;
     return true;
 }
 
-void audio_stop() {
-    if (!audio_active) return;
+bool audio_set_paused(uint8_t id, bool paused) {
+    if (audio_owner != (id & 7)) return false;
+    else if (audio_paused && paused) return false;
+    else if (!audio_paused && !paused) return false;
+
+    audio_paused = paused;
+    if (paused) {
+        audio_last_status[audio_owner] = ASC_PAUSED;
+    } else {
+        audio_last_status[audio_owner] = ASC_PLAYING;
+    }
+    return true;
+}
+
+void audio_stop(uint8_t id) {
+    if (audio_owner != (id & 7)) return;
 
     // to help mute external hardware, send a bunch of '0' samples prior to
     // halting the datastream; easiest way to do this is invalidating the
@@ -487,14 +531,17 @@ void audio_stop() {
     audio_stopping = false;
 
     // idle the subsystem
-    if (audio_file.isOpen()) {
-        audio_file.close();
-    }
-    audio_active = false;
+    audio_last_status[audio_owner] = ASC_COMPLETED;
+    audio_paused = false;
+    audio_owner = 0xFF;
 }
 
-#ifdef __cplusplus
+audio_status_code audio_get_status_code(uint8_t id) {
+    audio_status_code tmp = audio_last_status[id & 7];
+    if (tmp == ASC_COMPLETED || tmp == ASC_ERRORED) {
+        audio_last_status[id & 7] = ASC_NO_STATUS;
+    }
+    return tmp;
 }
-#endif
 
 #endif // ENABLE_AUDIO_OUTPUT

+ 1 - 25
lib/ZuluSCSI_platform_RP2040/audio.h

@@ -18,11 +18,7 @@
 #pragma once
 #ifdef ENABLE_AUDIO_OUTPUT
 
-#include <Arduino.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
+#include <stdint.h>
 
 // audio subsystem DMA channels
 #define SOUND_DMA_CHA 6
@@ -63,24 +59,4 @@ void audio_setup();
  */
 void audio_poll();
 
-/**
- * Begins audio playback for a file.
- *
- * \param file   Path of a file containing PCM samples to play.
- * \param start  Byte offset within file where playback will begin, inclusive.
- * \param end    Byte offset within file where playback will end, exclusive.
- * \param swap   If false, little-endian sample order, otherwise big-endian.
- * \return       True if successful, false otherwise.
- */
-bool audio_play(const char* file, uint64_t start, uint64_t end, bool swap);
-
-/**
- * Stops audio playback.
- */
-void audio_stop();
-
-#ifdef __cplusplus
-}
-#endif
-
 #endif // ENABLE_AUDIO_OUTPUT

+ 12 - 0
src/ImageBackingStore.cpp

@@ -313,3 +313,15 @@ void ImageBackingStore::flush()
         m_fsfile.flush();
     }
 }
+
+uint64_t ImageBackingStore::position()
+{
+    if (!m_israw && !m_isrom)
+    {
+        return m_fsfile.curPosition();
+    }
+    else
+    {
+        return 0;
+    }
+}

+ 4 - 0
src/ImageBackingStore.h

@@ -71,6 +71,10 @@ public:
     // Flush any pending changes to filesystem
     void flush();
 
+    // Gets current position for following read/write operations
+    // Result is only valid for regular files, not raw or flash access
+    uint64_t position();
+
 protected:
     bool m_israw;
     bool m_isrom;

+ 88 - 0
src/ZuluSCSI_audio.h

@@ -0,0 +1,88 @@
+/** 
+ * ZuluSCSI™ - Copyright (c) 2022 Rabbit Hole Computing™
+ * 
+ * ZuluSCSI™ firmware is licensed under the GPL version 3 or any later version. 
+ * 
+ * https://www.gnu.org/licenses/gpl-3.0.html
+ * ----
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version. 
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details. 
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+**/
+
+#pragma once
+
+#include <stdint.h>
+#include "ImageBackingStore.h"
+
+/*
+ * Status codes for audio playback, matching the SCSI 'audio status codes'.
+ *
+ * The first two are for a live condition and will be returned repeatedly. The
+ * following two reflect a historical condition and are only returned once.
+ */
+enum audio_status_code {
+    ASC_PLAYING = 0x11,
+    ASC_PAUSED = 0x12,
+    ASC_COMPLETED = 0x13,
+    ASC_ERRORED = 0x14,
+    ASC_NO_STATUS = 0x15
+};
+
+/**
+ * Indicates whether there is an active playback event for a given target.
+ *
+ * Note: this does not consider pause/resume events: even if audio is paused
+ * this will indicate playback is in progress.
+ *
+ * \param owner  The SCSI ID to check.
+ * \return       True if playback in progress, false if playback idle.
+ */
+bool audio_is_playing(uint8_t id);
+
+/**
+ * Begins audio playback for a file.
+ *
+ * \param owner  The SCSI ID that initiated this playback operation.
+ * \param img    Pointer to the image containing PCM samples to play.
+ * \param start  Byte offset within file where playback will begin, inclusive.
+ * \param end    Byte offset within file where playback will end, exclusive.
+ * \param swap   If false, little-endian sample order, otherwise big-endian.
+ * \return       True if successful, false otherwise.
+ */
+bool audio_play(uint8_t owner, ImageBackingStore* img, uint64_t start, uint64_t end, bool swap);
+
+/**
+ * Pauses audio playback. This may be delayed slightly to allow sample buffers
+ * to purge.
+ *
+ * \param id     The SCSI ID to pause audio playback on.
+ * \param pause  If true, pause, otherwise resume.
+ * \return       True if operation changed audio output, false if no change.
+ */
+bool audio_set_paused(uint8_t id, bool pause);
+
+/**
+ * Stops audio playback.
+ *
+ * \param id     The SCSI ID to stop audio playback on.
+ */
+void audio_stop(uint8_t id);
+
+/**
+ * Provides SCSI 'audio status code' for the given target. Depending on the
+ * code this operation may produce side-effects, see the enum for details.
+ *
+ * \param id    The SCSI ID to provide status codes for.
+ * \return      The matching audio status code.
+ */
+audio_status_code audio_get_status_code(uint8_t id);

+ 266 - 51
src/ZuluSCSI_cdrom.cpp

@@ -33,6 +33,9 @@
 #include "ZuluSCSI_config.h"
 #include <CUEParser.h>
 #include <assert.h>
+#ifdef ENABLE_AUDIO_OUTPUT
+#include "ZuluSCSI_audio.h"
+#endif
 
 extern "C" {
 #include <scsi.h>
@@ -686,6 +689,12 @@ static void doReadFullTOC(int convertBCD, uint8_t session, uint16_t allocationLe
 void doReadHeader(bool MSF, uint32_t lba, uint16_t allocationLength)
 {
     image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+
+#if ENABLE_AUDIO_OUTPUT
+    // terminate audio playback if active on this target (Annex C)
+    audio_stop(img.scsiId & 7);
+#endif
+
     CUEParser parser;
     if (!loadCueSheet(img, parser))
     {
@@ -841,6 +850,13 @@ bool cdromSwitchNextImage(image_config_t &img)
         scsiDiskGetImageNameFromConfig(img, filename, sizeof(filename));
     }
 
+#ifdef ENABLE_AUDIO_OUTPUT
+    // if in progress for this device, terminate audio playback immediately (Annex C)
+    audio_stop(target_idx);
+    // Reset position tracking for the new image
+    audio_get_status_code(target_idx); // trash audio status code
+#endif
+
     if (filename[0] != '\0')
     {
         logmsg("Switching to next CD-ROM image for ", target_idx, ": ", filename);
@@ -906,45 +922,168 @@ static void doGetEventStatusNotification(bool immed)
 /* CD-ROM audio playback              */
 /**************************************/
 
-// TODO: where to store these? Should be updated as playback progresses.
-static CDROMAudioPlaybackStatus g_cdrom_audio_status;
-static uint32_t g_cdrom_audio_lba;
-
-void cdromGetAudioPlaybackStatus(CDROMAudioPlaybackStatus *status, uint32_t *current_lba)
+void cdromGetAudioPlaybackStatus(uint8_t *status, uint32_t *current_lba, bool current_only)
 {
-    if (status) *status = g_cdrom_audio_status;
-    if (current_lba) *current_lba = g_cdrom_audio_lba;
+    image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+
+#ifdef ENABLE_AUDIO_OUTPUT
+    if (status) {
+        uint8_t target = img.scsiId & 7;
+        if (current_only) {
+            *status = audio_is_playing(target) ? 1 : 0;
+        } else {
+            *status = (uint8_t) audio_get_status_code(target);
+        }
+    }
+#else
+    if (status) *status = 0; // audio status code for 'unsupported/invalid' and not-playing indicator
+#endif
+    if (current_lba)
+    {
+        if (img.file.isOpen()) {
+            *current_lba = img.file.position() / 2352;
+        } else {
+            *current_lba = 0;
+        }
+    }
 }
 
 static void doPlayAudio(uint32_t lba, uint32_t length)
 {
-    logmsg("------ CD-ROM Play Audio starting at ", lba, " for ", length, " sectors");
+#ifdef ENABLE_AUDIO_OUTPUT
+    dbgmsg("------ CD-ROM Play Audio request at ", lba, " for ", length, " sectors");
+    image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+    uint8_t target_id = img.scsiId & 7;
 
-    g_cdrom_audio_lba = lba;
-    g_cdrom_audio_status = CDROMAudio_Stopped; // Playback not actually implemented
+    // Per Annex C terminate playback immediately if already in progress on
+    // the current target. Non-current targets may also get their audio
+    // interrupted later due to hardware limitations
+    audio_stop(img.scsiId & 7);
 
-    scsiDev.status = 0;
+    // if transfer length is zero no audio playback happens.
+    // don't treat as an error per SCSI-2; handle via short-circuit
+    if (length == 0)
+    {
+        scsiDev.status = 0;
+        scsiDev.phase = STATUS;
+        return;
+    }
+
+    // if actual playback is requested perform steps to verify prior to playback
+    CUEParser parser;
+    if (loadCueSheet(img, parser))
+    {
+        CUETrackInfo trackinfo = {};
+        getTrackFromLBA(parser, lba, &trackinfo);
+
+        if (lba == 0xFFFFFFFF)
+        {
+            // request to start playback from 'current position'
+            lba = img.file.position() / 2352;
+        }
+
+        // --- TODO --- determine proper track offset, software I tested with had a tendency
+        // to ask for offsets that seem to hint at 2048 here, not the 2352 you'd assume.
+        // Might be due to a mode page reporting something unexpected? Needs investigation.
+        uint64_t offset = trackinfo.file_offset + 2048 * (lba - trackinfo.data_start);
+        dbgmsg("------ Play audio CD: ", (int)length, " sectors starting at ", (int)lba,
+           ", track number ", trackinfo.track_number, ", data offset in file ", (int)offset);
+
+        if (trackinfo.track_mode != CUETrack_AUDIO)
+        {
+            dbgmsg("---- Host tried audio playback on track type ", (int)trackinfo.track_mode);
+            scsiDev.status = CHECK_CONDITION;
+            scsiDev.target->sense.code = ILLEGAL_REQUEST;
+            scsiDev.target->sense.asc = 0x6400; // ILLEGAL MODE FOR THIS TRACK
+            scsiDev.phase = STATUS;
+            return;
+        }
+
+        // playback request appears to be sane, so perform it
+        // see earlier note for context on the block length below
+        if (!audio_play(target_id, &(img.file), offset, offset + length * 2048, false))
+        {
+            // Underlying data/media error? Fake a disk scratch, which should
+            // be a condition most CD-DA players are expecting
+            scsiDev.status = CHECK_CONDITION;
+            scsiDev.target->sense.code = MEDIUM_ERROR;
+            scsiDev.target->sense.asc = 0x1106; // CIRC UNRECOVERED ERROR
+            scsiDev.phase = STATUS;
+            return;
+        }
+        scsiDev.status = 0;
+        scsiDev.phase = STATUS;
+    }
+    else
+    {
+        // virtual drive supports audio, just not with this disk image
+        dbgmsg("---- Request to play audio on non-audio image");
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = 0x6400; // ILLEGAL MODE FOR THIS TRACK
+        scsiDev.phase = STATUS;
+    }
+#else
+    dbgmsg("---- Target does not support audio playback");
+    // per SCSI-2, targets not supporting audio respond to zero-length
+    // PLAY AUDIO commands with ILLEGAL REQUEST; this seems to be a check
+    // performed by at least some audio playback software
+    scsiDev.status = CHECK_CONDITION;
+    scsiDev.target->sense.code = ILLEGAL_REQUEST;
+    scsiDev.target->sense.asc = 0x0000; // NO ADDITIONAL SENSE INFORMATION
     scsiDev.phase = STATUS;
+#endif
 }
 
 static void doPauseResumeAudio(bool resume)
 {
+#ifdef ENABLE_AUDIO_OUTPUT
     logmsg("------ CD-ROM ", resume ? "resume" : "pause", " audio playback");
+    image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+    uint8_t target_id = img.scsiId & 7;
 
-    scsiDev.status = 0;
+    if (audio_is_playing(target_id))
+    {
+        audio_set_paused(target_id, !resume);
+        scsiDev.status = 0;
+        scsiDev.phase = STATUS;
+    }
+    else
+    {
+        scsiDev.status = CHECK_CONDITION;
+        scsiDev.target->sense.code = ILLEGAL_REQUEST;
+        scsiDev.target->sense.asc = 0x2C00; // COMMAND SEQUENCE ERROR
+        scsiDev.phase = STATUS;
+    }
+#else
+    dbgmsg("---- Target does not support audio pausing");
+    scsiDev.status = CHECK_CONDITION;
+    scsiDev.target->sense.code = ILLEGAL_REQUEST; // assumed from PLAY AUDIO(10)
+    scsiDev.target->sense.asc = 0x0000; // NO ADDITIONAL SENSE INFORMATION
     scsiDev.phase = STATUS;
+#endif
+}
+
+static void doStopAudio()
+{
+    dbgmsg("------ CD-ROM Stop Audio request");
+#ifdef ENABLE_AUDIO_OUTPUT
+    image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+    uint8_t target_id = img.scsiId & 7;
+    audio_stop(target_id);
+#endif
 }
 
 static void doMechanismStatus(uint16_t allocation_length)
 {
     uint8_t *buf = scsiDev.data;
 
-    CDROMAudioPlaybackStatus status;
+    uint8_t status;
     uint32_t lba;
-    cdromGetAudioPlaybackStatus(&status, &lba);
+    cdromGetAudioPlaybackStatus(&status, &lba, true);
 
     *buf++ = 0x00; // No fault state
-    *buf++ = (status == CDROMAudio_Playing) ? 0x20 : 0x00; // Currently playing?
+    *buf++ = (status) ? 0x20 : 0x00; // Currently playing?
     *buf++ = (lba >> 16) & 0xFF;
     *buf++ = (lba >> 8) & 0xFF;
     *buf++ = (lba >> 0) & 0xFF;
@@ -967,6 +1106,12 @@ static void doReadCD(uint32_t lba, uint32_t length, uint8_t sector_type,
                      uint8_t main_channel, uint8_t sub_channel)
 {
     image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+
+#if ENABLE_AUDIO_OUTPUT
+    // terminate audio playback if active on this target (Annex C)
+    audio_stop(img.scsiId & 7);
+#endif
+
     CUEParser parser;
     if (!loadCueSheet(img, parser)
         && (sector_type == 0 || sector_type == 2)
@@ -1172,9 +1317,9 @@ static void doReadSubchannel(bool time, bool subq, uint8_t parameter, uint8_t tr
 
     if (parameter == 0x01)
     {
-        CDROMAudioPlaybackStatus audiostatus;
+        uint8_t audiostatus;
         uint32_t lba;
-        cdromGetAudioPlaybackStatus(&audiostatus, &lba);
+        cdromGetAudioPlaybackStatus(&audiostatus, &lba, false);
         dbgmsg("------ Get audio playback position: status ", (int)audiostatus, " lba ", (int)lba);
 
         // Fetch current track info
@@ -1186,32 +1331,54 @@ static void doReadSubchannel(bool time, bool subq, uint8_t parameter, uint8_t tr
 
         // Request sub channel data at current playback position
         *buf++ = 0; // Reserved
+        *buf++ = audiostatus;
 
-        switch (audiostatus)
+        int len;
+        if (subq)
         {
-            case CDROMAudio_Playing: *buf++ = 0x11; break;
-            case CDROMAudio_Paused:  *buf++ = 0x12; break;
-            case CDROMAudio_Stopped: *buf++ = 0x13; break;
-            default: *buf++ = 0; break;
-        }
+            len = 12;
+            *buf++ = 0;  // Subchannel data length (MSB)
+            *buf++ = len; // Subchannel data length (LSB)
+            *buf++ = 0x01; // Subchannel data format
+            *buf++ = (trackinfo.track_mode == CUETrack_AUDIO ? 0x10 : 0x14);
+            *buf++ = trackinfo.track_number;
+            *buf++ = (lba >= trackinfo.data_start) ? 1 : 0; // Index number (0 = pregap)
+            if (time)
+            {
+                LBA2MSF(lba, buf);
+                dbgmsg("------ ABS M ", *(buf+1), " S ", *(buf+2), " F ", *(buf+3));
+                *buf += 4;
+            }
+            else
+            {
+                *buf++ = (lba >> 24) & 0xFF; // Absolute block address
+                *buf++ = (lba >> 16) & 0xFF;
+                *buf++ = (lba >>  8) & 0xFF;
+                *buf++ = (lba >>  0) & 0xFF;
+            }
 
-        int len = 12;
-        *buf++ = 0;  // Subchannel data length (MSB)
-        *buf++ = len; // Subchannel data length (LSB)
-        *buf++ = 0x01; // Subchannel data format
-        *buf++ = (trackinfo.track_mode == CUETrack_AUDIO ? 0x10 : 0x14);
-        *buf++ = trackinfo.track_number;
-        *buf++ = (lba >= trackinfo.data_start) ? 1 : 0; // Index number (0 = pregap)
-        *buf++ = (lba >> 24) & 0xFF; // Absolute block address
-        *buf++ = (lba >> 16) & 0xFF;
-        *buf++ = (lba >>  8) & 0xFF;
-        *buf++ = (lba >>  0) & 0xFF;
-
-        uint32_t relpos = (uint32_t)((int32_t)lba - (int32_t)trackinfo.data_start);
-        *buf++ = (relpos >> 24) & 0xFF; // Track relative position (may be negative)
-        *buf++ = (relpos >> 16) & 0xFF;
-        *buf++ = (relpos >>  8) & 0xFF;
-        *buf++ = (relpos >>  0) & 0xFF;
+            uint32_t relpos = (uint32_t)((int32_t)lba - (int32_t)trackinfo.data_start);
+            if (time)
+            {
+                LBA2MSF(relpos, buf);
+                dbgmsg("------ REL M ", *(buf+1), " S ", *(buf+2), " F ", *(buf+3));
+                *buf += 4;
+            }
+            else
+            {
+                *buf++ = (relpos >> 24) & 0xFF; // Track relative position (may be negative)
+                *buf++ = (relpos >> 16) & 0xFF;
+                *buf++ = (relpos >>  8) & 0xFF;
+                *buf++ = (relpos >>  0) & 0xFF;
+            }
+        }
+        else
+        {
+            len = 0;
+            *buf++ = 0;
+            *buf++ = 0;
+        }
+        len += 4;
 
         if (len > allocation_length) len = allocation_length;
         scsiDev.dataLen = len;
@@ -1240,21 +1407,33 @@ extern "C" int scsiCDRomCommand()
     int commandHandled = 1;
 
     uint8_t command = scsiDev.cdb[0];
-    if (command == 0x1B && (scsiDev.cdb[4] & 2))
+    if (command == 0x1B)
     {
-        // CD-ROM load & eject
-        int start = scsiDev.cdb[4] & 1;
-        if (start)
+#if ENABLE_AUDIO_OUTPUT
+        // terminate audio playback if active on this target (Annex C)
+        audio_stop(img.scsiId & 7);
+#endif
+        if ((scsiDev.cdb[4] & 2))
         {
-            dbgmsg("------ CDROM close tray");
-            img.ejected = false;
-            img.cdrom_events = 2; // New media
+            // CD-ROM load & eject
+            int start = scsiDev.cdb[4] & 1;
+            if (start)
+            {
+                dbgmsg("------ CDROM close tray");
+                img.ejected = false;
+                img.cdrom_events = 2; // New media
+            }
+            else
+            {
+                dbgmsg("------ CDROM open tray");
+                img.ejected = true;
+                img.cdrom_events = 3; // Media removal
+            }
         }
         else
         {
-            dbgmsg("------ CDROM open tray");
-            img.ejected = true;
-            img.cdrom_events = 3; // Media removal
+            // flow through to disk handler
+            commandHandled = 0;
         }
     }
     else if (command == 0x43)
@@ -1344,7 +1523,18 @@ extern "C" int scsiCDRomCommand()
         uint32_t start = (scsiDev.cdb[3] * 60 + scsiDev.cdb[4]) * 75 + scsiDev.cdb[5];
         uint32_t end   = (scsiDev.cdb[6] * 60 + scsiDev.cdb[7]) * 75 + scsiDev.cdb[8];
 
-        doPlayAudio(start, end - start);
+        uint32_t lba = start;
+        if (scsiDev.cdb[3] == 0xFF
+                && scsiDev.cdb[4] == 0xFF
+                && scsiDev.cdb[5] == 0xFF)
+        {
+            // request to start playback from 'current position'
+            image_config_t &img = *(image_config_t*)scsiDev.target->cfg;
+            lba = img.file.position() / 2352;
+        }
+
+        uint32_t length = end - lba;
+        doPlayAudio(lba, length);
     }
     else if (command == 0x4B)
     {
@@ -1433,6 +1623,31 @@ extern "C" int scsiCDRomCommand()
 
         doReadCD(lba, blocks, 0, 0x10, 0);
     }
+    else if (command == 0x4E)
+    {
+        // STOP PLAY/SCAN
+        doStopAudio();
+        scsiDev.status = 0;
+        scsiDev.phase = STATUS;
+    }
+    else if (command == 0x01)
+    {
+        // REZERO UNIT
+        // AppleCD Audio Player uses this as a nonstandard
+        // "stop audio playback" command
+        doStopAudio();
+        scsiDev.status = 0;
+        scsiDev.phase = STATUS;
+    }
+    else if (command == 0x0B || command == 0x2B)
+    {
+        // SEEK
+        // implement Annex C termination requirement and pass to disk handler
+        doStopAudio();
+        // this may need more specific handling, the Win9x player appears to
+        // expect a pickup move to the given LBA
+        commandHandled = 0;
+    }
     else
     {
         commandHandled = 0;

+ 3 - 6
src/ZuluSCSI_cdrom.h

@@ -22,9 +22,6 @@ bool cdromSwitchNextImage(image_config_t &img);
 bool cdromValidateCueSheet(image_config_t &img);
 
 // Audio playback status
-enum CDROMAudioPlaybackStatus {
-    CDROMAudio_Stopped = 0,
-    CDROMAudio_Playing = 1,
-    CDROMAudio_Paused = 2
-};
-void cdromGetAudioPlaybackStatus(CDROMAudioPlaybackStatus *status, uint32_t *current_lba);
+// boolean flag is true if just basic mechanism status (playback true/false)
+// is desired, or false if historical audio status codes should be returned
+void cdromGetAudioPlaybackStatus(uint8_t *status, uint32_t *current_lba, bool current_only);

+ 5 - 0
src/ZuluSCSI_log_trace.cpp

@@ -75,14 +75,19 @@ static const char *getCommandName(uint8_t cmd)
         case 0x37: return "ReadDefectData";
         case 0x3B: return "WriteBuffer";
         case 0x3C: return "ReadBuffer";
+        case 0x42: return "CDROM Read SubChannel";
         case 0x43: return "CDROM Read TOC";
         case 0x44: return "CDROM Read Header";
+        case 0x46: return "CDROM GetConfiguration";
         case 0x4A: return "GetEventStatusNotification";
+        case 0x4B: return "CDROM PauseResume";
+        case 0x4E: return "CDROM StopPlayScan";
         case 0x51: return "CDROM ReadDiscInformation";
         case 0x45: return "CDROM PlayAudio10";
         case 0xA5: return "CDROM PlayAudio12";
         case 0x47: return "CDROM PlayAudioMSF";
         case 0x48: return "CDROM PauseResume";
+        case 0x52: return "CDROM ReadTrackInformation";
         case 0xBB: return "CDROM SetCDSpeed";
         case 0xBD: return "CDROM MechanismStatus";
         case 0xBE: return "ReadCD";