| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- /**
- * Copyright (C) 2023 saybur
- * ZuluSCSI™ - Copyright (c) 2023-2025 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/>.
- **/
- #ifdef ENABLE_AUDIO_OUTPUT_I2S
- #include "audio_i2s.h"
- #include "ZuluSCSI_platform.h"
- #include "ZuluSCSI_audio.h"
- #include "ZuluSCSI_v1_1_gpio.h"
- #include "ZuluSCSI_log.h"
- extern "C"
- {
- #include "gd32f20x_rcu.h"
- #include "gd32f20x_dma.h"
- #include "gd32f20x_misc.h"
- }
- bool g_audio_enabled = false;
- bool g_audio_stopped = true;
- // some chonky buffers to store audio samples
- static uint8_t sample_circ_buf[AUDIO_BUFFER_SIZE] __attribute__((aligned(4)));
- // tracking for the state for the circular buffer, A first half and B second half
- enum bufstate { STALE, FILLING, READY };
- static volatile bufstate sbufst_a = STALE;
- static volatile bufstate sbufst_b = STALE;
- static uint8_t sbufswap = 0;
- // tracking for audio playback
- static uint8_t audio_owner; // SCSI ID or 0xFF when idle
- static volatile bool audio_paused = false;
- static ImageBackingStore* audio_file;
- static volatile uint64_t fpos;
- static volatile uint32_t fleft;
- extern bool g_audio_stopped;
- // historical playback status information
- static audio_status_code audio_last_status[8] = {ASC_NO_STATUS, ASC_NO_STATUS, ASC_NO_STATUS, ASC_NO_STATUS,
- ASC_NO_STATUS, ASC_NO_STATUS, ASC_NO_STATUS, 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
- };
- static volatile uint16_t channels[8] = {
- AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK,
- AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK, AUDIO_CHANNEL_ENABLE_MASK
- };
- bool audio_is_active() {
- return audio_owner != 0xFF;
- }
- bool audio_is_playing(uint8_t id) {
- return audio_owner == (id & 7);
- }
- static void audio_start_dma()
- {
- dma_channel_disable(ODE_DMA, ODE_DMA_CH);
- dma_interrupt_flag_clear(ODE_DMA, ODE_DMA_CH, DMA_INT_FLAG_HTF);
- dma_interrupt_flag_clear(ODE_DMA, ODE_DMA_CH, DMA_INT_FLAG_FTF);
- dma_interrupt_enable(ODE_DMA, ODE_DMA_CH, DMA_INT_HTF | DMA_INT_FTF);
- dma_transfer_number_config(ODE_DMA, ODE_DMA_CH, AUDIO_BUFFER_SIZE / 2); // convert to 16bit transfer count
- spi_enable(ODE_I2S_SPI);
- dma_channel_enable(ODE_DMA, ODE_DMA_CH);
- }
- extern "C"
- {
- void ODE_IRQHandler()
- {
- if ( SET == dma_interrupt_flag_get(ODE_DMA, ODE_DMA_CH, DMA_INT_FLAG_HTF))
- {
- dma_interrupt_flag_clear(ODE_DMA, ODE_DMA_CH, DMA_INT_FLAG_HTF);
- sbufst_a = STALE;
- }
- if (SET == dma_interrupt_flag_get(ODE_DMA, ODE_DMA_CH, DMA_INT_FLAG_FTF))
- {
- dma_interrupt_flag_clear(ODE_DMA, ODE_DMA_CH, DMA_INT_FLAG_FTF);
- sbufst_b = STALE;
- }
- }
- }
- void audio_setup()
- {
- // Setup clocks
- rcu_periph_clock_enable(ODE_RCU_I2S_SPI);
- rcu_periph_clock_enable(ODE_RCU_DMA);
- // Install NVIC
- nvic_irq_enable(ODE_DMA_IRQn, 3, 0);
- // DMA setup
- dma_parameter_struct dma_init_struct;
- dma_struct_para_init(&dma_init_struct);
- dma_deinit(ODE_DMA, ODE_DMA_CH);
- dma_init_struct.periph_addr = (uint32_t)&SPI_DATA(ODE_I2S_SPI);
- dma_init_struct.memory_addr = (uint32_t)sample_circ_buf;
- dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL;
- dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT;
- dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT;
- dma_init_struct.priority = DMA_PRIORITY_LOW;
- dma_init_struct.number = AUDIO_BUFFER_SIZE / 2; // 8 bit to 16 bit conversion length
- dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
- dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
- dma_init(ODE_DMA, ODE_DMA_CH, &dma_init_struct);
- /* configure DMA mode */
- dma_circulation_enable(ODE_DMA, ODE_DMA_CH);
- dma_memory_to_memory_disable(ODE_DMA, ODE_DMA_CH);
- /* configure I2S1 */
- i2s_disable(ODE_I2S_SPI);
- spi_i2s_deinit(ODE_I2S_SPI);
- i2s_init(ODE_I2S_SPI, I2S_MODE_MASTERTX, I2S_STD_PHILLIPS, I2S_CKPL_LOW);
- i2s_psc_config(ODE_I2S_SPI, I2S_AUDIOSAMPLE_44K, I2S_FRAMEFORMAT_DT16B_CH16B, I2S_MCKOUT_DISABLE);
- spi_dma_enable(ODE_I2S_SPI, SPI_DMA_TRANSMIT);
- i2s_enable(ODE_I2S_SPI);
- }
- /*
- * Takes in a buffer with interleaved 16bit samples and adjusts their volume
- */
- static void audio_adjust(uint8_t owner, int16_t* buffer, size_t length)
- {
- uint8_t volume[2];
- uint16_t packed_volume = volumes[owner & 7];
- volume[0] = packed_volume >> 8;
- volume[1] = packed_volume & 0xFF;
- // enable or disable based on the channel information for both output
- // ports, where the high byte and mask control the right channel, and
- // the low control the left channel
- uint16_t chn = channels[owner & 7] & AUDIO_CHANNEL_ENABLE_MASK;
- if (!(chn >> 8))
- {
- volume[0] = 0;
- }
- if (!(chn & 0xFF))
- {
- volume[1] = 0;
- }
- for (int i = 0; i < length; i++)
- {
- // linear volume
- buffer[i] = (int16_t)(( ((int32_t)buffer[i]) * volume[i & 0x1]) >> 8);
- }
- }
- void audio_poll()
- {
- if(!g_audio_enabled)
- return;
- if ((!g_audio_stopped) && 1 == (0x1 & platform_get_buttons()))
- {
- audio_stop(audio_owner);
- }
- 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_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;
- }
- if ( STALE == sbufst_a || STALE == sbufst_b )
- {
- uint8_t* audiobuf;
- if (sbufst_a == STALE) {
- sbufst_a = FILLING;
- audiobuf = sample_circ_buf;
- }
-
- if (sbufst_b == STALE) {
- sbufst_b = FILLING;
- audiobuf = sample_circ_buf + AUDIO_BUFFER_HALF_SIZE;
- }
- platform_set_sd_callback(NULL, NULL);
-
- uint16_t toRead = AUDIO_BUFFER_HALF_SIZE;
- if (fleft < toRead) toRead = fleft;
- 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);
- }
- }
- ssize_t read_length = audio_file->read(audiobuf, AUDIO_BUFFER_HALF_SIZE);
-
- if ( read_length < AUDIO_BUFFER_HALF_SIZE )
- {
- if ( read_length < 0)
- {
- logmsg("Playback file half buffer size read error: ", read_length);
- return;
- }
- else
- {
- // pad buffer with zeros
- memset(audiobuf + read_length, 0, AUDIO_BUFFER_HALF_SIZE - read_length);
- }
- }
- audio_adjust(audio_owner, (int16_t*) audiobuf, AUDIO_BUFFER_HALF_SIZE / 2);
-
- fpos += toRead;
- fleft -= toRead;
- if (sbufst_a == FILLING) {
- sbufst_a = READY;
- } else if (sbufst_b == FILLING) {
- sbufst_b = READY;
- }
- }
- }
- bool audio_play(uint8_t owner, image_config_t* img, uint64_t start, uint64_t end, bool swap)
- {
- if (audio_is_active()) audio_stop(audio_owner);
- // 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;
- }
- audio_file = &img->file;
- if (!audio_file->isOpen()) {
- logmsg("File not open for audio playback, ", owner);
- return false;
- }
- 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 <= AUDIO_BUFFER_SIZE)
- {
- logmsg("File playback request (", start, ":", end, ") too short");
- return false;
- }
- // read in initial sample buffers
- if (!audio_file->seek(start))
- {
- logmsg("Sample file failed start seek to ", start);
- return false;
- }
- ssize_t read_length = audio_file->read(sample_circ_buf, AUDIO_BUFFER_SIZE);
- if ( read_length < AUDIO_BUFFER_SIZE)
- {
- if ( read_length < 0)
- {
- logmsg("Playback file read error: ", read_length);
- return false;
- }
- else
- {
- // pad buffer with zeros
- memset(sample_circ_buf + read_length, 0, AUDIO_BUFFER_SIZE - read_length);
- }
- }
- audio_adjust(owner, (int16_t*)sample_circ_buf, AUDIO_BUFFER_SIZE / 2);
-
- // prepare initial tracking state
- fpos = audio_file->position();
- fleft -= AUDIO_BUFFER_SIZE;
- sbufswap = swap;
- sbufst_a = READY;
- sbufst_b = READY;
- audio_owner = owner & 7;
- audio_last_status[audio_owner] = ASC_PLAYING;
- audio_paused = false;
- g_audio_stopped = false;
- audio_start_dma();
- return true;
- }
- 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;
- if (paused)
- {
- // Turn off interrupts to stop flagging new audio samples to load
- dma_interrupt_disable(ODE_DMA, ODE_DMA_CH, DMA_INT_FTF | DMA_INT_HTF);
- dma_interrupt_flag_clear(ODE_DMA, ODE_DMA_CH, DMA_INT_FLAG_HTF);
- dma_interrupt_flag_clear(ODE_DMA, ODE_DMA_CH, DMA_INT_FLAG_FTF);
- // Set status for both left and right channel to stop audio polling
- // from loading new samples
- sbufst_a = READY;
- sbufst_b = READY;
- // Let the DMA continue to run but set audio out to 0s
- memset(sample_circ_buf, 0, AUDIO_BUFFER_SIZE);
- audio_last_status[audio_owner] = ASC_PAUSED;
- }
- else
- {
- // Enable audio polling and DMA interrupts to load and play new samples
- sbufst_a = STALE;
- sbufst_b = STALE;
- dma_interrupt_flag_clear(ODE_DMA, ODE_DMA_CH, DMA_INT_FLAG_HTF);
- dma_interrupt_flag_clear(ODE_DMA, ODE_DMA_CH, DMA_INT_FLAG_FTF);
- dma_interrupt_enable(ODE_DMA, ODE_DMA_CH, DMA_INT_FTF | DMA_INT_HTF);
- audio_last_status[audio_owner] = ASC_PLAYING;
- }
- return true;
- }
- void audio_stop(uint8_t id)
- {
- if (audio_owner != (id & 7)) return;
- spi_disable(ODE_I2S_SPI);
- dma_channel_disable(ODE_DMA, ODE_DMA_CH);
- // idle the subsystem
- audio_last_status[audio_owner] = ASC_COMPLETED;
- audio_paused = false;
- g_audio_stopped = true;
- 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;
- }
- 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;
- }
- uint16_t audio_get_channel(uint8_t id) {
- return channels[id & 7];
- }
- void audio_set_channel(uint8_t id, uint16_t chn) {
- channels[id & 7] = chn;
- }
- uint64_t audio_get_file_position()
- {
- return fpos;
- }
- void audio_set_file_position(uint8_t id, uint32_t lba)
- {
- fpos = 2352 * (uint64_t)lba;
- }
- #endif // ENABLE_AUDIO_OUTPUT_I2S
|