Преглед изворни кода

Experimental audio playback support from BIN/CUE files.

saybur пре 2 година
родитељ
комит
42ff3aeef7

+ 72 - 12
lib/BlueSCSI_platform_RP2040/audio.cpp

@@ -134,11 +134,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 uint8_t audio_owner; // SCSI ID or 0xFF when idle
+static volatile bool audio_paused = false;
 static FsFile audio_file;
 static uint32_t fleft;
 
+// historical playback status information
+static audio_status_code audio_last_status[8] = {ASC_NO_STATUS};
+static uint32_t audio_bytes_read[8] = {0};
+
+// 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 +325,15 @@ void audio_dma_irq() {
 }
 
 bool audio_is_active() {
-    return audio_active;
+    return audio_owner != 0xFF;
+}
+
+bool audio_is_paused() {
+    return audio_paused;
+}
+
+uint8_t audio_get_owner() {
+    return audio_owner;
 }
 
 void audio_setup() {
@@ -338,7 +353,8 @@ 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();
@@ -368,6 +384,7 @@ void audio_poll() {
         log("Audio sample data underrun");
     }
     fleft -= toRead;
+    audio_bytes_read[audio_owner] += toRead;
 
     if (sbufst_a == FILLING) {
         sbufst_a = READY;
@@ -376,13 +393,17 @@ void audio_poll() {
     }
 }
 
-bool audio_play(const char* file, uint64_t start, uint64_t end, bool swap) {
+bool audio_play(uint8_t owner, const char* file, uint64_t start, uint64_t end, bool swap) {
     // stop any existing playback first
-    if (audio_active) audio_stop();
+    if (audio_is_active()) audio_stop();
 
     // debuglog("Request to play ('", file, "':", start, ":", end, ")");
 
     // verify audio file is present and inputs are (somewhat) sane
+    if (owner == 0xFF) {
+        log("Illegal audio owner");
+        return false;
+    }
     if (start >= end) {
         log("Invalid range for audio (", start, ":", end, ")");
         return false;
@@ -394,12 +415,18 @@ bool audio_play(const char* file, uint64_t start, uint64_t end, bool swap) {
         return false;
     }
     uint64_t len = audio_file.size();
-    if (start > len || end > len) {
-        log("File '", file, "' playback request (",
-                start, ":", end, ":", len, ") outside bounds");
+    if (start > len) {
+        log("File '", file, "' playback request start (",
+                start, ":", len, ") outside file bounds");
         audio_file.close();
         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) {
         log("File '", file, "' playback request (",
@@ -432,6 +459,9 @@ bool audio_play(const char* file, uint64_t start, uint64_t end, bool swap) {
     sbufswap = swap;
     sbufst_a = READY;
     sbufst_b = READY;
+    audio_owner = owner & 7;
+    audio_bytes_read[audio_owner] = AUDIO_BUFFER_SIZE * 2;
+    audio_last_status[audio_owner] = ASC_PLAYING;
 
     // prepare the wire buffers
     for (uint16_t i = 0; i < WIRE_BUFFER_SIZE; i++) {
@@ -465,12 +495,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;
+}
+
+bool audio_set_paused(bool paused) {
+    if (!audio_is_active()) 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() {
-    if (!audio_active) return;
+    if (!audio_is_active()) 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
@@ -490,7 +533,24 @@ void audio_stop() {
     if (audio_file.isOpen()) {
         audio_file.close();
     }
-    audio_active = false;
+    audio_last_status[audio_owner] = ASC_COMPLETED;
+    audio_owner = 0xFF;
+}
+
+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;
+}
+
+uint32_t audio_get_bytes_read(uint8_t id) {
+    return audio_bytes_read[id & 7];
+}
+
+void audio_clear_bytes_read(uint8_t id) {
+    audio_bytes_read[id & 7] = 0;
 }
 
 #ifdef __cplusplus

+ 66 - 1
lib/BlueSCSI_platform_RP2040/audio.h

@@ -32,6 +32,20 @@ extern "C" {
 // these must be divisible by 1024
 #define AUDIO_BUFFER_SIZE 8192 // ~46.44ms
 
+/*
+ * 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
+};
+
 /**
  * Handler for DMA interrupts
  *
@@ -52,6 +66,16 @@ void audio_dma_irq();
  */
 bool audio_is_active();
 
+/**
+ * \return true if audio streaming is paused, false otherwise.
+ */
+bool audio_is_paused();
+
+/**
+ * \return the owner value passed to the _play() call, or 0xFF if no owner.
+ */
+uint8_t audio_get_owner();
+
 /**
  * Initializes the audio subsystem. Should be called only once, toward the end
  * of platform_late_init().
@@ -66,19 +90,60 @@ void audio_poll();
 /**
  * Begins audio playback for a file.
  *
+ * \param owner  The SCSI ID that initiated this playback operation.
  * \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);
+bool audio_play(uint8_t owner, const char* file, uint64_t start, uint64_t end, bool swap);
+
+/**
+ * Pauses audio playback. This may be delayed slightly to allow sample buffers
+ * to purge.
+ *
+ * \param pause  If true, pause, otherwise resume.
+ * \return       True if operation changed audio output, false if no change.
+ */
+bool audio_set_paused(bool pause);
 
 /**
  * Stops audio playback.
  */
 void audio_stop();
 
+/**
+ * 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);
+
+/**
+ * Provides the number of sample bytes read in during an audio_play() call.
+ * This can be combined with an (external) starting offset to determine
+ * virtual CD positioning information. This is only an approximation since
+ * this tracker is always at the end of the most recently read sample data.
+ *
+ * This is intentionally not cleared by audio_stop(): audio_play() events will
+ * reset this information.
+ *
+ * \param id    The SCSI ID target to return data for.
+ * \return      The number of bytes read in during a playback.
+ */
+uint32_t audio_get_bytes_read(uint8_t id);
+
+/**
+ * Clears the byte counter in the above call. This is insensitive to whether
+ * audio playback is occurring but is safe to call in any event.
+ *
+ * \param id    The SCSI ID target to return data for.
+ */
+void audio_clear_bytes_read(uint8_t id);
+
 #ifdef __cplusplus
 }
 #endif

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

@@ -220,6 +220,23 @@ static const uint8_t ControlModePage[] =
 0x00, 0x00 // AEN holdoff period.
 };
 
+#ifdef ENABLE_AUDIO_OUTPUT
+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 +513,21 @@ static void doModeSense(
 		idx += sizeof(ControlModePage);
 	}
 
+#ifdef ENABLE_AUDIO_OUTPUT
+	if ((scsiDev.compatMode >= COMPAT_SCSI2)
+		&& (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))
 	{

+ 146 - 24
src/BlueSCSI_cdrom.cpp

@@ -34,6 +34,9 @@
 #include "BlueSCSI_cdrom.h"
 #include <CUEParser.h>
 #include <assert.h>
+#ifdef ENABLE_AUDIO_OUTPUT
+#include "audio.h"
+#endif
 
 extern "C" {
 #include <scsi.h>
@@ -907,45 +910,171 @@ 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;
+typedef struct {
+    uint32_t last_lba = 0;
+} mechanism_status_t;
+static mechanism_status_t mechanism_status[8];
 
-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;
+    uint8_t target = img.scsiId & 7;
+
+#ifdef ENABLE_AUDIO_OUTPUT
+    if (status) {
+        if (current_only) {
+            *status = audio_get_owner() == target ? 1 : 0;
+        } else {
+            *status = (uint8_t) audio_get_status_code(target);
+        }
+    }
+    if (current_lba) *current_lba = mechanism_status[target].last_lba
+            + audio_get_bytes_read(target) / 2352;
+#elif
+    if (status) *status = 0; // audio status code for 'unsupported/invalid' and not-playing indicator
+    if (current_lba) *current_lba = mechanism_status[target].last_lba;
+#endif
 }
 
 static void doPlayAudio(uint32_t lba, uint32_t length)
 {
-    log("------ CD-ROM Play Audio starting at ", lba, " for ", length, " sectors");
+#ifdef ENABLE_AUDIO_OUTPUT
+    debuglog("------ 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
+    if ((img.scsiId & 7) == audio_get_owner()) {
+        audio_stop();
+    }
 
-    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 = mechanism_status[target_id].last_lba + audio_get_bytes_read(target_id) / 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);
+        debuglog("------ 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)
+        {
+            debuglog("---- 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
+        char filename[MAX_FILE_PATH];
+        if (!img.file.name(filename, sizeof(filename)))
+        {
+            // No underlying file available?
+            log("---- No filename for SCSI ID ", target_id);
+            scsiDev.status = CHECK_CONDITION;
+            scsiDev.target->sense.code = ILLEGAL_REQUEST;
+            scsiDev.target->sense.asc = 0x0000;
+            scsiDev.phase = STATUS;
+            return;
+        }
+        if (!audio_play(target_id, filename, offset, offset + length * 2352, 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;
+        }
+        mechanism_status[target_id].last_lba = lba;
+        scsiDev.status = 0;
+        scsiDev.phase = STATUS;
+    }
+    else
+    {
+        // virtual drive supports audio, just not with this disk image
+        debuglog("---- 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;
+    }
+#elif
+    debuglog("---- 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
     log("------ 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_get_owner() == target_id)
+    {
+        audio_set_paused(!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;
+    }
+#elif
+    debuglog("---- 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 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;
@@ -1173,9 +1302,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);
         debuglog("------ Get audio playback position: status ", (int)audiostatus, " lba ", (int)lba);
 
         // Fetch current track info
@@ -1187,14 +1316,7 @@ static void doReadSubchannel(bool time, bool subq, uint8_t parameter, uint8_t tr
 
         // Request sub channel data at current playback position
         *buf++ = 0; // Reserved
-
-        switch (audiostatus)
-        {
-            case CDROMAudio_Playing: *buf++ = 0x11; break;
-            case CDROMAudio_Paused:  *buf++ = 0x12; break;
-            case CDROMAudio_Stopped: *buf++ = 0x13; break;
-            default: *buf++ = 0; break;
-        }
+        *buf++ = audiostatus;
 
         int len = 12;
         *buf++ = 0;  // Subchannel data length (MSB)

+ 3 - 6
src/BlueSCSI_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);

+ 1 - 0
src/BlueSCSI_log_trace.cpp

@@ -53,6 +53,7 @@ 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 0x4A: return "GetEventStatusNotification";

+ 12 - 0
src/ImageBackingStore.cpp

@@ -299,3 +299,15 @@ void ImageBackingStore::flush()
         m_fsfile.flush();
     }
 }
+
+size_t ImageBackingStore::name(char* filename, size_t len)
+{
+    if (!m_israw && !m_isrom)
+    {
+        return m_fsfile.getName(filename, len);
+    }
+    else
+    {
+        return 0;
+    }
+}

+ 3 - 0
src/ImageBackingStore.h

@@ -71,6 +71,9 @@ public:
     // Flush any pending changes to filesystem
     void flush();
 
+    // If available, read filename and return actual length.
+    size_t name(char* filename, size_t len);
+
 protected:
     bool m_israw;
     bool m_isrom;