diff --git a/lib/SCSI2SD/src/firmware/mode.c b/lib/SCSI2SD/src/firmware/mode.c index ff65862..0ef3c4b 100755 --- a/lib/SCSI2SD/src/firmware/mode.c +++ b/lib/SCSI2SD/src/firmware/mode.c @@ -21,6 +21,7 @@ #include "mode.h" #include "disk.h" #include "inquiry.h" +#include "ZuluSCSI_mode.h" #include @@ -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 diff --git a/lib/ZuluSCSI_platform_RP2040/audio.cpp b/lib/ZuluSCSI_platform_RP2040/audio.cpp index b74204a..90e7ef9 100644 --- a/lib/ZuluSCSI_platform_RP2040/audio.cpp +++ b/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 \ No newline at end of file diff --git a/src/ZuluSCSI_audio.h b/src/ZuluSCSI_audio.h index ef30eec..1b9f4b5 100644 --- a/src/ZuluSCSI_audio.h +++ b/src/ZuluSCSI_audio.h @@ -24,6 +24,17 @@ #include #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); diff --git a/src/ZuluSCSI_mode.cpp b/src/ZuluSCSI_mode.cpp new file mode 100644 index 0000000..000f2f1 --- /dev/null +++ b/src/ZuluSCSI_mode.cpp @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2013 Michael McMaster + * Copyright (C) 2014 Doug Brown + * Copyright (C) 2019 Landon Rodgers + * 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 . +**/ + +#include +#include + +#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 +} diff --git a/src/ZuluSCSI_mode.h b/src/ZuluSCSI_mode.h new file mode 100644 index 0000000..73fb8b8 --- /dev/null +++ b/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 . +**/ + +#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);