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

Experimental audio playback support from BIN/CUE files. #207

Merged
merged 16 commits into from May 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 53 additions & 0 deletions lib/SCSI2SD/src/firmware/mode.c
Expand Up @@ -220,6 +220,33 @@ static const uint8_t ControlModePage[] =
0x00, 0x00 // AEN holdoff period.
};

#ifdef ENABLE_AUDIO_OUTPUT
PetteriAimonen marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -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))
{
Expand Down
131 changes: 89 additions & 42 deletions lib/ZuluSCSI_platform_RP2040/audio.cpp
Expand Up @@ -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.
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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() {
Expand All @@ -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?
Expand All @@ -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) {
Expand All @@ -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++) {
Expand Down Expand Up @@ -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
Expand All @@ -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
26 changes: 1 addition & 25 deletions lib/ZuluSCSI_platform_RP2040/audio.h
Expand Up @@ -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
Expand Down Expand Up @@ -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 changes: 12 additions & 0 deletions src/ImageBackingStore.cpp
Expand Up @@ -313,3 +313,15 @@ void ImageBackingStore::flush()
m_fsfile.flush();
}
}

uint64_t ImageBackingStore::position()
{
if (!m_israw && !m_isrom)
PetteriAimonen marked this conversation as resolved.
Show resolved Hide resolved
{
return m_fsfile.curPosition();
}
else
{
return 0;
}
}
4 changes: 4 additions & 0 deletions src/ImageBackingStore.h
Expand Up @@ -71,6 +71,10 @@ class ImageBackingStore
// 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;
Expand Down