Эх сурвалжийг харах

Merge pull request #228 from saybur/cdrom-modes

CD-ROM: volume control and mode changes
Alex Perez 2 жил өмнө
parent
commit
e8e08497ee

+ 19 - 55
lib/SCSI2SD/src/firmware/mode.c

@@ -21,6 +21,7 @@
 #include "mode.h"
 #include "disk.h"
 #include "inquiry.h"
+#include "ZuluSCSI_mode.h"
 
 #include <string.h>
 
@@ -220,33 +221,6 @@ 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
@@ -420,7 +394,8 @@ static void doModeSense(
 		}
 	}
 
-	if (pageCode == 0x03 || pageCode == 0x3F)
+	if ((pageCode == 0x03 || pageCode == 0x3F) &&
+		(scsiDev.target->cfg->deviceType != S2S_CFG_OPTICAL))
 	{
 		pageFound = 1;
 		pageIn(pc, idx, FormatDevicePage, sizeof(FormatDevicePage));
@@ -445,7 +420,8 @@ static void doModeSense(
 		idx += sizeof(FormatDevicePage);
 	}
 
-	if (pageCode == 0x04 || pageCode == 0x3F)
+	if ((pageCode == 0x04 || pageCode == 0x3F) &&
+		(scsiDev.target->cfg->deviceType != S2S_CFG_OPTICAL))
 	{
 		pageFound = 1;
 		if ((scsiDev.compatMode >= COMPAT_SCSI2))
@@ -523,31 +499,8 @@ 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
+	idx += modeSenseCDDevicePage(pc, idx, pageCode, &pageFound);
+	idx += modeSenseCDAudioControlPage(pc, idx, pageCode, &pageFound);
 
 	if ((scsiDev.target->cfg->deviceType == S2S_CFG_SEQUENTIAL) &&
 		(pageCode == 0x10 || pageCode == 0x3F))
@@ -670,10 +623,16 @@ static void doModeSelect(void)
 
 		while (idx < scsiDev.dataLen)
 		{
+			// Change from SCSI2SD: if code page is 0x0 (vendor-specific) it
+			// will not follow the normal page mode format and cannot be
+			// parsed, but isn't necessarily an error. Instead, just treat it
+			// as an 'end of data' field and allow normal command completion.
+			int pageCode = scsiDev.data[idx] & 0x3F;
+			if (pageCode == 0) goto out;
+
 			int pageLen = scsiDev.data[idx + 1];
 			if (idx + 2 + pageLen > scsiDev.dataLen) goto bad;
 
-			int pageCode = scsiDev.data[idx] & 0x3F;
 			switch (pageCode)
 			{
 			case 0x03: // Format Device Page
@@ -699,6 +658,11 @@ static void doModeSelect(void)
 				}
 			}
 			break;
+			case 0x0E: // CD audio control page
+			{
+				if (!modeSelectCDAudioControlPage(pageLen, idx)) goto bad;
+			}
+			break;
 			//default:
 
 				// Easiest to just ignore for now. We'll get here when changing

+ 34 - 16
lib/ZuluSCSI_platform_RP2040/audio.cpp

@@ -140,6 +140,12 @@ static uint32_t fleft;
 // historical playback status information
 static audio_status_code audio_last_status[8] = {ASC_NO_STATUS};
 
+// volume information for targets
+static volatile uint16_t volumes[8] = {
+    DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL,
+    DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL
+};
+
 // mechanism for cleanly stopping DMA units
 static volatile bool audio_stopping = false;
 
@@ -158,31 +164,35 @@ static uint8_t invert = 0; // biphase encode help: set if last wire bit was '1'
  * output.
  */
 static void snd_encode(uint8_t* samples, uint16_t* wire_patterns, uint16_t len, uint8_t swap) {
+    uint16_t wvol = volumes[audio_owner & 7];
+    uint8_t vol = ((wvol >> 8) + (wvol & 0xFF)) >> 1; // average of both values
+    // limit maximum volume; with my DACs I've had persistent issues
+    // with signal clipping when sending data in the highest bit position
+    vol = vol >> 2;
+
     uint16_t widx = 0;
     for (uint16_t i = 0; i < len; i += 2) {
         uint32_t sample = 0;
         uint8_t parity = 0;
         if (samples != NULL) {
+            int32_t rsamp;
             if (swap) {
-                sample = samples[i + 1] + (samples[i] << 8);
+                rsamp = (int16_t)(samples[i + 1] + (samples[i] << 8));
             } else {
-                sample = samples[i] + (samples[i + 1] << 8);
+                rsamp = (int16_t)(samples[i] + (samples[i + 1] << 8));
             }
-            // determine parity, simplified to one lookup via an XOR
-            parity = (sample >> 8) ^ sample;
+            // linear scale to requested audio value
+            rsamp *= vol;
+            // use 20 bits of value only, which allows ignoring the lowest 8
+            // bits during biphase conversion (after including sample shift)
+            sample = ((uint32_t)rsamp) & 0xFFFFF0;
+
+            // determine parity, simplified to one lookup via XOR
+            parity = ((sample >> 16) ^ (sample >> 8)) ^ sample;
             parity = snd_parity[parity];
 
-            /*
-             * Shift sample into the correct bit positions of the sub-frame. This
-             * would normally be << 12, but with my DACs I've had persistent issues
-             * with signal clipping when sending data in the highest bit position.
-             */
-            sample = sample << 11;
-            if (sample & 0x04000000) {
-                // handle two's complement
-                sample |= 0x08000000;
-                parity++;
-            }
+            // shift sample into the correct bit positions of the sub-frame.
+            sample = sample << 4;
         }
 
         // if needed, establish even parity with P bit
@@ -202,7 +212,7 @@ static void snd_encode(uint8_t* samples, uint16_t* wire_patterns, uint16_t len,
         if (invert) wp = ~wp;
         invert = wp & 1;
         wire_patterns[widx++] = wp;
-        // next 8 bits (only high 4 have data)
+        // next 8 bits
         wp = biphase[(uint8_t) (sample >> 8)];
         if (invert) wp = ~wp;
         invert = wp & 1;
@@ -544,4 +554,12 @@ audio_status_code audio_get_status_code(uint8_t id) {
     return tmp;
 }
 
+uint16_t audio_get_volume(uint8_t id) {
+    return volumes[id & 7];
+}
+
+void audio_set_volume(uint8_t id, uint16_t vol) {
+    volumes[id & 7] = vol;
+}
+
 #endif // ENABLE_AUDIO_OUTPUT

+ 31 - 0
src/ZuluSCSI_audio.h

@@ -24,6 +24,17 @@
 #include <stdint.h>
 #include "ImageBackingStore.h"
 
+/*
+ * Starting volume level for audio output, with 0 being muted and 255 being
+ * max volume. SCSI-2 says this should be 25% of maximum by default, MMC-1
+ * says 100%. Testing shows this tends to be obnoxious at high volumes, so
+ * go with SCSI-2.
+ *
+ * This implementation uses the high byte for output port 1 and the low byte
+ * for port 0. The two values are averaged to determine final volume level.
+ */
+#define DEFAULT_VOLUME_LEVEL 0x3F3F
+
 /*
  * Status codes for audio playback, matching the SCSI 'audio status codes'.
  *
@@ -86,3 +97,23 @@ void audio_stop(uint8_t id);
  * \return      The matching audio status code.
  */
 audio_status_code audio_get_status_code(uint8_t id);
+
+/**
+ * Gets the current volume level for a target. This is a pair of 8-bit values
+ * ranging from 0-255 that are averaged together to determine the final output
+ * level, where 0 is muted and 255 is maximum volume. The high byte corresponds
+ * to 0x0E channel 1 and the low byte to 0x0E channel 0. See the spec's mode
+ * page documentation for more details.
+ *
+ * \param id    SCSI ID to provide volume for.
+ * \return      The matching volume level.
+ */
+uint16_t audio_get_volume(uint8_t id);
+
+/**
+ * Sets the volume level for a target, as above. See 0x0E mode page for more.
+ *
+ * \param id    SCSI ID to set volume for.
+ * \param vol   The new volume level.
+ */
+void audio_set_volume(uint8_t id, uint16_t vol);

+ 159 - 0
src/ZuluSCSI_mode.cpp

@@ -0,0 +1,159 @@
+/**
+ * Copyright (C) 2013 Michael McMaster <michael@codesrc.com>
+ * Copyright (C) 2014 Doug Brown <doug@downtowndougbrown.com>
+ * Copyright (C) 2019 Landon Rodgers <g.landon.rodgers@gmail.com>
+ * ZuluSCSI™ - Copyright (c) 2023 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/>.
+**/
+
+#include <stdint.h>
+#include <string.h>
+
+#ifdef ENABLE_AUDIO_OUTPUT
+#include "ZuluSCSI_audio.h"
+#endif
+#include "ZuluSCSI_cdrom.h"
+#include "ZuluSCSI_log.h"
+
+extern "C" {
+#include "ZuluSCSI_mode.h"
+}
+
+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
+};
+
+#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
+0x01, 0xFF, // output port 0 active, max volume
+0x02, 0xFF, // output port 1 active, max volume
+0x00, 0x00, // output port 2 inactive
+0x00, 0x00 // output port 3 inactive
+};
+#endif
+
+static void pageIn(int pc, int dataIdx, const uint8_t* pageData, int pageLen)
+{
+    memcpy(&scsiDev.data[dataIdx], pageData, pageLen);
+
+    if (pc == 0x01) // Mask out (un)changable values
+    {
+        memset(&scsiDev.data[dataIdx+2], 0, pageLen - 2);
+    }
+}
+
+extern "C"
+int modeSenseCDDevicePage(int pc, int idx, int pageCode, int* pageFound)
+{
+    if ((scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
+        && (pageCode == 0x0D || pageCode == 0x3F))
+    {
+        *pageFound = 1;
+        pageIn(
+            pc,
+            idx,
+            CDROMCDParametersPage,
+            sizeof(CDROMCDParametersPage));
+        return sizeof(CDROMCDParametersPage);
+    }
+    else
+    {
+        return 0;
+    }
+}
+
+extern "C"
+int modeSenseCDAudioControlPage(int pc, int idx, int pageCode, int* pageFound)
+{
+#ifdef ENABLE_AUDIO_OUTPUT
+    if ((scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
+        && (pageCode == 0x0E || pageCode == 0x3F))
+    {
+        *pageFound = 1;
+        pageIn(
+            pc,
+            idx,
+            CDROMAudioControlParametersPage,
+            sizeof(CDROMAudioControlParametersPage));
+        if (pc == 0x00)
+        {
+            // report current volume level
+            uint16_t vol = audio_get_volume(scsiDev.target->targetId);
+            scsiDev.data[idx+9] = vol & 0xFF;
+            scsiDev.data[idx+11] = vol >> 8;
+        }
+        else if (pc == 0x01)
+        {
+            // report bits that can be set
+            scsiDev.data[idx+9] = 0xFF;
+            scsiDev.data[idx+11] = 0xFF;
+        }
+        else
+        {
+            // report defaults for 0x02
+            // also report same for 0x03, though we are actually supposed
+            // to terminate with CHECK CONDITION and SAVING PARAMETERS NOT SUPPORTED
+            scsiDev.data[idx+9] = DEFAULT_VOLUME_LEVEL & 0xFF;
+            scsiDev.data[idx+11] = DEFAULT_VOLUME_LEVEL >> 8;
+        }
+        return sizeof(CDROMAudioControlParametersPage);
+    }
+    else
+    {
+        return 0;
+    }
+#else
+    return 0;
+#endif
+}
+
+extern "C"
+int modeSelectCDAudioControlPage(int pageLen, int idx)
+{
+#ifdef ENABLE_AUDIO_OUTPUT
+    if (scsiDev.target->cfg->deviceType == S2S_CFG_OPTICAL)
+    {
+        if (pageLen != 0x0E) return 0;
+        uint16_t vol = (scsiDev.data[idx+11] << 8) + scsiDev.data[idx+9];
+        dbgmsg("------ CD audio control page volume (", vol, ")");
+        audio_set_volume(scsiDev.target->targetId, vol);
+        return 1;
+    }
+    else
+    {
+        return 0;
+    }
+#else
+    return 0;
+#endif
+}

+ 27 - 0
src/ZuluSCSI_mode.h

@@ -0,0 +1,27 @@
+/**
+ * ZuluSCSI™ - Copyright (c) 2023 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
+
+int modeSenseCDDevicePage(int pc, int idx, int pageCode, int* pageFound);
+int modeSenseCDAudioControlPage(int pc, int idx, int pageCode, int* pageFound);
+
+int modeSelectCDAudioControlPage(int pageLen, int idx);