Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CD-ROM: volume control and mode changes #228

Merged
merged 8 commits into from May 28, 2023
74 changes: 19 additions & 55 deletions lib/SCSI2SD/src/firmware/mode.c
Expand Up @@ -21,6 +21,7 @@
#include "mode.h"
#include "disk.h"
#include "inquiry.h"
#include "ZuluSCSI_mode.h"

#include <string.h>

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand All @@ -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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
50 changes: 34 additions & 16 deletions lib/ZuluSCSI_platform_RP2040/audio.cpp
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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 changes: 31 additions & 0 deletions src/ZuluSCSI_audio.h
Expand Up @@ -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'.
*
Expand Down Expand Up @@ -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);