From 3b920d084f37d7f098e9f4df14803712114b7903 Mon Sep 17 00:00:00 2001 From: Movie Vertigo Date: Thu, 23 Nov 2023 19:11:20 +0000 Subject: [PATCH 01/15] Allow frequency changes on audio samples --- video/audio_channel.h | 3 +-- video/enhanced_samples_generator.h | 32 ++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/video/audio_channel.h b/video/audio_channel.h index 0e6a879..5614798 100644 --- a/video/audio_channel.h +++ b/video/audio_channel.h @@ -334,9 +334,8 @@ void audio_channel::loop() { if (this->_waveformType == AUDIO_WAVE_SAMPLE) { // hack to ensure samples always start from beginning this->_waveform->setFrequency(-1); - } else { - this->_waveform->setFrequency(this->getFrequency(0)); } + this->_waveform->setFrequency(this->getFrequency(0)); this->_waveform->enable(true); // if we have an envelope then we loop, otherwise just delay for duration if (this->_volumeEnvelope || this->_frequencyEnvelope) { diff --git a/video/enhanced_samples_generator.h b/video/enhanced_samples_generator.h index a6cf2c2..2c52071 100644 --- a/video/enhanced_samples_generator.h +++ b/video/enhanced_samples_generator.h @@ -22,6 +22,13 @@ class EnhancedSamplesGenerator : public WaveformGenerator { private: std::weak_ptr _sample; + + int previousSample = 0; + int currentSample = 0; + double samplesPerGet = 1.0; + double fractionalSampleOffset = 0.0; + + const int baseFrequency = 16000; }; EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr sample) @@ -29,16 +36,22 @@ EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr {} void EnhancedSamplesGenerator::setFrequency(int value) { - // usually this will do nothing... - // but we'll hijack this method to allow us to reset the sample index + // We'll hijack this method to allow us to reset the sample index // ideally we'd override the enable method, but C++ doesn't let us do that if (value < 0) { // rewind our sample if it's still valid if (!_sample.expired()) { auto samplePtr = _sample.lock(); samplePtr->rewind(); + + fractionalSampleOffset = 0.0; + previousSample = samplePtr->getSample(); + currentSample = samplePtr->getSample(); } } + else { + samplesPerGet = (double)value / (double)baseFrequency; + } } int EnhancedSamplesGenerator::getSample() { @@ -47,7 +60,18 @@ int EnhancedSamplesGenerator::getSample() { } auto samplePtr = _sample.lock(); - int sample = samplePtr->getSample(); + + while (fractionalSampleOffset >= 1.0) + { + previousSample = currentSample; + currentSample = samplePtr->getSample(); + fractionalSampleOffset -= 1.0; + } + + // Interpolate between the samples to reduce aliasing + int sample = currentSample * fractionalSampleOffset + previousSample * (1.0-fractionalSampleOffset); + + fractionalSampleOffset += samplesPerGet; // process volume sample = sample * volume() / 127; @@ -59,7 +83,7 @@ int EnhancedSamplesGenerator::getSample() { int EnhancedSamplesGenerator::getDuration() { // NB this is hard-coded for a 16khz sample rate - return _sample.expired() ? 0 : _sample.lock()->getDuration(); + return _sample.expired() ? 0 : _sample.lock()->getDuration() / samplesPerGet; } #endif // ENHANCED_SAMPLES_GENERATOR_H From f7501baff11f7f2cd12381ef7a12ff678a227389 Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Sun, 10 Dec 2023 15:19:56 +0000 Subject: [PATCH 02/15] some tidying of audio code use an enum for AudioState use more consistent variable naming fix some code formatting --- video/agon.h | 14 +-- video/agon_audio.h | 41 +++---- video/audio_channel.h | 168 ++++++++++++++--------------- video/audio_sample.h | 16 +-- video/enhanced_samples_generator.h | 46 ++++---- video/envelopes/frequency.h | 4 +- video/envelopes/volume.h | 2 +- video/vdu.h | 2 +- video/vdu_audio.h | 20 ++-- video/video.ino | 4 +- 10 files changed, 159 insertions(+), 158 deletions(-) diff --git a/video/agon.h b/video/agon.h index b17231b..a9cbb07 100644 --- a/video/agon.h +++ b/video/agon.h @@ -118,12 +118,14 @@ #define AUDIO_STATUS_HAS_VOLUME_ENVELOPE 0x08 // Channel has a volume envelope set #define AUDIO_STATUS_HAS_FREQUENCY_ENVELOPE 0x10 // Channel has a frequency envelope set -#define AUDIO_STATE_IDLE 0 // Channel is idle/silent -#define AUDIO_STATE_PENDING 1 // Channel is pending (note will be played next loop call) -#define AUDIO_STATE_PLAYING 2 // Channel is playing a note (passive) -#define AUDIO_STATE_PLAY_LOOP 3 // Channel is in active note playing loop -#define AUDIO_STATE_RELEASE 4 // Channel is releasing a note -#define AUDIO_STATE_ABORT 5 // Channel is aborting a note +enum AudioState : uint8_t { // Audio channel state + Idle = 0, // currently idle/silent + Pending, // note will be played next loop call + Playing, // playing (passive) + PlayLoop, // active playing loop (used when an envelope is active) + Release, // in "release" phase + Abort // aborting a note +}; // Mouse commands #define MOUSE_ENABLE 0 // Enable mouse diff --git a/video/agon_audio.h b/video/agon_audio.h index 2ea9deb..b54b661 100644 --- a/video/agon_audio.h +++ b/video/agon_audio.h @@ -20,27 +20,27 @@ #include "types.h" // audio channels and their associated tasks -std::unordered_map> audio_channels; +std::unordered_map> audioChannels; std::vector> audioHandlers; -std::unordered_map> samples; // Storage for the sample data +std::unordered_map> samples; // Storage for the sample data -fabgl::SoundGenerator SoundGenerator; // The audio class +std::shared_ptr soundGenerator; // audio handling sub-system // Audio channel driver task // -void audio_driver(void * parameters) { +void audioDriver(void * parameters) { uint8_t channel = *(uint8_t *)parameters; - audio_channels[channel] = make_shared_psram(channel); + audioChannels[channel] = make_shared_psram(channel); while (true) { - audio_channels[channel]->loop(); + audioChannels[channel]->loop(); vTaskDelay(1); } } -void init_audio_channel(uint8_t channel) { - xTaskCreatePinnedToCore(audio_driver, "audio_driver", +void initAudioChannel(uint8_t channel) { + xTaskCreatePinnedToCore(audioDriver, "audioDriver", 4096, // This stack size can be checked & adjusted by reading the Stack Highwater &channel, // Parameters PLAY_SOUND_PRIORITY, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest. @@ -59,7 +59,7 @@ void audioTaskKill(uint8_t channel) { if (audioHandlers[channel]) { vTaskDelete(audioHandlers[channel]); audioHandlers[channel] = nullptr; - audio_channels.erase(channel); + audioChannels.erase(channel); debug_log("audioTaskKill: channel %d killed\n\r", channel); } else { debug_log("audioTaskKill: channel %d not found\n\r", channel); @@ -68,26 +68,27 @@ void audioTaskKill(uint8_t channel) { // Initialise the sound driver // -void init_audio() { +void initAudio() { + soundGenerator = std::make_shared(); audioHandlers.reserve(MAX_AUDIO_CHANNELS); - debug_log("init_audio: we have reserved %d channels\n\r", audioHandlers.capacity()); + debug_log("initAudio: we have reserved %d channels\n\r", audioHandlers.capacity()); for (uint8_t i = 0; i < AUDIO_CHANNELS; i++) { - init_audio_channel(i); + initAudioChannel(i); } - SoundGenerator.play(true); + soundGenerator->play(true); } // Channel enabled? // bool channelEnabled(uint8_t channel) { - return channel < MAX_AUDIO_CHANNELS && audio_channels[channel]; + return channel < MAX_AUDIO_CHANNELS && audioChannels[channel]; } // Play a note // -uint8_t play_note(uint8_t channel, uint8_t volume, uint16_t frequency, uint16_t duration) { +uint8_t playNote(uint8_t channel, uint8_t volume, uint16_t frequency, uint16_t duration) { if (channelEnabled(channel)) { - return audio_channels[channel]->play_note(volume, frequency, duration); + return audioChannels[channel]->playNote(volume, frequency, duration); } return 1; } @@ -96,7 +97,7 @@ uint8_t play_note(uint8_t channel, uint8_t volume, uint16_t frequency, uint16_t // uint8_t getChannelStatus(uint8_t channel) { if (channelEnabled(channel)) { - return audio_channels[channel]->getStatus(); + return audioChannels[channel]->getStatus(); } return -1; } @@ -105,7 +106,7 @@ uint8_t getChannelStatus(uint8_t channel) { // void setVolume(uint8_t channel, uint8_t volume) { if (channelEnabled(channel)) { - audio_channels[channel]->setVolume(volume); + audioChannels[channel]->setVolume(volume); } } @@ -113,7 +114,7 @@ void setVolume(uint8_t channel, uint8_t volume) { // void setFrequency(uint8_t channel, uint16_t frequency) { if (channelEnabled(channel)) { - audio_channels[channel]->setFrequency(frequency); + audioChannels[channel]->setFrequency(frequency); } } @@ -121,7 +122,7 @@ void setFrequency(uint8_t channel, uint16_t frequency) { // void setWaveform(uint8_t channel, int8_t waveformType, uint16_t sampleId) { if (channelEnabled(channel)) { - auto channelRef = audio_channels[channel]; + auto channelRef = audioChannels[channel]; channelRef->setWaveform(waveformType, channelRef, sampleId); } } diff --git a/video/audio_channel.h b/video/audio_channel.h index 5614798..9648261 100644 --- a/video/audio_channel.h +++ b/video/audio_channel.h @@ -10,19 +10,19 @@ #include "envelopes/volume.h" #include "envelopes/frequency.h" -extern fabgl::SoundGenerator SoundGenerator; // The audio class +extern std::shared_ptr soundGenerator; // audio handling sub-system extern void audioTaskAbortDelay(uint8_t channel); // The audio channel class // -class audio_channel { +class AudioChannel { public: - audio_channel(uint8_t channel); - ~audio_channel(); - uint8_t play_note(uint8_t volume, uint16_t frequency, int32_t duration); + AudioChannel(uint8_t channel); + ~AudioChannel(); + uint8_t playNote(uint8_t volume, uint16_t frequency, int32_t duration); uint8_t getStatus(); - std::unique_ptr getSampleWaveform(uint16_t sampleId, std::shared_ptr channelRef); - void setWaveform(int8_t waveformType, std::shared_ptr channelRef, uint16_t sampleId = 0); + std::unique_ptr getSampleWaveform(uint16_t sampleId, std::shared_ptr channelRef); + void setWaveform(int8_t waveformType, std::shared_ptr channelRef, uint16_t sampleId = 0); void setVolume(uint8_t volume); void setFrequency(uint16_t frequency); void setVolumeEnvelope(std::unique_ptr envelope); @@ -35,7 +35,7 @@ class audio_channel { uint16_t getFrequency(uint32_t elapsed); bool isReleasing(uint32_t elapsed); bool isFinished(uint32_t elapsed); - uint8_t _state; + AudioState _state; uint8_t _channel; uint8_t _volume; uint16_t _frequency; @@ -49,32 +49,32 @@ class audio_channel { #include "audio_sample.h" #include "enhanced_samples_generator.h" -extern std::unordered_map> samples; // Storage for the sample data +extern std::unordered_map> samples; // Storage for the sample data -audio_channel::audio_channel(uint8_t channel) { +AudioChannel::AudioChannel(uint8_t channel) { this->_channel = channel; - this->_state = AUDIO_STATE_IDLE; + this->_state = AudioState::Idle; this->_volume = 0; this->_frequency = 750; this->_duration = -1; setWaveform(AUDIO_WAVE_DEFAULT, nullptr); - debug_log("audio_driver: init %d\n\r", this->_channel); + debug_log("AudioChannel: init %d\n\r", this->_channel); debug_log("free mem: %d\n\r", heap_caps_get_free_size(MALLOC_CAP_8BIT)); } -audio_channel::~audio_channel() { - debug_log("audio_driver: deiniting %d\n\r", this->_channel); +AudioChannel::~AudioChannel() { + debug_log("AudioChannel: deiniting %d\n\r", this->_channel); if (this->_waveform) { this->_waveform->enable(false); - SoundGenerator.detach(this->_waveform.get()); + soundGenerator->detach(this->_waveform.get()); } - debug_log("audio_driver: deinit %d\n\r", this->_channel); + debug_log("AudioChannel: deinit %d\n\r", this->_channel); } -uint8_t audio_channel::play_note(uint8_t volume, uint16_t frequency, int32_t duration) { +uint8_t AudioChannel::playNote(uint8_t volume, uint16_t frequency, int32_t duration) { switch (this->_state) { - case AUDIO_STATE_IDLE: - case AUDIO_STATE_RELEASE: + case AudioState::Idle: + case AudioState::Release: this->_volume = volume; this->_frequency = frequency; this->_duration = duration == 65535 ? -1 : duration; @@ -89,14 +89,14 @@ uint8_t audio_channel::play_note(uint8_t volume, uint16_t frequency, int32_t dur this->_duration = 1; } } - this->_state = AUDIO_STATE_PENDING; - debug_log("audio_driver: play_note %d,%d,%d,%d\n\r", this->_channel, this->_volume, this->_frequency, this->_duration); + this->_state = AudioState::Pending; + debug_log("AudioChannel: playNote %d,%d,%d,%d\n\r", this->_channel, this->_volume, this->_frequency, this->_duration); return 1; } return 0; } -uint8_t audio_channel::getStatus() { +uint8_t AudioChannel::getStatus() { uint8_t status = 0; if (this->_waveform && this->_waveform->enabled()) { status |= AUDIO_STATUS_ACTIVE; @@ -105,9 +105,9 @@ uint8_t audio_channel::getStatus() { } } switch (this->_state) { - case AUDIO_STATE_PENDING: - case AUDIO_STATE_PLAYING: - case AUDIO_STATE_PLAY_LOOP: + case AudioState::Pending: + case AudioState::Playing: + case AudioState::PlayLoop: status |= AUDIO_STATUS_PLAYING; break; } @@ -118,11 +118,11 @@ uint8_t audio_channel::getStatus() { status |= AUDIO_STATUS_HAS_FREQUENCY_ENVELOPE; } - debug_log("audio_driver: getStatus %d\n\r", status); + debug_log("AudioChannel: getStatus %d\n\r", status); return status; } -std::unique_ptr audio_channel::getSampleWaveform(uint16_t sampleId, std::shared_ptr channelRef) { +std::unique_ptr AudioChannel::getSampleWaveform(uint16_t sampleId, std::shared_ptr channelRef) { if (samples.find(sampleId) != samples.end()) { auto sample = samples.at(sampleId); // remove this channel from other samples @@ -138,7 +138,7 @@ std::unique_ptr audio_channel::getSampleWaveform(uint1 return nullptr; } -void audio_channel::setWaveform(int8_t waveformType, std::shared_ptr channelRef, uint16_t sampleId) { +void AudioChannel::setWaveform(int8_t waveformType, std::shared_ptr channelRef, uint16_t sampleId) { std::unique_ptr newWaveform = nullptr; switch (waveformType) { @@ -162,7 +162,7 @@ void audio_channel::setWaveform(int8_t waveformType, std::shared_ptr_state != AUDIO_STATE_IDLE) { - debug_log("audio_driver: aborting current playback\n\r"); + debug_log("AudioChannel: setWaveform %d\n\r", waveformType); + if (this->_state != AudioState::Idle) { + debug_log("AudioChannel: aborting current playback\n\r"); // some kind of playback is happening, so abort any current task delay to allow playback to end - this->_state = AUDIO_STATE_ABORT; + this->_state = AudioState::Abort; audioTaskAbortDelay(this->_channel); waitForAbort(); } if (this->_waveform) { - debug_log("audio_driver: detaching old waveform\n\r"); - SoundGenerator.detach(this->_waveform.get()); + debug_log("AudioChannel: detaching old waveform\n\r"); + soundGenerator->detach(this->_waveform.get()); } this->_waveform = std::move(newWaveform); _waveformType = waveformType; - SoundGenerator.attach(this->_waveform.get()); - debug_log("audio_driver: setWaveform %d done\n\r", waveformType); + soundGenerator->attach(this->_waveform.get()); + debug_log("AudioChannel: setWaveform %d done\n\r", waveformType); } } -void audio_channel::setVolume(uint8_t volume) { - debug_log("audio_driver: setVolume %d\n\r", volume); +void AudioChannel::setVolume(uint8_t volume) { + debug_log("AudioChannel: setVolume %d\n\r", volume); if (this->_waveform) { waitForAbort(); switch (this->_state) { - case AUDIO_STATE_IDLE: + case AudioState::Idle: if (volume > 0) { // new note playback this->_volume = volume; this->_duration = -1; // indefinite duration - this->_state = AUDIO_STATE_PENDING; + this->_state = AudioState::Pending; } break; - case AUDIO_STATE_PLAY_LOOP: + case AudioState::PlayLoop: // we are looping, so an envelope may be active if (volume == 0) { // silence whilst looping always stops playback - curtail duration @@ -227,8 +227,8 @@ void audio_channel::setVolume(uint8_t volume) { this->_volume = volume; } break; - case AUDIO_STATE_PENDING: - case AUDIO_STATE_RELEASE: + case AudioState::Pending: + case AudioState::Release: // Set level so next loop will pick up the new volume this->_volume = volume; break; @@ -238,7 +238,7 @@ void audio_channel::setVolume(uint8_t volume) { this->_waveform->setVolume(volume); if (volume == 0) { // we're going silent, so abort any current playback - this->_state = AUDIO_STATE_ABORT; + this->_state = AudioState::Abort; audioTaskAbortDelay(this->_channel); } break; @@ -246,16 +246,16 @@ void audio_channel::setVolume(uint8_t volume) { } } -void audio_channel::setFrequency(uint16_t frequency) { - debug_log("audio_driver: setFrequency %d\n\r", frequency); +void AudioChannel::setFrequency(uint16_t frequency) { + debug_log("AudioChannel: setFrequency %d\n\r", frequency); this->_frequency = frequency; if (this->_waveform) { waitForAbort(); switch (this->_state) { - case AUDIO_STATE_PENDING: - case AUDIO_STATE_PLAY_LOOP: - case AUDIO_STATE_RELEASE: + case AudioState::Pending: + case AudioState::PlayLoop: + case AudioState::Release: // Do nothing as next loop will pick up the new frequency break; default: @@ -264,46 +264,46 @@ void audio_channel::setFrequency(uint16_t frequency) { } } -void audio_channel::setVolumeEnvelope(std::unique_ptr envelope) { +void AudioChannel::setVolumeEnvelope(std::unique_ptr envelope) { this->_volumeEnvelope = std::move(envelope); - if (envelope && this->_state == AUDIO_STATE_PLAYING) { + if (envelope && this->_state == AudioState::Playing) { // swap to looping - this->_state = AUDIO_STATE_PLAY_LOOP; + this->_state = AudioState::PlayLoop; audioTaskAbortDelay(this->_channel); } } -void audio_channel::setFrequencyEnvelope(std::unique_ptr envelope) { +void AudioChannel::setFrequencyEnvelope(std::unique_ptr envelope) { this->_frequencyEnvelope = std::move(envelope); - if (envelope && this->_state == AUDIO_STATE_PLAYING) { + if (envelope && this->_state == AudioState::Playing) { // swap to looping - this->_state = AUDIO_STATE_PLAY_LOOP; + this->_state = AudioState::PlayLoop; audioTaskAbortDelay(this->_channel); } } -void audio_channel::waitForAbort() { - while (this->_state == AUDIO_STATE_ABORT) { +void AudioChannel::waitForAbort() { + while (this->_state == AudioState::Abort) { // wait for abort to complete vTaskDelay(1); } } -uint8_t audio_channel::getVolume(uint32_t elapsed) { +uint8_t AudioChannel::getVolume(uint32_t elapsed) { if (this->_volumeEnvelope) { return this->_volumeEnvelope->getVolume(this->_volume, elapsed, this->_duration); } return this->_volume; } -uint16_t audio_channel::getFrequency(uint32_t elapsed) { +uint16_t AudioChannel::getFrequency(uint32_t elapsed) { if (this->_frequencyEnvelope) { return this->_frequencyEnvelope->getFrequency(this->_frequency, elapsed, this->_duration); } return this->_frequency; } -bool audio_channel::isReleasing(uint32_t elapsed) { +bool AudioChannel::isReleasing(uint32_t elapsed) { if (this->_volumeEnvelope) { return this->_volumeEnvelope->isReleasing(elapsed, this->_duration); } @@ -313,7 +313,7 @@ bool audio_channel::isReleasing(uint32_t elapsed) { return elapsed >= this->_duration; } -bool audio_channel::isFinished(uint32_t elapsed) { +bool AudioChannel::isFinished(uint32_t elapsed) { if (this->_volumeEnvelope) { return this->_volumeEnvelope->isFinished(elapsed, this->_duration); } @@ -323,10 +323,10 @@ bool audio_channel::isFinished(uint32_t elapsed) { return (elapsed >= this->_duration); } -void audio_channel::loop() { +void AudioChannel::loop() { switch (this->_state) { - case AUDIO_STATE_PENDING: - debug_log("audio_driver: play %d,%d,%d,%d\n\r", this->_channel, this->_volume, this->_frequency, this->_duration); + case AudioState::Pending: + debug_log("AudioChannel: play %d,%d,%d,%d\n\r", this->_channel, this->_volume, this->_frequency, this->_duration); // we have a new note to play this->_startTime = millis(); // set our initial volume and frequency @@ -339,38 +339,38 @@ void audio_channel::loop() { this->_waveform->enable(true); // if we have an envelope then we loop, otherwise just delay for duration if (this->_volumeEnvelope || this->_frequencyEnvelope) { - this->_state = AUDIO_STATE_PLAY_LOOP; + this->_state = AudioState::PlayLoop; } else { - this->_state = AUDIO_STATE_PLAYING; + this->_state = AudioState::Playing; // if delay value is negative then this delays for a super long time vTaskDelay(pdMS_TO_TICKS(this->_duration)); } break; - case AUDIO_STATE_PLAYING: + case AudioState::Playing: if (this->_duration >= 0) { // simple playback - delay until we have reached our duration uint32_t elapsed = millis() - this->_startTime; - debug_log("audio_driver: elapsed %d\n\r", elapsed); + debug_log("AudioChannel: elapsed %d\n\r", elapsed); if (elapsed >= this->_duration) { this->_waveform->enable(false); - debug_log("audio_driver: end\n\r"); - this->_state = AUDIO_STATE_IDLE; + debug_log("AudioChannel: end\n\r"); + this->_state = AudioState::Idle; } else { - debug_log("audio_driver: loop (%d)\n\r", this->_duration - elapsed); + debug_log("AudioChannel: loop (%d)\n\r", this->_duration - elapsed); vTaskDelay(pdMS_TO_TICKS(this->_duration - elapsed)); } } else { // our duration is indefinite, so delay for a long time - debug_log("audio_driver: loop (indefinite playback)\n\r"); + debug_log("AudioChannel: loop (indefinite playback)\n\r"); vTaskDelay(pdMS_TO_TICKS(-1)); } break; // loop and release states used for envelopes - case AUDIO_STATE_PLAY_LOOP: { + case AudioState::PlayLoop: { uint32_t elapsed = millis() - this->_startTime; if (isReleasing(elapsed)) { - debug_log("audio_driver: releasing...\n\r"); - this->_state = AUDIO_STATE_RELEASE; + debug_log("AudioChannel: releasing...\n\r"); + this->_state = AudioState::Release; } // update volume and frequency as appropriate if (this->_volumeEnvelope) @@ -379,7 +379,7 @@ void audio_channel::loop() { this->_waveform->setFrequency(this->getFrequency(elapsed)); break; } - case AUDIO_STATE_RELEASE: { + case AudioState::Release: { uint32_t elapsed = millis() - this->_startTime; // update volume and frequency as appropriate if (this->_volumeEnvelope) @@ -389,15 +389,15 @@ void audio_channel::loop() { if (isFinished(elapsed)) { this->_waveform->enable(false); - debug_log("audio_driver: end (released)\n\r"); - this->_state = AUDIO_STATE_IDLE; + debug_log("AudioChannel: end (released)\n\r"); + this->_state = AudioState::Idle; } break; } - case AUDIO_STATE_ABORT: + case AudioState::Abort: this->_waveform->enable(false); - debug_log("audio_driver: abort\n\r"); - this->_state = AUDIO_STATE_IDLE; + debug_log("AudioChannel: abort\n\r"); + this->_state = AudioState::Idle; break; } } diff --git a/video/audio_sample.h b/video/audio_sample.h index ec417d4..0fd5125 100644 --- a/video/audio_sample.h +++ b/video/audio_sample.h @@ -8,9 +8,9 @@ #include "audio_channel.h" #include "buffer_stream.h" -struct audio_sample { - audio_sample(std::vector>& streams, uint8_t format) : blocks(streams), format(format), index(0), blockIndex(0) {} - ~audio_sample(); +struct AudioSample { + AudioSample(std::vector>& streams, uint8_t format) : blocks(streams), format(format), index(0), blockIndex(0) {} + ~AudioSample(); int8_t getSample(); void rewind() { index = 0; @@ -36,24 +36,24 @@ struct audio_sample { uint8_t format; // Format of the sample data uint32_t index; // Current index inside the current sample block uint32_t blockIndex; // Current index into the sample data blocks - std::unordered_map> channels; // Channels playing this sample + std::unordered_map> channels; // Channels playing this sample }; -audio_sample::~audio_sample() { +AudioSample::~AudioSample() { // iterate over channels for (auto channelPair : this->channels) { auto channelRef = channelPair.second; if (!channelRef.expired()) { auto channel = channelRef.lock(); - debug_log("audio_sample: removing sample from channel %d\n\r", channel->channel()); + debug_log("AudioSample: removing sample from channel %d\n\r", channel->channel()); // Remove sample from channel channel->setWaveform(AUDIO_WAVE_DEFAULT, nullptr); } } - debug_log("audio_sample cleared\n\r"); + debug_log("AudioSample cleared\n\r"); } -int8_t audio_sample::getSample() { +int8_t AudioSample::getSample() { // our blocks might have changed, so we need to check if we're still in range checkIndexes(); diff --git a/video/enhanced_samples_generator.h b/video/enhanced_samples_generator.h index 2c52071..2a1c43d 100644 --- a/video/enhanced_samples_generator.h +++ b/video/enhanced_samples_generator.h @@ -13,25 +13,25 @@ // class EnhancedSamplesGenerator : public WaveformGenerator { public: - EnhancedSamplesGenerator(std::shared_ptr sample); + EnhancedSamplesGenerator(std::shared_ptr sample); void setFrequency(int value); int getSample(); int getDuration(); - + private: - std::weak_ptr _sample; + std::weak_ptr _sample; - int previousSample = 0; - int currentSample = 0; - double samplesPerGet = 1.0; - double fractionalSampleOffset = 0.0; + int previousSample = 0; + int currentSample = 0; + double samplesPerGet = 1.0; + double fractionalSampleOffset = 0.0; - const int baseFrequency = 16000; + const int baseFrequency = 16000; }; -EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr sample) +EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr sample) : _sample(sample) {} @@ -44,14 +44,13 @@ void EnhancedSamplesGenerator::setFrequency(int value) { auto samplePtr = _sample.lock(); samplePtr->rewind(); - fractionalSampleOffset = 0.0; - previousSample = samplePtr->getSample(); - currentSample = samplePtr->getSample(); + fractionalSampleOffset = 0.0; + previousSample = samplePtr->getSample(); + currentSample = samplePtr->getSample(); } + } else { + samplesPerGet = (double)value / (double)baseFrequency; } - else { - samplesPerGet = (double)value / (double)baseFrequency; - } } int EnhancedSamplesGenerator::getSample() { @@ -61,17 +60,16 @@ int EnhancedSamplesGenerator::getSample() { auto samplePtr = _sample.lock(); - while (fractionalSampleOffset >= 1.0) - { - previousSample = currentSample; - currentSample = samplePtr->getSample(); - fractionalSampleOffset -= 1.0; - } + while (fractionalSampleOffset >= 1.0) { + previousSample = currentSample; + currentSample = samplePtr->getSample(); + fractionalSampleOffset -= 1.0; + } - // Interpolate between the samples to reduce aliasing - int sample = currentSample * fractionalSampleOffset + previousSample * (1.0-fractionalSampleOffset); + // Interpolate between the samples to reduce aliasing + int sample = currentSample * fractionalSampleOffset + previousSample * (1.0-fractionalSampleOffset); - fractionalSampleOffset += samplesPerGet; + fractionalSampleOffset += samplesPerGet; // process volume sample = sample * volume() / 127; diff --git a/video/envelopes/frequency.h b/video/envelopes/frequency.h index 0183eef..92e396b 100644 --- a/video/envelopes/frequency.h +++ b/video/envelopes/frequency.h @@ -50,8 +50,8 @@ SteppedFrequencyEnvelope::SteppedFrequencyEnvelope(std::shared_ptr_totalSteps, this->_totalAdjustment); - debug_log("audio_driver: SteppedFrequencyEnvelope: stepLength=%d, repeats=%d, restricts=%d, totalLength=%d\n\r", this->_stepLength, this->_repeats, this->_restrict, _totalLength); + debug_log("audioDriver: SteppedFrequencyEnvelope: totalSteps=%d, totalAdjustment=%d\n\r", this->_totalSteps, this->_totalAdjustment); + debug_log("audioDriver: SteppedFrequencyEnvelope: stepLength=%d, repeats=%d, restricts=%d, totalLength=%d\n\r", this->_stepLength, this->_repeats, this->_restrict, _totalLength); } uint16_t SteppedFrequencyEnvelope::getFrequency(uint16_t baseFrequency, uint32_t elapsed, int32_t duration) { diff --git a/video/envelopes/volume.h b/video/envelopes/volume.h index b386c59..da99dae 100644 --- a/video/envelopes/volume.h +++ b/video/envelopes/volume.h @@ -36,7 +36,7 @@ ADSRVolumeEnvelope::ADSRVolumeEnvelope(uint16_t attack, uint16_t decay, uint8_t { // attack, decay, release are time values in milliseconds // sustain is 0-255, centered on 127, and is the relative sustain level - debug_log("audio_driver: ADSRVolumeEnvelope: attack=%d, decay=%d, sustain=%d, release=%d\n\r", this->_attack, this->_decay, this->_sustain, this->_release); + debug_log("audioDriver: ADSRVolumeEnvelope: attack=%d, decay=%d, sustain=%d, release=%d\n\r", this->_attack, this->_decay, this->_sustain, this->_release); } uint8_t ADSRVolumeEnvelope::getVolume(uint8_t baseVolume, uint32_t elapsed, int32_t duration) { diff --git a/video/vdu.h b/video/vdu.h index c12f940..b95c566 100644 --- a/video/vdu.h +++ b/video/vdu.h @@ -33,7 +33,7 @@ void VDUStreamProcessor::vdu(uint8_t c) { setActiveViewport(VIEWPORT_GRAPHICS); break; case 0x07: // Bell - play_note(0, 100, 750, 125); + playNote(0, 100, 750, 125); break; case 0x08: // Cursor Left cursorLeft(); diff --git a/video/vdu_audio.h b/video/vdu_audio.h index 1ca35c1..497439b 100644 --- a/video/vdu_audio.h +++ b/video/vdu_audio.h @@ -14,6 +14,7 @@ #include "agon.h" #include "agon_audio.h" +#include "audio_sample.h" #include "buffers.h" #include "types.h" @@ -29,7 +30,7 @@ void VDUStreamProcessor::vdu_sys_audio() { auto frequency = readWord_t(); if (frequency == -1) return; auto duration = readWord_t(); if (duration == -1) return; - sendAudioStatus(channel, play_note(channel, volume, frequency, duration)); + sendAudioStatus(channel, playNote(channel, volume, frequency, duration)); } break; case AUDIO_CMD_STATUS: { @@ -124,8 +125,8 @@ void VDUStreamProcessor::vdu_sys_audio() { } break; case AUDIO_CMD_ENABLE: { - if (channel >= 0 && channel < MAX_AUDIO_CHANNELS && audio_channels[channel] == nullptr) { - init_audio_channel(channel); + if (channel >= 0 && channel < MAX_AUDIO_CHANNELS && audioChannels[channel] == nullptr) { + initAudioChannel(channel); } } break; @@ -138,7 +139,7 @@ void VDUStreamProcessor::vdu_sys_audio() { case AUDIO_CMD_RESET: { if (channelEnabled(channel)) { audioTaskKill(channel); - init_audio_channel(channel); + initAudioChannel(channel); } } break; } @@ -176,8 +177,7 @@ uint8_t VDUStreamProcessor::createSampleFromBuffer(uint16_t bufferId, uint8_t fo return 0; } clearSample(bufferId); - auto sample = make_shared_psram(buffers[bufferId], format); - // auto sample = make_shared_psram(bufferId, format); + auto sample = make_shared_psram(buffers[bufferId], format); if (sample) { samples[bufferId] = sample; return 1; @@ -191,7 +191,7 @@ void VDUStreamProcessor::setVolumeEnvelope(uint8_t channel, uint8_t type) { if (channelEnabled(channel)) { switch (type) { case AUDIO_ENVELOPE_NONE: - audio_channels[channel]->setVolumeEnvelope(nullptr); + audioChannels[channel]->setVolumeEnvelope(nullptr); debug_log("vdu_sys_audio: channel %d - volume envelope disabled\n\r", channel); break; case AUDIO_ENVELOPE_ADSR: @@ -200,7 +200,7 @@ void VDUStreamProcessor::setVolumeEnvelope(uint8_t channel, uint8_t type) { auto sustain = readByte_t(); if (sustain == -1) return; auto release = readWord_t(); if (release == -1) return; std::unique_ptr envelope = make_unique_psram(attack, decay, sustain, release); - audio_channels[channel]->setVolumeEnvelope(std::move(envelope)); + audioChannels[channel]->setVolumeEnvelope(std::move(envelope)); break; } } @@ -212,7 +212,7 @@ void VDUStreamProcessor::setFrequencyEnvelope(uint8_t channel, uint8_t type) { if (channelEnabled(channel)) { switch (type) { case AUDIO_ENVELOPE_NONE: - audio_channels[channel]->setFrequencyEnvelope(nullptr); + audioChannels[channel]->setFrequencyEnvelope(nullptr); debug_log("vdu_sys_audio: channel %d - frequency envelope disabled\n\r", channel); break; case AUDIO_FREQUENCY_ENVELOPE_STEPPED: @@ -229,7 +229,7 @@ void VDUStreamProcessor::setFrequencyEnvelope(uint8_t channel, uint8_t type) { bool cumulative = control & AUDIO_FREQUENCY_CUMULATIVE; bool restrict = control & AUDIO_FREQUENCY_RESTRICT; std::unique_ptr envelope = make_unique_psram(phases, stepLength, repeats, cumulative, restrict); - audio_channels[channel]->setFrequencyEnvelope(std::move(envelope)); + audioChannels[channel]->setFrequencyEnvelope(std::move(envelope)); break; } } diff --git a/video/video.ino b/video/video.ino index af0b500..4c4b082 100644 --- a/video/video.ino +++ b/video/video.ino @@ -30,7 +30,7 @@ // 29/03/2023: + Typo in boot screen fixed // 01/04/2023: + Added resetPalette to MODE, timeouts to VDU commands // 08/04/2023: RC4 + Removed delay in readbyte_t, fixed VDP_SCRCHAR, VDP_SCRPIXEL -// 12/04/2023: + Fixed bug in play_note +// 12/04/2023: + Fixed bug in playNote // 13/04/2023: + Fixed bootup fail with no keyboard // 17/04/2023: RC5 + Moved wait_completion in vdu so that it only executes after graphical operations // 18/04/2023: + Minor tweaks to wait completion logic @@ -84,7 +84,7 @@ void setup() { processor = new VDUStreamProcessor(&VDPSerial); processor->wait_eZ80(); setupKeyboardAndMouse(); - init_audio(); + initAudio(); copy_font(); set_mode(1); processor->sendModeInformation(); From e8436efe8c9e25dec2ccc97a9e531d9604912fe6 Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Sun, 10 Dec 2023 16:17:34 +0000 Subject: [PATCH 03/15] allow samples to have variable sample rates --- video/agon.h | 3 +++ video/agon_audio.h | 2 +- video/audio_sample.h | 11 +++++++---- video/enhanced_samples_generator.h | 14 ++++++-------- video/vdu_audio.h | 14 ++++++++++---- video/vdu_stream_processor.h | 2 +- 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/video/agon.h b/video/agon.h index a9cbb07..3625101 100644 --- a/video/agon.h +++ b/video/agon.h @@ -68,6 +68,7 @@ #define PACKET_MOUSE 0x09 // Mouse data #define AUDIO_CHANNELS 3 // Default number of audio channels +#define AUDIO_DEFAULT_SAMPLE_RATE FABGL_SOUNDGEN_DEFAULT_SAMPLE_RATE // Default sample rate #define MAX_AUDIO_CHANNELS 32 // Maximum number of audio channels #define PLAY_SOUND_PRIORITY 3 // Sound driver task priority with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest @@ -102,6 +103,8 @@ #define AUDIO_FORMAT_8BIT_SIGNED 0 // 8-bit signed sample #define AUDIO_FORMAT_8BIT_UNSIGNED 1 // 8-bit unsigned sample +#define AUDIO_FORMAT_DATA_MASK 7 // data bit mask for format +#define AUDIO_FORMAT_WITH_RATE 8 // OR this with the format to indicate a sample rate follows #define AUDIO_ENVELOPE_NONE 0 // No envelope #define AUDIO_ENVELOPE_ADSR 1 // Simple ADSR volume envelope diff --git a/video/agon_audio.h b/video/agon_audio.h index b54b661..fc1f84f 100644 --- a/video/agon_audio.h +++ b/video/agon_audio.h @@ -69,7 +69,7 @@ void audioTaskKill(uint8_t channel) { // Initialise the sound driver // void initAudio() { - soundGenerator = std::make_shared(); + soundGenerator = std::make_shared(AUDIO_DEFAULT_SAMPLE_RATE); audioHandlers.reserve(MAX_AUDIO_CHANNELS); debug_log("initAudio: we have reserved %d channels\n\r", audioHandlers.capacity()); for (uint8_t i = 0; i < AUDIO_CHANNELS; i++) { diff --git a/video/audio_sample.h b/video/audio_sample.h index 0fd5125..83ac83b 100644 --- a/video/audio_sample.h +++ b/video/audio_sample.h @@ -9,7 +9,8 @@ #include "buffer_stream.h" struct AudioSample { - AudioSample(std::vector>& streams, uint8_t format) : blocks(streams), format(format), index(0), blockIndex(0) {} + AudioSample(std::vector>& streams, uint8_t format, uint32_t sampleRate = AUDIO_DEFAULT_SAMPLE_RATE, uint16_t frequency = AUDIO_DEFAULT_FREQUENCY) : + blocks(streams), format(format), sampleRate(sampleRate), baseFrequency(frequency), index(0), blockIndex(0) {} ~AudioSample(); int8_t getSample(); void rewind() { @@ -26,16 +27,18 @@ struct AudioSample { } } uint32_t getDuration() { - uint32_t duration = 0; + uint32_t samples = 0; for (auto block : blocks) { - duration += block->size(); + samples += block->size(); } - return duration / 16; + return (samples * 1000) / sampleRate; } std::vector>& blocks; uint8_t format; // Format of the sample data uint32_t index; // Current index inside the current sample block uint32_t blockIndex; // Current index into the sample data blocks + uint32_t sampleRate; // Sample rate of the sample + uint16_t baseFrequency; // Base frequency of the sample std::unordered_map> channels; // Channels playing this sample }; diff --git a/video/enhanced_samples_generator.h b/video/enhanced_samples_generator.h index 2a1c43d..3194273 100644 --- a/video/enhanced_samples_generator.h +++ b/video/enhanced_samples_generator.h @@ -27,8 +27,6 @@ class EnhancedSamplesGenerator : public WaveformGenerator { int currentSample = 0; double samplesPerGet = 1.0; double fractionalSampleOffset = 0.0; - - const int baseFrequency = 16000; }; EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr sample) @@ -38,18 +36,18 @@ EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr void EnhancedSamplesGenerator::setFrequency(int value) { // We'll hijack this method to allow us to reset the sample index // ideally we'd override the enable method, but C++ doesn't let us do that - if (value < 0) { - // rewind our sample if it's still valid - if (!_sample.expired()) { - auto samplePtr = _sample.lock(); + if (!_sample.expired()) { + auto samplePtr = _sample.lock(); + if (value < 0) { + // rewind our sample if it's still valid samplePtr->rewind(); fractionalSampleOffset = 0.0; previousSample = samplePtr->getSample(); currentSample = samplePtr->getSample(); + } else { + samplesPerGet = (double)value / (double)(samplePtr->sampleRate); } - } else { - samplesPerGet = (double)value / (double)baseFrequency; } } diff --git a/video/vdu_audio.h b/video/vdu_audio.h index 497439b..bc0a77c 100644 --- a/video/vdu_audio.h +++ b/video/vdu_audio.h @@ -85,8 +85,12 @@ void VDUStreamProcessor::vdu_sys_audio() { case AUDIO_SAMPLE_FROM_BUFFER: { auto bufferId = readWord_t(); if (bufferId == -1) return; auto format = readByte_t(); if (format == -1) return; + uint32_t sampleRate = AUDIO_DEFAULT_SAMPLE_RATE; + if (format & AUDIO_FORMAT_WITH_RATE) { + sampleRate = readWord_t(); if (sampleRate == -1) return; + } - sendAudioStatus(channel, createSampleFromBuffer(bufferId, format)); + sendAudioStatus(channel, createSampleFromBuffer(bufferId, format, sampleRate)); } break; case AUDIO_SAMPLE_DEBUG_INFO: { @@ -166,18 +170,20 @@ uint8_t VDUStreamProcessor::loadSample(uint16_t bufferId, uint32_t length) { // timed out, or couldn't allocate buffer - so abort return 0; } - return createSampleFromBuffer(bufferId, 0); + return createSampleFromBuffer(bufferId, 0, AUDIO_DEFAULT_SAMPLE_RATE); } // Create a sample from a buffer // -uint8_t VDUStreamProcessor::createSampleFromBuffer(uint16_t bufferId, uint8_t format) { +uint8_t VDUStreamProcessor::createSampleFromBuffer(uint16_t bufferId, uint8_t format, uint16_t sampleRate) { if (buffers.find(bufferId) == buffers.end()) { debug_log("vdu_sys_audio: buffer %d not found\n\r", bufferId); return 0; } clearSample(bufferId); - auto sample = make_shared_psram(buffers[bufferId], format); + auto sample = (format & AUDIO_FORMAT_WITH_RATE) ? + make_shared_psram(buffers[bufferId], format & AUDIO_FORMAT_DATA_MASK, sampleRate) + : make_shared_psram(buffers[bufferId], format); if (sample) { samples[bufferId] = sample; return 1; diff --git a/video/vdu_stream_processor.h b/video/vdu_stream_processor.h index 6c29656..bcde103 100644 --- a/video/vdu_stream_processor.h +++ b/video/vdu_stream_processor.h @@ -53,7 +53,7 @@ class VDUStreamProcessor { void vdu_sys_audio(); void sendAudioStatus(uint8_t channel, uint8_t status); uint8_t loadSample(uint16_t bufferId, uint32_t length); - uint8_t createSampleFromBuffer(uint16_t bufferId, uint8_t format); + uint8_t createSampleFromBuffer(uint16_t bufferId, uint8_t format, uint16_t sampleRate); void setVolumeEnvelope(uint8_t channel, uint8_t type); void setFrequencyEnvelope(uint8_t channel, uint8_t type); From 2d2a7ae850265a6466abaaa1f1fcf2a14af4f68e Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Sun, 10 Dec 2023 16:50:38 +0000 Subject: [PATCH 04/15] normalise sample frequency calculate sample playback rate based on a base frequency value for the sample. this defaults to 532hz, or C5 (the C above middle-C) --- video/agon.h | 2 ++ video/enhanced_samples_generator.h | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/video/agon.h b/video/agon.h index 3625101..5cd2792 100644 --- a/video/agon.h +++ b/video/agon.h @@ -101,6 +101,8 @@ #define AUDIO_SAMPLE_FROM_BUFFER 2 // Load a sample from a buffer #define AUDIO_SAMPLE_DEBUG_INFO 0x10 // Get debug info about a sample +#define AUDIO_DEFAULT_FREQUENCY 523 // Default sample frequency (C5, or C above middle C) + #define AUDIO_FORMAT_8BIT_SIGNED 0 // 8-bit signed sample #define AUDIO_FORMAT_8BIT_UNSIGNED 1 // 8-bit unsigned sample #define AUDIO_FORMAT_DATA_MASK 7 // data bit mask for format diff --git a/video/enhanced_samples_generator.h b/video/enhanced_samples_generator.h index 3194273..4fafc3b 100644 --- a/video/enhanced_samples_generator.h +++ b/video/enhanced_samples_generator.h @@ -33,12 +33,12 @@ EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr : _sample(sample) {} -void EnhancedSamplesGenerator::setFrequency(int value) { +void EnhancedSamplesGenerator::setFrequency(int frequency) { // We'll hijack this method to allow us to reset the sample index // ideally we'd override the enable method, but C++ doesn't let us do that if (!_sample.expired()) { auto samplePtr = _sample.lock(); - if (value < 0) { + if (frequency < 0) { // rewind our sample if it's still valid samplePtr->rewind(); @@ -46,7 +46,7 @@ void EnhancedSamplesGenerator::setFrequency(int value) { previousSample = samplePtr->getSample(); currentSample = samplePtr->getSample(); } else { - samplesPerGet = (double)value / (double)(samplePtr->sampleRate); + samplesPerGet = ((double)frequency / (double)samplePtr->baseFrequency) * ((double)samplePtr->sampleRate / (double)(AUDIO_DEFAULT_SAMPLE_RATE)); } } } From 97ca809b405193e981db87d4b5f25a76344f0a51 Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Sun, 10 Dec 2023 18:04:53 +0000 Subject: [PATCH 05/15] explicitly flag tuneable samples this allows frequency adjustment of sample playback to be compatible with existing code that could be providing any value for sample frequency to `playNote` --- video/agon.h | 1 + video/audio_channel.h | 1 + video/audio_sample.h | 3 ++- video/enhanced_samples_generator.h | 5 ++++- video/vdu_audio.h | 3 +++ 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/video/agon.h b/video/agon.h index 5cd2792..4e84c54 100644 --- a/video/agon.h +++ b/video/agon.h @@ -107,6 +107,7 @@ #define AUDIO_FORMAT_8BIT_UNSIGNED 1 // 8-bit unsigned sample #define AUDIO_FORMAT_DATA_MASK 7 // data bit mask for format #define AUDIO_FORMAT_WITH_RATE 8 // OR this with the format to indicate a sample rate follows +#define AUDIO_FORMAT_TUNEABLE 16 // OR this with the format to indicate sample can be tuned (frequency adjustable) #define AUDIO_ENVELOPE_NONE 0 // No envelope #define AUDIO_ENVELOPE_ADSR 1 // Simple ADSR volume envelope diff --git a/video/audio_channel.h b/video/audio_channel.h index 9648261..7098a49 100644 --- a/video/audio_channel.h +++ b/video/audio_channel.h @@ -80,6 +80,7 @@ uint8_t AudioChannel::playNote(uint8_t volume, uint16_t frequency, int32_t durat this->_duration = duration == 65535 ? -1 : duration; if (this->_duration == 0 && this->_waveformType == AUDIO_WAVE_SAMPLE) { // zero duration means play whole sample + // TODO duration needs to be flexible for streamed samples where total buffer size is unknown this->_duration = ((EnhancedSamplesGenerator *)this->_waveform.get())->getDuration(); if (this->_volumeEnvelope) { // subtract the "release" time from the duration diff --git a/video/audio_sample.h b/video/audio_sample.h index 83ac83b..cd138d8 100644 --- a/video/audio_sample.h +++ b/video/audio_sample.h @@ -9,7 +9,7 @@ #include "buffer_stream.h" struct AudioSample { - AudioSample(std::vector>& streams, uint8_t format, uint32_t sampleRate = AUDIO_DEFAULT_SAMPLE_RATE, uint16_t frequency = AUDIO_DEFAULT_FREQUENCY) : + AudioSample(std::vector>& streams, uint8_t format, uint32_t sampleRate = AUDIO_DEFAULT_SAMPLE_RATE, uint16_t frequency = 0) : blocks(streams), format(format), sampleRate(sampleRate), baseFrequency(frequency), index(0), blockIndex(0) {} ~AudioSample(); int8_t getSample(); @@ -27,6 +27,7 @@ struct AudioSample { } } uint32_t getDuration() { + // TODO this needs to change to calculate duration with frequency borne in mind uint32_t samples = 0; for (auto block : blocks) { samples += block->size(); diff --git a/video/enhanced_samples_generator.h b/video/enhanced_samples_generator.h index 4fafc3b..7006f8a 100644 --- a/video/enhanced_samples_generator.h +++ b/video/enhanced_samples_generator.h @@ -46,7 +46,9 @@ void EnhancedSamplesGenerator::setFrequency(int frequency) { previousSample = samplePtr->getSample(); currentSample = samplePtr->getSample(); } else { - samplesPerGet = ((double)frequency / (double)samplePtr->baseFrequency) * ((double)samplePtr->sampleRate / (double)(AUDIO_DEFAULT_SAMPLE_RATE)); + auto baseFrequency = samplePtr->baseFrequency; + auto frequencyAdjust = baseFrequency > 0 ? (double)frequency / (double)baseFrequency : 1.0; + samplesPerGet = frequencyAdjust * ((double)samplePtr->sampleRate / (double)(AUDIO_DEFAULT_SAMPLE_RATE)); } } } @@ -58,6 +60,7 @@ int EnhancedSamplesGenerator::getSample() { auto samplePtr = _sample.lock(); + // if we've moved far enough along, read the next sample while (fractionalSampleOffset >= 1.0) { previousSample = currentSample; currentSample = samplePtr->getSample(); diff --git a/video/vdu_audio.h b/video/vdu_audio.h index bc0a77c..45f615b 100644 --- a/video/vdu_audio.h +++ b/video/vdu_audio.h @@ -185,6 +185,9 @@ uint8_t VDUStreamProcessor::createSampleFromBuffer(uint16_t bufferId, uint8_t fo make_shared_psram(buffers[bufferId], format & AUDIO_FORMAT_DATA_MASK, sampleRate) : make_shared_psram(buffers[bufferId], format); if (sample) { + if (format & AUDIO_FORMAT_TUNEABLE) { + sample->baseFrequency = AUDIO_DEFAULT_FREQUENCY; + } samples[bufferId] = sample; return 1; } From 63267e4f462714848cf695beba4c3fa353b75365 Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Sun, 10 Dec 2023 18:27:38 +0000 Subject: [PATCH 06/15] add commands to set sample base frequency includes variants both for negative sample numbers and bufferId-based numbering --- video/agon.h | 4 +++- video/vdu_audio.h | 24 ++++++++++++++++++++++++ video/vdu_stream_processor.h | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/video/agon.h b/video/agon.h index 4e84c54..dea1ab3 100644 --- a/video/agon.h +++ b/video/agon.h @@ -99,9 +99,11 @@ #define AUDIO_SAMPLE_LOAD 0 // Send a sample to the VDP #define AUDIO_SAMPLE_CLEAR 1 // Clear/delete a sample #define AUDIO_SAMPLE_FROM_BUFFER 2 // Load a sample from a buffer +#define AUDIO_SAMPLE_SET_FREQUENCY 3 // Set the base frequency of a sample +#define AUDIO_SAMPLE_BUFFER_SET_FREQUENCY 4 // Set the base frequency of a sample (using buffer ID) #define AUDIO_SAMPLE_DEBUG_INFO 0x10 // Get debug info about a sample -#define AUDIO_DEFAULT_FREQUENCY 523 // Default sample frequency (C5, or C above middle C) +#define AUDIO_DEFAULT_FREQUENCY 523 // Default sample frequency (C5, or C above middle C) #define AUDIO_FORMAT_8BIT_SIGNED 0 // 8-bit signed sample #define AUDIO_FORMAT_8BIT_UNSIGNED 1 // 8-bit unsigned sample diff --git a/video/vdu_audio.h b/video/vdu_audio.h index 45f615b..1659e12 100644 --- a/video/vdu_audio.h +++ b/video/vdu_audio.h @@ -93,6 +93,19 @@ void VDUStreamProcessor::vdu_sys_audio() { sendAudioStatus(channel, createSampleFromBuffer(bufferId, format, sampleRate)); } break; + case AUDIO_SAMPLE_SET_FREQUENCY: { + auto frequency = readWord_t(); if (frequency == -1) return; + + sendAudioStatus(channel, setSampleFrequency(sampleNum, frequency)); + } break; + + case AUDIO_SAMPLE_BUFFER_SET_FREQUENCY: { + auto bufferId = readWord_t(); if (bufferId == -1) return; + auto frequency = readWord_t(); if (frequency == -1) return; + + sendAudioStatus(channel, setSampleFrequency(bufferId, frequency)); + } break; + case AUDIO_SAMPLE_DEBUG_INFO: { debug_log("Sample info: %d (%d)\n\r", (int8_t)channel, sampleNum); debug_log(" samples count: %d\n\r", samples.size()); @@ -244,4 +257,15 @@ void VDUStreamProcessor::setFrequencyEnvelope(uint8_t channel, uint8_t type) { } } +// Set sample frequency +// +uint8_t VDUStreamProcessor::setSampleFrequency(uint16_t sampleId, uint16_t frequency) { + if (samples.find(sampleId) == samples.end()) { + debug_log("vdu_sys_audio: sample %d not found\n\r", sampleId); + return 0; + } + samples[sampleId]->baseFrequency = frequency; + return 1; +} + #endif // VDU_AUDIO_H diff --git a/video/vdu_stream_processor.h b/video/vdu_stream_processor.h index bcde103..a2fdd2f 100644 --- a/video/vdu_stream_processor.h +++ b/video/vdu_stream_processor.h @@ -56,6 +56,7 @@ class VDUStreamProcessor { uint8_t createSampleFromBuffer(uint16_t bufferId, uint8_t format, uint16_t sampleRate); void setVolumeEnvelope(uint8_t channel, uint8_t type); void setFrequencyEnvelope(uint8_t channel, uint8_t type); + uint8_t setSampleFrequency(uint16_t bufferId, uint16_t frequency); void vdu_sys_sprites(void); void receiveBitmap(uint8_t cmd, uint16_t width, uint16_t height); From 0bab5b36ef3711f6bbf54e2f41ff7635d575aae7 Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Mon, 11 Dec 2023 18:23:45 +0000 Subject: [PATCH 07/15] preliminary looping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit support added for defining looping on a sample sample playback however needs reworking. the current system has the AudioSample structure keeping track of where playback is inside a sample, however that wouldn’t allow the same sample to be played simultaneously on more than one channel control of sample playback instead needs to move into the EnhancedSamplesGenerator. much of what’s in this commit needs to be moved --- video/agon.h | 11 +++- video/audio_channel.h | 5 +- video/audio_sample.h | 89 ++++++++++++++++++++---------- video/enhanced_samples_generator.h | 27 ++++++--- video/vdu_audio.h | 71 +++++++++++++++++++++++- video/vdu_stream_processor.h | 2 + 6 files changed, 160 insertions(+), 45 deletions(-) diff --git a/video/agon.h b/video/agon.h index dea1ab3..6606c35 100644 --- a/video/agon.h +++ b/video/agon.h @@ -98,9 +98,14 @@ #define AUDIO_SAMPLE_LOAD 0 // Send a sample to the VDP #define AUDIO_SAMPLE_CLEAR 1 // Clear/delete a sample -#define AUDIO_SAMPLE_FROM_BUFFER 2 // Load a sample from a buffer -#define AUDIO_SAMPLE_SET_FREQUENCY 3 // Set the base frequency of a sample -#define AUDIO_SAMPLE_BUFFER_SET_FREQUENCY 4 // Set the base frequency of a sample (using buffer ID) +#define AUDIO_SAMPLE_FROM_BUFFER 2 // Load a sample from a buffer +#define AUDIO_SAMPLE_SET_FREQUENCY 3 // Set the base frequency of a sample +#define AUDIO_SAMPLE_BUFFER_SET_FREQUENCY 4 // Set the base frequency of a sample (using buffer ID) +#define AUDIO_SAMPLE_SET_REPEAT_START 5 // Set the repeat start point of a sample +#define AUDIO_SAMPLE_BUFFER_SET_REPEAT_START 6 // Set the repeat start point of a sample (using buffer ID) +#define AUDIO_SAMPLE_SET_REPEAT_LENGTH 7 // Set the repeat length of a sample +#define AUDIO_SAMPLE_BUFFER_SET_REPEAT_LENGTH 8 // Set the repeat length of a sample (using buffer ID) +#define AUDIO_SAMPLE_SEEK 9 // Seek to a position in a sample #define AUDIO_SAMPLE_DEBUG_INFO 0x10 // Get debug info about a sample #define AUDIO_DEFAULT_FREQUENCY 523 // Default sample frequency (C5, or C above middle C) diff --git a/video/audio_channel.h b/video/audio_channel.h index 7098a49..32b35c3 100644 --- a/video/audio_channel.h +++ b/video/audio_channel.h @@ -80,8 +80,9 @@ uint8_t AudioChannel::playNote(uint8_t volume, uint16_t frequency, int32_t durat this->_duration = duration == 65535 ? -1 : duration; if (this->_duration == 0 && this->_waveformType == AUDIO_WAVE_SAMPLE) { // zero duration means play whole sample - // TODO duration needs to be flexible for streamed samples where total buffer size is unknown - this->_duration = ((EnhancedSamplesGenerator *)this->_waveform.get())->getDuration(); + // NB this can only work out sample duration based on sample provided + // so if sample data is streaming in an explicit length should be used instead + this->_duration = ((EnhancedSamplesGenerator *)this->_waveform.get())->getDuration(frequency); if (this->_volumeEnvelope) { // subtract the "release" time from the duration this->_duration -= this->_volumeEnvelope->getRelease(); diff --git a/video/audio_sample.h b/video/audio_sample.h index cd138d8..61d6123 100644 --- a/video/audio_sample.h +++ b/video/audio_sample.h @@ -13,33 +13,18 @@ struct AudioSample { blocks(streams), format(format), sampleRate(sampleRate), baseFrequency(frequency), index(0), blockIndex(0) {} ~AudioSample(); int8_t getSample(); - void rewind() { - index = 0; - blockIndex = 0; - } - void checkIndexes() { - if (blockIndex >= blocks.size()) { - blockIndex = 0; - index = 0; - } - if (index >= blocks[blockIndex]->size()) { - index = 0; - } - } - uint32_t getDuration() { - // TODO this needs to change to calculate duration with frequency borne in mind - uint32_t samples = 0; - for (auto block : blocks) { - samples += block->size(); - } - return (samples * 1000) / sampleRate; - } + void seekTo(uint32_t position); + uint32_t getSize(); + uint32_t getDuration(); std::vector>& blocks; - uint8_t format; // Format of the sample data - uint32_t index; // Current index inside the current sample block - uint32_t blockIndex; // Current index into the sample data blocks - uint32_t sampleRate; // Sample rate of the sample - uint16_t baseFrequency; // Base frequency of the sample + uint8_t format; // Format of the sample data + uint32_t index; // Current index inside the current sample block + uint32_t blockIndex; // Current index into the sample data blocks + uint32_t sampleRate; // Sample rate of the sample + uint16_t baseFrequency = 0; // Base frequency of the sample + int32_t repeatStart = 0; // Start offset for repeat, in samples + int32_t repeatLength = -1; // Length of the repeat section in samples, -1 means to end of sample + int32_t repeatCount = 0; // Sample count when repeating std::unordered_map> channels; // Channels playing this sample }; @@ -58,15 +43,24 @@ AudioSample::~AudioSample() { } int8_t AudioSample::getSample() { - // our blocks might have changed, so we need to check if we're still in range - checkIndexes(); + // get the next sample + if (blockIndex >= blocks.size()) { + // we've reached the end of the sample, and haven't looped, so return 0 (silence) + return 0; + } auto block = blocks[blockIndex]; int8_t sample = block->getBuffer()[index++]; - // Insert looping magic here + // looping magic + repeatCount--; + if (repeatCount == 0) { + // we've reached the end of the repeat section, so loop back + seekTo(repeatStart); + } + if (index >= block->size()) { - // block reached end, move to next, or loop + // block reached end, move to next block index = 0; blockIndex++; } @@ -78,4 +72,39 @@ int8_t AudioSample::getSample() { return sample; } +void AudioSample::seekTo(uint32_t position) { + // NB repeatCount calculation here can result in zero, or a negative number, + // or a number that's beyond the end of the sample, which is fine + // it just means that the sample will never loop + if (repeatLength < 0) { + // repeat to end of sample + repeatCount = getSize() - position; + } else if (repeatLength > 0) { + auto repeatEnd = repeatStart + repeatLength; + repeatCount = repeatEnd - position; + } else { + repeatCount = 0; + } + + blockIndex = 0; + index = position; + while (blockIndex < blocks.size() && index >= blocks[blockIndex]->size()) { + index -= blocks[blockIndex]->size(); + blockIndex++; + } +} + +uint32_t AudioSample::getSize() { + uint32_t samples = 0; + for (auto block : blocks) { + samples += block->size(); + } + return samples; +} + +uint32_t AudioSample::getDuration() { + // returns duration of sample in ms when played back without tuning + return (getSize() * 1000) / sampleRate; +} + #endif // AUDIO_SAMPLE_H diff --git a/video/enhanced_samples_generator.h b/video/enhanced_samples_generator.h index 7006f8a..2c08dbc 100644 --- a/video/enhanced_samples_generator.h +++ b/video/enhanced_samples_generator.h @@ -18,7 +18,7 @@ class EnhancedSamplesGenerator : public WaveformGenerator { void setFrequency(int value); int getSample(); - int getDuration(); + int getDuration(uint16_t frequency); private: std::weak_ptr _sample; @@ -27,6 +27,8 @@ class EnhancedSamplesGenerator : public WaveformGenerator { int currentSample = 0; double samplesPerGet = 1.0; double fractionalSampleOffset = 0.0; + + double calculateSamplerate(uint16_t frequency); }; EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr sample) @@ -40,15 +42,13 @@ void EnhancedSamplesGenerator::setFrequency(int frequency) { auto samplePtr = _sample.lock(); if (frequency < 0) { // rewind our sample if it's still valid - samplePtr->rewind(); - + samplePtr->seekTo(0); + // prepare our fractional sample data for playback fractionalSampleOffset = 0.0; previousSample = samplePtr->getSample(); currentSample = samplePtr->getSample(); } else { - auto baseFrequency = samplePtr->baseFrequency; - auto frequencyAdjust = baseFrequency > 0 ? (double)frequency / (double)baseFrequency : 1.0; - samplesPerGet = frequencyAdjust * ((double)samplePtr->sampleRate / (double)(AUDIO_DEFAULT_SAMPLE_RATE)); + samplesPerGet = calculateSamplerate(frequency); } } } @@ -80,9 +80,18 @@ int EnhancedSamplesGenerator::getSample() { return sample; } -int EnhancedSamplesGenerator::getDuration() { - // NB this is hard-coded for a 16khz sample rate - return _sample.expired() ? 0 : _sample.lock()->getDuration() / samplesPerGet; +int EnhancedSamplesGenerator::getDuration(uint16_t frequency) { + return _sample.expired() ? 0 : _sample.lock()->getDuration() / calculateSamplerate(frequency); +} + +double EnhancedSamplesGenerator::calculateSamplerate(uint16_t frequency) { + if (!_sample.expired()) { + auto samplePtr = _sample.lock(); + auto baseFrequency = samplePtr->baseFrequency; + auto frequencyAdjust = baseFrequency > 0 ? (double)frequency / (double)baseFrequency : 1.0; + return frequencyAdjust * ((double)samplePtr->sampleRate / (double)(AUDIO_DEFAULT_SAMPLE_RATE)); + } + return 1.0; } #endif // ENHANCED_SAMPLES_GENERATOR_H diff --git a/video/vdu_audio.h b/video/vdu_audio.h index 1659e12..43f1c17 100644 --- a/video/vdu_audio.h +++ b/video/vdu_audio.h @@ -106,6 +106,44 @@ void VDUStreamProcessor::vdu_sys_audio() { sendAudioStatus(channel, setSampleFrequency(bufferId, frequency)); } break; + case AUDIO_SAMPLE_SET_REPEAT_START: { + auto repeatStart = read24_t(); if (repeatStart == -1) return; + + sendAudioStatus(channel, setSampleRepeatStart(sampleNum, repeatStart)); + } break; + + case AUDIO_SAMPLE_BUFFER_SET_REPEAT_START: { + auto bufferId = readWord_t(); if (bufferId == -1) return; + auto repeatStart = read24_t(); if (repeatStart == -1) return; + + sendAudioStatus(channel, setSampleRepeatStart(bufferId, repeatStart)); + } break; + + case AUDIO_SAMPLE_SET_REPEAT_LENGTH: { + auto repeatLength = read24_t(); if (repeatLength == -1) return; + + sendAudioStatus(channel, setSampleRepeatLength(sampleNum, repeatLength)); + } break; + + case AUDIO_SAMPLE_BUFFER_SET_REPEAT_LENGTH: { + auto bufferId = readWord_t(); if (bufferId == -1) return; + auto repeatLength = read24_t(); if (repeatLength == -1) return; + + sendAudioStatus(channel, setSampleRepeatLength(bufferId, repeatLength)); + } break; + + case AUDIO_SAMPLE_SEEK: { + auto position = read24_t(); if (position == -1) return; + + if (samples.find(sampleNum) == samples.end()) { + debug_log("vdu_sys_audio: sample %d not found\n\r", sampleNum); + sendAudioStatus(channel, 0); + break; + } + samples[sampleNum]->seekTo(position); + sendAudioStatus(channel, 1); + } break; + case AUDIO_SAMPLE_DEBUG_INFO: { debug_log("Sample info: %d (%d)\n\r", (int8_t)channel, sampleNum); debug_log(" samples count: %d\n\r", samples.size()); @@ -116,7 +154,16 @@ void VDUStreamProcessor::vdu_sys_audio() { break; } auto buffer = sample->blocks; - debug_log(" length: %d\n\r", buffer.size()); + debug_log(" length: %d blocks\n\r", buffer.size()); + debug_log(" size: %d\n\r", sample->getSize()); + debug_log(" format: %d\n\r", sample->format); + debug_log(" sample rate: %d\n\r", sample->sampleRate); + debug_log(" base frequency: %d\n\r", sample->baseFrequency); + debug_log(" repeat start: %d\n\r", sample->repeatStart); + debug_log(" repeat length: %d\n\r", sample->repeatLength); + debug_log(" repeat count: %d\n\r", sample->repeatCount); + debug_log(" index: %d\n\r", sample->index); + debug_log(" block index: %d\n\r", sample->blockIndex); if (buffer.size() > 0) { debug_log(" data first byte: %d\n\r", buffer[0]->getBuffer()[0]); } @@ -268,4 +315,26 @@ uint8_t VDUStreamProcessor::setSampleFrequency(uint16_t sampleId, uint16_t frequ return 1; } +// Set sample repeatStart +// +uint8_t VDUStreamProcessor::setSampleRepeatStart(uint16_t sampleId, uint32_t repeatStart) { + if (samples.find(sampleId) == samples.end()) { + debug_log("vdu_sys_audio: sample %d not found\n\r", sampleId); + return 0; + } + samples[sampleId]->repeatStart = repeatStart; + return 1; +} + +// Set sample repeatLength +// +uint8_t VDUStreamProcessor::setSampleRepeatLength(uint16_t sampleId, uint32_t repeatLength) { + if (samples.find(sampleId) == samples.end()) { + debug_log("vdu_sys_audio: sample %d not found\n\r", sampleId); + return 0; + } + samples[sampleId]->repeatLength = repeatLength; + return 1; +} + #endif // VDU_AUDIO_H diff --git a/video/vdu_stream_processor.h b/video/vdu_stream_processor.h index a2fdd2f..790cde7 100644 --- a/video/vdu_stream_processor.h +++ b/video/vdu_stream_processor.h @@ -57,6 +57,8 @@ class VDUStreamProcessor { void setVolumeEnvelope(uint8_t channel, uint8_t type); void setFrequencyEnvelope(uint8_t channel, uint8_t type); uint8_t setSampleFrequency(uint16_t bufferId, uint16_t frequency); + uint8_t setSampleRepeatStart(uint16_t bufferId, uint32_t offset); + uint8_t setSampleRepeatLength(uint16_t bufferId, uint32_t length); void vdu_sys_sprites(void); void receiveBitmap(uint8_t cmd, uint16_t width, uint16_t height); From 53a13160109ff1fdad7a7df34b70fee444694db5 Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Mon, 11 Dec 2023 21:13:45 +0000 Subject: [PATCH 08/15] sample playback now controlled by sample generator logic moved out of AudioSample for keeping track of position within sample this will allow the same sample to be played back on multiple channels simultaneously hack of using frequency -1 also no longer needed, as channel and sample generator now support `seekTo` --- video/agon.h | 2 +- video/audio_channel.h | 12 ++++-- video/audio_sample.h | 22 ++++------ video/enhanced_samples_generator.h | 64 ++++++++++++++++++++---------- video/vdu_audio.h | 25 +++++------- 5 files changed, 69 insertions(+), 56 deletions(-) diff --git a/video/agon.h b/video/agon.h index 6606c35..d894cfe 100644 --- a/video/agon.h +++ b/video/agon.h @@ -85,6 +85,7 @@ #define AUDIO_CMD_ENABLE 8 // Enables a channel #define AUDIO_CMD_DISABLE 9 // Disables (destroys) a channel #define AUDIO_CMD_RESET 10 // Reset audio channel +#define AUDIO_CMD_SEEK 11 // Seek to a position in a sample #define AUDIO_WAVE_DEFAULT 0 // Default waveform (Square wave) #define AUDIO_WAVE_SQUARE 0 // Square wave @@ -105,7 +106,6 @@ #define AUDIO_SAMPLE_BUFFER_SET_REPEAT_START 6 // Set the repeat start point of a sample (using buffer ID) #define AUDIO_SAMPLE_SET_REPEAT_LENGTH 7 // Set the repeat length of a sample #define AUDIO_SAMPLE_BUFFER_SET_REPEAT_LENGTH 8 // Set the repeat length of a sample (using buffer ID) -#define AUDIO_SAMPLE_SEEK 9 // Seek to a position in a sample #define AUDIO_SAMPLE_DEBUG_INFO 0x10 // Get debug info about a sample #define AUDIO_DEFAULT_FREQUENCY 523 // Default sample frequency (C5, or C above middle C) diff --git a/video/audio_channel.h b/video/audio_channel.h index 32b35c3..18fda11 100644 --- a/video/audio_channel.h +++ b/video/audio_channel.h @@ -27,6 +27,7 @@ class AudioChannel { void setFrequency(uint16_t frequency); void setVolumeEnvelope(std::unique_ptr envelope); void setFrequencyEnvelope(std::unique_ptr envelope); + void seekTo(uint32_t position); void loop(); uint8_t channel() { return _channel; } private: @@ -284,6 +285,12 @@ void AudioChannel::setFrequencyEnvelope(std::unique_ptr envel } } +void AudioChannel::seekTo(uint32_t position) { + if (this->_waveformType == AUDIO_WAVE_SAMPLE) { + ((EnhancedSamplesGenerator *)this->_waveform.get())->seekTo(position); + } +} + void AudioChannel::waitForAbort() { while (this->_state == AudioState::Abort) { // wait for abort to complete @@ -333,10 +340,7 @@ void AudioChannel::loop() { this->_startTime = millis(); // set our initial volume and frequency this->_waveform->setVolume(this->getVolume(0)); - if (this->_waveformType == AUDIO_WAVE_SAMPLE) { - // hack to ensure samples always start from beginning - this->_waveform->setFrequency(-1); - } + this->seekTo(0); this->_waveform->setFrequency(this->getFrequency(0)); this->_waveform->enable(true); // if we have an envelope then we loop, otherwise just delay for duration diff --git a/video/audio_sample.h b/video/audio_sample.h index 61d6123..f2641d2 100644 --- a/video/audio_sample.h +++ b/video/audio_sample.h @@ -10,21 +10,20 @@ struct AudioSample { AudioSample(std::vector>& streams, uint8_t format, uint32_t sampleRate = AUDIO_DEFAULT_SAMPLE_RATE, uint16_t frequency = 0) : - blocks(streams), format(format), sampleRate(sampleRate), baseFrequency(frequency), index(0), blockIndex(0) {} + blocks(streams), format(format), sampleRate(sampleRate), baseFrequency(frequency) {} ~AudioSample(); - int8_t getSample(); - void seekTo(uint32_t position); + + int8_t getSample(uint32_t & index, uint32_t & blockIndex); + void seekTo(uint32_t position, uint32_t & index, uint32_t & blockIndex, int32_t & repeatCount); uint32_t getSize(); uint32_t getDuration(); + std::vector>& blocks; uint8_t format; // Format of the sample data - uint32_t index; // Current index inside the current sample block - uint32_t blockIndex; // Current index into the sample data blocks uint32_t sampleRate; // Sample rate of the sample uint16_t baseFrequency = 0; // Base frequency of the sample int32_t repeatStart = 0; // Start offset for repeat, in samples int32_t repeatLength = -1; // Length of the repeat section in samples, -1 means to end of sample - int32_t repeatCount = 0; // Sample count when repeating std::unordered_map> channels; // Channels playing this sample }; @@ -42,7 +41,7 @@ AudioSample::~AudioSample() { debug_log("AudioSample cleared\n\r"); } -int8_t AudioSample::getSample() { +int8_t AudioSample::getSample(uint32_t & index, uint32_t & blockIndex) { // get the next sample if (blockIndex >= blocks.size()) { // we've reached the end of the sample, and haven't looped, so return 0 (silence) @@ -51,13 +50,6 @@ int8_t AudioSample::getSample() { auto block = blocks[blockIndex]; int8_t sample = block->getBuffer()[index++]; - - // looping magic - repeatCount--; - if (repeatCount == 0) { - // we've reached the end of the repeat section, so loop back - seekTo(repeatStart); - } if (index >= block->size()) { // block reached end, move to next block @@ -72,7 +64,7 @@ int8_t AudioSample::getSample() { return sample; } -void AudioSample::seekTo(uint32_t position) { +void AudioSample::seekTo(uint32_t position, uint32_t & index, uint32_t & blockIndex, int32_t & repeatCount) { // NB repeatCount calculation here can result in zero, or a negative number, // or a number that's beyond the end of the sample, which is fine // it just means that the sample will never loop diff --git a/video/enhanced_samples_generator.h b/video/enhanced_samples_generator.h index 2c08dbc..210f08c 100644 --- a/video/enhanced_samples_generator.h +++ b/video/enhanced_samples_generator.h @@ -20,15 +20,24 @@ class EnhancedSamplesGenerator : public WaveformGenerator { int getDuration(uint16_t frequency); + void seekTo(uint32_t position); + private: std::weak_ptr _sample; - int previousSample = 0; - int currentSample = 0; - double samplesPerGet = 1.0; - double fractionalSampleOffset = 0.0; + uint32_t index; // Current index inside the current sample block + uint32_t blockIndex; // Current index into the sample data blocks + int32_t repeatCount = 0; // Sample count when repeating + // TODO consider whether repeatStart and repeatLength may need to be here + // which would allow for per-channel repeat settings + + int previousSample = 0; + int currentSample = 0; + double samplesPerGet = 1.0; + double fractionalSampleOffset = 0.0; double calculateSamplerate(uint16_t frequency); + int8_t getNextSample(); }; EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr sample) @@ -36,21 +45,7 @@ EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr {} void EnhancedSamplesGenerator::setFrequency(int frequency) { - // We'll hijack this method to allow us to reset the sample index - // ideally we'd override the enable method, but C++ doesn't let us do that - if (!_sample.expired()) { - auto samplePtr = _sample.lock(); - if (frequency < 0) { - // rewind our sample if it's still valid - samplePtr->seekTo(0); - // prepare our fractional sample data for playback - fractionalSampleOffset = 0.0; - previousSample = samplePtr->getSample(); - currentSample = samplePtr->getSample(); - } else { - samplesPerGet = calculateSamplerate(frequency); - } - } + samplesPerGet = calculateSamplerate(frequency); } int EnhancedSamplesGenerator::getSample() { @@ -63,7 +58,7 @@ int EnhancedSamplesGenerator::getSample() { // if we've moved far enough along, read the next sample while (fractionalSampleOffset >= 1.0) { previousSample = currentSample; - currentSample = samplePtr->getSample(); + currentSample = getNextSample(); fractionalSampleOffset -= 1.0; } @@ -84,6 +79,18 @@ int EnhancedSamplesGenerator::getDuration(uint16_t frequency) { return _sample.expired() ? 0 : _sample.lock()->getDuration() / calculateSamplerate(frequency); } +void EnhancedSamplesGenerator::seekTo(uint32_t position) { + if (!_sample.expired()) { + auto samplePtr = _sample.lock(); + samplePtr->seekTo(position, index, blockIndex, repeatCount); + + // prepare our fractional sample data for playback + fractionalSampleOffset = 0.0; + previousSample = samplePtr->getSample(index, blockIndex); + currentSample = samplePtr->getSample(index, blockIndex); + } +} + double EnhancedSamplesGenerator::calculateSamplerate(uint16_t frequency) { if (!_sample.expired()) { auto samplePtr = _sample.lock(); @@ -94,4 +101,21 @@ double EnhancedSamplesGenerator::calculateSamplerate(uint16_t frequency) { return 1.0; } +int8_t EnhancedSamplesGenerator::getNextSample() { + if (!_sample.expired()) { + auto samplePtr = _sample.lock(); + auto sample = samplePtr->getSample(index, blockIndex); + + // looping magic + repeatCount--; + if (repeatCount == 0) { + // we've reached the end of the repeat section, so loop back + seekTo(samplePtr->repeatStart); + } + + return sample; + } + return 0; +} + #endif // ENHANCED_SAMPLES_GENERATOR_H diff --git a/video/vdu_audio.h b/video/vdu_audio.h index 43f1c17..0329a5c 100644 --- a/video/vdu_audio.h +++ b/video/vdu_audio.h @@ -132,18 +132,6 @@ void VDUStreamProcessor::vdu_sys_audio() { sendAudioStatus(channel, setSampleRepeatLength(bufferId, repeatLength)); } break; - case AUDIO_SAMPLE_SEEK: { - auto position = read24_t(); if (position == -1) return; - - if (samples.find(sampleNum) == samples.end()) { - debug_log("vdu_sys_audio: sample %d not found\n\r", sampleNum); - sendAudioStatus(channel, 0); - break; - } - samples[sampleNum]->seekTo(position); - sendAudioStatus(channel, 1); - } break; - case AUDIO_SAMPLE_DEBUG_INFO: { debug_log("Sample info: %d (%d)\n\r", (int8_t)channel, sampleNum); debug_log(" samples count: %d\n\r", samples.size()); @@ -161,9 +149,6 @@ void VDUStreamProcessor::vdu_sys_audio() { debug_log(" base frequency: %d\n\r", sample->baseFrequency); debug_log(" repeat start: %d\n\r", sample->repeatStart); debug_log(" repeat length: %d\n\r", sample->repeatLength); - debug_log(" repeat count: %d\n\r", sample->repeatCount); - debug_log(" index: %d\n\r", sample->index); - debug_log(" block index: %d\n\r", sample->blockIndex); if (buffer.size() > 0) { debug_log(" data first byte: %d\n\r", buffer[0]->getBuffer()[0]); } @@ -206,6 +191,14 @@ void VDUStreamProcessor::vdu_sys_audio() { initAudioChannel(channel); } } break; + + case AUDIO_CMD_SEEK: { + auto position = read24_t(); if (position == -1) return; + + if (channelEnabled(channel)) { + audioChannels[channel]->seekTo(position); + } + } break; } } @@ -243,7 +236,7 @@ uint8_t VDUStreamProcessor::createSampleFromBuffer(uint16_t bufferId, uint8_t fo clearSample(bufferId); auto sample = (format & AUDIO_FORMAT_WITH_RATE) ? make_shared_psram(buffers[bufferId], format & AUDIO_FORMAT_DATA_MASK, sampleRate) - : make_shared_psram(buffers[bufferId], format); + : make_shared_psram(buffers[bufferId], format & AUDIO_FORMAT_DATA_MASK); if (sample) { if (format & AUDIO_FORMAT_TUNEABLE) { sample->baseFrequency = AUDIO_DEFAULT_FREQUENCY; From c955dbdd6d9066ccd4c3d8c1ba3c30e09e184c23 Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Mon, 11 Dec 2023 21:54:44 +0000 Subject: [PATCH 09/15] add set duration command allows for note duration to be set to a 24-bit value --- video/agon.h | 1 + video/audio_channel.h | 27 ++++++++++++++++++++++++++- video/vdu_audio.h | 12 ++++++++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/video/agon.h b/video/agon.h index d894cfe..f28b462 100644 --- a/video/agon.h +++ b/video/agon.h @@ -86,6 +86,7 @@ #define AUDIO_CMD_DISABLE 9 // Disables (destroys) a channel #define AUDIO_CMD_RESET 10 // Reset audio channel #define AUDIO_CMD_SEEK 11 // Seek to a position in a sample +#define AUDIO_CMD_DURATION 12 // Set the duration of a channel #define AUDIO_WAVE_DEFAULT 0 // Default waveform (Square wave) #define AUDIO_WAVE_SQUARE 0 // Square wave diff --git a/video/audio_channel.h b/video/audio_channel.h index 18fda11..8d6caab 100644 --- a/video/audio_channel.h +++ b/video/audio_channel.h @@ -25,6 +25,7 @@ class AudioChannel { void setWaveform(int8_t waveformType, std::shared_ptr channelRef, uint16_t sampleId = 0); void setVolume(uint8_t volume); void setFrequency(uint16_t frequency); + void setDuration(int32_t duration); void setVolumeEnvelope(std::unique_ptr envelope); void setFrequencyEnvelope(std::unique_ptr envelope); void seekTo(uint32_t position); @@ -55,7 +56,7 @@ extern std::unordered_map> samples; // St AudioChannel::AudioChannel(uint8_t channel) { this->_channel = channel; this->_state = AudioState::Idle; - this->_volume = 0; + this->_volume = 64; this->_frequency = 750; this->_duration = -1; setWaveform(AUDIO_WAVE_DEFAULT, nullptr); @@ -267,6 +268,30 @@ void AudioChannel::setFrequency(uint16_t frequency) { } } +void AudioChannel::setDuration(int32_t duration) { + debug_log("AudioChannel: setDuration %d\n\r", duration); + if (duration == 0xFFFFFF) { + duration = -1; + } + this->_duration = duration; + + if (this->_waveform) { + waitForAbort(); + switch (this->_state) { + case AudioState::Idle: + // kick off a new note playback + this->_state = AudioState::Pending; + break; + case AudioState::Playing: + audioTaskAbortDelay(this->_channel); + break; + default: + // any other state we should be looping so it will just get picked up + break; + } + } +} + void AudioChannel::setVolumeEnvelope(std::unique_ptr envelope) { this->_volumeEnvelope = std::move(envelope); if (envelope && this->_state == AudioState::Playing) { diff --git a/video/vdu_audio.h b/video/vdu_audio.h index 0329a5c..f9ccf87 100644 --- a/video/vdu_audio.h +++ b/video/vdu_audio.h @@ -199,6 +199,14 @@ void VDUStreamProcessor::vdu_sys_audio() { audioChannels[channel]->seekTo(position); } } break; + + case AUDIO_CMD_DURATION: { + auto duration = read24_t(); if (duration == -1) return; + + if (channelEnabled(channel)) { + audioChannels[channel]->setDuration(duration); + } + } } } @@ -278,9 +286,9 @@ void VDUStreamProcessor::setFrequencyEnvelope(uint8_t channel, uint8_t type) { debug_log("vdu_sys_audio: channel %d - frequency envelope disabled\n\r", channel); break; case AUDIO_FREQUENCY_ENVELOPE_STEPPED: - auto phaseCount = readByte_t(); if (phaseCount == -1) return; + auto phaseCount = readByte_t(); if (phaseCount == -1) return; auto control = readByte_t(); if (control == -1) return; - auto stepLength = readWord_t(); if (stepLength == -1) return; + auto stepLength = readWord_t(); if (stepLength == -1) return; auto phases = make_shared_psram>(); for (auto n = 0; n < phaseCount; n++) { auto adjustment = readWord_t(); if (adjustment == -1) return; From b3e064cde3ee1ed99101f8eb17b412b2dd417ff3 Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Tue, 12 Dec 2023 13:51:28 +0000 Subject: [PATCH 10/15] adjustable sample rates allows for the the sample rate for the underlying system, or for a waveform on an individual channel, to be adjusted --- video/agon.h | 1 + video/agon_audio.h | 31 +++++++++++++++++++++++++++--- video/audio_channel.h | 19 ++++++++++++++++-- video/enhanced_samples_generator.h | 13 ++++++++++--- video/vdu_audio.h | 13 ++++++++++++- 5 files changed, 68 insertions(+), 9 deletions(-) diff --git a/video/agon.h b/video/agon.h index f28b462..3df2fd0 100644 --- a/video/agon.h +++ b/video/agon.h @@ -87,6 +87,7 @@ #define AUDIO_CMD_RESET 10 // Reset audio channel #define AUDIO_CMD_SEEK 11 // Seek to a position in a sample #define AUDIO_CMD_DURATION 12 // Set the duration of a channel +#define AUDIO_CMD_SAMPLERATE 13 // Set the samplerate for channel or underlying audio system #define AUDIO_WAVE_DEFAULT 0 // Default waveform (Square wave) #define AUDIO_WAVE_SQUARE 0 // Square wave diff --git a/video/agon_audio.h b/video/agon_audio.h index fc1f84f..e5557fb 100644 --- a/video/agon_audio.h +++ b/video/agon_audio.h @@ -25,7 +25,7 @@ std::vector> audioHandlers; std::unordered_map> samples; // Storage for the sample data -std::shared_ptr soundGenerator; // audio handling sub-system +std::unique_ptr soundGenerator; // audio handling sub-system // Audio channel driver task // @@ -66,16 +66,41 @@ void audioTaskKill(uint8_t channel) { } } +// Change the sample rate +// +void setSampleRate(uint16_t sampleRate) { + // make a new sound generator and re-attach all our active channels + if (sampleRate == 65535) { + sampleRate = AUDIO_DEFAULT_SAMPLE_RATE; + } + // detatch the old sound generator + if (soundGenerator) { + soundGenerator->play(false); + for (auto channelPair : audioChannels) { + auto channel = channelPair.second; + soundGenerator->detach(channel->getWaveform()); + } + } + // delete the old sound generator + soundGenerator = nullptr; + soundGenerator = std::unique_ptr(new fabgl::SoundGenerator(sampleRate)); + for (auto channelPair : audioChannels) { + auto channel = channelPair.second; + channel->attachSoundGenerator(); + } + soundGenerator->play(true); +} + // Initialise the sound driver // void initAudio() { - soundGenerator = std::make_shared(AUDIO_DEFAULT_SAMPLE_RATE); + // make new sound generator + setSampleRate(AUDIO_DEFAULT_SAMPLE_RATE); audioHandlers.reserve(MAX_AUDIO_CHANNELS); debug_log("initAudio: we have reserved %d channels\n\r", audioHandlers.capacity()); for (uint8_t i = 0; i < AUDIO_CHANNELS; i++) { initAudioChannel(i); } - soundGenerator->play(true); } // Channel enabled? diff --git a/video/audio_channel.h b/video/audio_channel.h index 8d6caab..8e4c65f 100644 --- a/video/audio_channel.h +++ b/video/audio_channel.h @@ -10,7 +10,7 @@ #include "envelopes/volume.h" #include "envelopes/frequency.h" -extern std::shared_ptr soundGenerator; // audio handling sub-system +extern std::unique_ptr soundGenerator; // audio handling sub-system extern void audioTaskAbortDelay(uint8_t channel); // The audio channel class @@ -21,17 +21,20 @@ class AudioChannel { ~AudioChannel(); uint8_t playNote(uint8_t volume, uint16_t frequency, int32_t duration); uint8_t getStatus(); - std::unique_ptr getSampleWaveform(uint16_t sampleId, std::shared_ptr channelRef); void setWaveform(int8_t waveformType, std::shared_ptr channelRef, uint16_t sampleId = 0); void setVolume(uint8_t volume); void setFrequency(uint16_t frequency); void setDuration(int32_t duration); void setVolumeEnvelope(std::unique_ptr envelope); void setFrequencyEnvelope(std::unique_ptr envelope); + void setSampleRate(uint16_t sampleRate); + WaveformGenerator * getWaveform() { return this->_waveform.get(); } + void attachSoundGenerator(); void seekTo(uint32_t position); void loop(); uint8_t channel() { return _channel; } private: + std::unique_ptr getSampleWaveform(uint16_t sampleId, std::shared_ptr channelRef); void waitForAbort(); uint8_t getVolume(uint32_t elapsed); uint16_t getFrequency(uint32_t elapsed); @@ -310,6 +313,18 @@ void AudioChannel::setFrequencyEnvelope(std::unique_ptr envel } } +void AudioChannel::setSampleRate(uint16_t sampleRate) { + if (this->_waveform) { + this->_waveform->setSampleRate(sampleRate); + } +} + +void AudioChannel::attachSoundGenerator() { + if (this->_waveform) { + soundGenerator->attach(this->_waveform.get()); + } +} + void AudioChannel::seekTo(uint32_t position) { if (this->_waveformType == AUDIO_WAVE_SAMPLE) { ((EnhancedSamplesGenerator *)this->_waveform.get())->seekTo(position); diff --git a/video/enhanced_samples_generator.h b/video/enhanced_samples_generator.h index 210f08c..cbe4d26 100644 --- a/video/enhanced_samples_generator.h +++ b/video/enhanced_samples_generator.h @@ -16,12 +16,12 @@ class EnhancedSamplesGenerator : public WaveformGenerator { EnhancedSamplesGenerator(std::shared_ptr sample); void setFrequency(int value); + void setSampleRate(int value); int getSample(); int getDuration(uint16_t frequency); void seekTo(uint32_t position); - private: std::weak_ptr _sample; @@ -31,6 +31,7 @@ class EnhancedSamplesGenerator : public WaveformGenerator { // TODO consider whether repeatStart and repeatLength may need to be here // which would allow for per-channel repeat settings + int frequency = 0; int previousSample = 0; int currentSample = 0; double samplesPerGet = 1.0; @@ -44,7 +45,13 @@ EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr : _sample(sample) {} -void EnhancedSamplesGenerator::setFrequency(int frequency) { +void EnhancedSamplesGenerator::setFrequency(int value) { + frequency = value; + samplesPerGet = calculateSamplerate(value); +} + +void EnhancedSamplesGenerator::setSampleRate(int value) { + WaveformGenerator::setSampleRate(value); samplesPerGet = calculateSamplerate(frequency); } @@ -96,7 +103,7 @@ double EnhancedSamplesGenerator::calculateSamplerate(uint16_t frequency) { auto samplePtr = _sample.lock(); auto baseFrequency = samplePtr->baseFrequency; auto frequencyAdjust = baseFrequency > 0 ? (double)frequency / (double)baseFrequency : 1.0; - return frequencyAdjust * ((double)samplePtr->sampleRate / (double)(AUDIO_DEFAULT_SAMPLE_RATE)); + return frequencyAdjust * ((double)samplePtr->sampleRate / (double)(sampleRate())); } return 1.0; } diff --git a/video/vdu_audio.h b/video/vdu_audio.h index f9ccf87..f90d98a 100644 --- a/video/vdu_audio.h +++ b/video/vdu_audio.h @@ -206,7 +206,18 @@ void VDUStreamProcessor::vdu_sys_audio() { if (channelEnabled(channel)) { audioChannels[channel]->setDuration(duration); } - } + } break; + + case AUDIO_CMD_SAMPLERATE: { + auto sampleRate = readWord_t(); if (sampleRate == -1) return; + + if (channel == 255) { + // set underlying sample rate + setSampleRate(sampleRate); + } else if (channelEnabled(channel)) { + audioChannels[channel]->setSampleRate(sampleRate); + } + } break; } } From 1236fc21d6d9b23a11b69611e9c9ddcb3047f96b Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Tue, 12 Dec 2023 13:56:01 +0000 Subject: [PATCH 11/15] hard-code the default sample rate --- video/agon.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/video/agon.h b/video/agon.h index 3df2fd0..79a6de1 100644 --- a/video/agon.h +++ b/video/agon.h @@ -68,7 +68,7 @@ #define PACKET_MOUSE 0x09 // Mouse data #define AUDIO_CHANNELS 3 // Default number of audio channels -#define AUDIO_DEFAULT_SAMPLE_RATE FABGL_SOUNDGEN_DEFAULT_SAMPLE_RATE // Default sample rate +#define AUDIO_DEFAULT_SAMPLE_RATE 16384 // Default sample rate #define MAX_AUDIO_CHANNELS 32 // Maximum number of audio channels #define PLAY_SOUND_PRIORITY 3 // Sound driver task priority with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest From b2437aca762a8083a796a62c1e6b98eaed024135 Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Tue, 12 Dec 2023 14:21:21 +0000 Subject: [PATCH 12/15] use attachSoundGenerator --- video/audio_channel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/video/audio_channel.h b/video/audio_channel.h index 8e4c65f..c9dbb95 100644 --- a/video/audio_channel.h +++ b/video/audio_channel.h @@ -201,7 +201,7 @@ void AudioChannel::setWaveform(int8_t waveformType, std::shared_ptr_waveform = std::move(newWaveform); _waveformType = waveformType; - soundGenerator->attach(this->_waveform.get()); + attachSoundGenerator(); debug_log("AudioChannel: setWaveform %d done\n\r", waveformType); } } From 21ad5a0845ae0ca06a7d636e7ba502afbe951112 Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Tue, 12 Dec 2023 16:04:43 +0000 Subject: [PATCH 13/15] allow repeat length of -1 to be set --- video/vdu_audio.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/video/vdu_audio.h b/video/vdu_audio.h index f90d98a..b83c42e 100644 --- a/video/vdu_audio.h +++ b/video/vdu_audio.h @@ -345,6 +345,9 @@ uint8_t VDUStreamProcessor::setSampleRepeatLength(uint16_t sampleId, uint32_t re debug_log("vdu_sys_audio: sample %d not found\n\r", sampleId); return 0; } + if (repeatLength == 0xFFFFFF) { + repeatLength = -1; + } samples[sampleId]->repeatLength = repeatLength; return 1; } From de83fc0ec480f431e4278d941957e6f2e7f4a3b9 Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Sat, 16 Dec 2023 15:16:17 +0000 Subject: [PATCH 14/15] fix duration calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit duration of a sample when played back calculation was buggy, resulting in looping samples when they shouldn’t the sample generator’s `getDuration` call needs to calculate based on how long the _underlying_ audio system will take to step through samples, therefore it is the sample rate of the underlying system that needs to be used for calculations --- video/audio_sample.h | 6 ------ video/enhanced_samples_generator.h | 5 ++++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/video/audio_sample.h b/video/audio_sample.h index f2641d2..283bf7a 100644 --- a/video/audio_sample.h +++ b/video/audio_sample.h @@ -16,7 +16,6 @@ struct AudioSample { int8_t getSample(uint32_t & index, uint32_t & blockIndex); void seekTo(uint32_t position, uint32_t & index, uint32_t & blockIndex, int32_t & repeatCount); uint32_t getSize(); - uint32_t getDuration(); std::vector>& blocks; uint8_t format; // Format of the sample data @@ -94,9 +93,4 @@ uint32_t AudioSample::getSize() { return samples; } -uint32_t AudioSample::getDuration() { - // returns duration of sample in ms when played back without tuning - return (getSize() * 1000) / sampleRate; -} - #endif // AUDIO_SAMPLE_H diff --git a/video/enhanced_samples_generator.h b/video/enhanced_samples_generator.h index cbe4d26..bc00b63 100644 --- a/video/enhanced_samples_generator.h +++ b/video/enhanced_samples_generator.h @@ -83,7 +83,10 @@ int EnhancedSamplesGenerator::getSample() { } int EnhancedSamplesGenerator::getDuration(uint16_t frequency) { - return _sample.expired() ? 0 : _sample.lock()->getDuration() / calculateSamplerate(frequency); + // TODO this will produce an incorrect duration if the sample rate for the channel has been + // adjusted to differ from the underlying audio system sample rate + // At this point it's not clear how to resolve this, so we'll assume it hasn't been adjusted + return _sample.expired() ? 0 : (_sample.lock()->getSize() * 1000 / sampleRate()) / calculateSamplerate(frequency); } void EnhancedSamplesGenerator::seekTo(uint32_t position) { From 16edeaf24f6c9d4b4142704176cbc4ffbb52289d Mon Sep 17 00:00:00 2001 From: Steve Sims Date: Mon, 8 Jan 2024 15:32:35 +0000 Subject: [PATCH 15/15] Thread-safe audio avoids placing audio objects (waveform generators and AudioSample objects) in psram, as the ESP32 has cache bugs related to psram. whilst there is a compiler work-around for this (which we're using) the fix doesn't seem to work in the context of our multi-threaded and interrupt-driven audio system audio logging improved audio channel state is an atomic value to ensure consistency across different threads --- video/agon_audio.h | 6 +- video/audio_channel.h | 134 +++++++++++++++++------------ video/audio_sample.h | 25 +++--- video/enhanced_samples_generator.h | 77 +++++++---------- video/vdu_audio.h | 6 +- video/vdu_buffered.h | 9 +- 6 files changed, 135 insertions(+), 122 deletions(-) diff --git a/video/agon_audio.h b/video/agon_audio.h index e5557fb..288abe7 100644 --- a/video/agon_audio.h +++ b/video/agon_audio.h @@ -73,12 +73,12 @@ void setSampleRate(uint16_t sampleRate) { if (sampleRate == 65535) { sampleRate = AUDIO_DEFAULT_SAMPLE_RATE; } - // detatch the old sound generator + // detach the old sound generator if (soundGenerator) { soundGenerator->play(false); for (auto channelPair : audioChannels) { auto channel = channelPair.second; - soundGenerator->detach(channel->getWaveform()); + channel->detachSoundGenerator(); } } // delete the old sound generator @@ -160,7 +160,7 @@ uint8_t clearSample(uint16_t sampleId) { debug_log("clearSample: sample %d not found\n\r", sampleId); return 0; } - samples.erase(sampleId); + samples[sampleId] = nullptr; debug_log("reset sample\n\r"); return 1; } diff --git a/video/audio_channel.h b/video/audio_channel.h index c9dbb95..922ae5f 100644 --- a/video/audio_channel.h +++ b/video/audio_channel.h @@ -2,6 +2,7 @@ #define AUDIO_CHANNEL_H #include +#include #include #include @@ -30,24 +31,25 @@ class AudioChannel { void setSampleRate(uint16_t sampleRate); WaveformGenerator * getWaveform() { return this->_waveform.get(); } void attachSoundGenerator(); + void detachSoundGenerator(); void seekTo(uint32_t position); void loop(); uint8_t channel() { return _channel; } private: - std::unique_ptr getSampleWaveform(uint16_t sampleId, std::shared_ptr channelRef); + std::shared_ptr getSampleWaveform(uint16_t sampleId, std::shared_ptr channelRef); void waitForAbort(); uint8_t getVolume(uint32_t elapsed); uint16_t getFrequency(uint32_t elapsed); bool isReleasing(uint32_t elapsed); bool isFinished(uint32_t elapsed); - AudioState _state; uint8_t _channel; uint8_t _volume; uint16_t _frequency; int32_t _duration; uint32_t _startTime; uint8_t _waveformType; - std::unique_ptr _waveform; + std::atomic _state; + std::shared_ptr _waveform = nullptr; std::unique_ptr _volumeEnvelope; std::unique_ptr _frequencyEnvelope; }; @@ -56,27 +58,26 @@ class AudioChannel { #include "enhanced_samples_generator.h" extern std::unordered_map> samples; // Storage for the sample data -AudioChannel::AudioChannel(uint8_t channel) { - this->_channel = channel; - this->_state = AudioState::Idle; - this->_volume = 64; - this->_frequency = 750; - this->_duration = -1; +AudioChannel::AudioChannel(uint8_t channel) : _channel(channel), _state(AudioState::Idle), _volume(64), _frequency(750), _duration(-1) { setWaveform(AUDIO_WAVE_DEFAULT, nullptr); - debug_log("AudioChannel: init %d\n\r", this->_channel); + debug_log("AudioChannel: init %d\n\r", channel); debug_log("free mem: %d\n\r", heap_caps_get_free_size(MALLOC_CAP_8BIT)); } AudioChannel::~AudioChannel() { - debug_log("AudioChannel: deiniting %d\n\r", this->_channel); + debug_log("AudioChannel: deiniting %d\n\r", channel()); if (this->_waveform) { this->_waveform->enable(false); - soundGenerator->detach(this->_waveform.get()); + soundGenerator->detach(getWaveform()); } - debug_log("AudioChannel: deinit %d\n\r", this->_channel); + debug_log("AudioChannel: deinit %d\n\r", channel()); } uint8_t AudioChannel::playNote(uint8_t volume, uint16_t frequency, int32_t duration) { + if (!this->_waveform) { + debug_log("AudioChannel: no waveform on channel %d\n\r", channel()); + return 0; + } switch (this->_state) { case AudioState::Idle: case AudioState::Release: @@ -87,7 +88,7 @@ uint8_t AudioChannel::playNote(uint8_t volume, uint16_t frequency, int32_t durat // zero duration means play whole sample // NB this can only work out sample duration based on sample provided // so if sample data is streaming in an explicit length should be used instead - this->_duration = ((EnhancedSamplesGenerator *)this->_waveform.get())->getDuration(frequency); + this->_duration = ((EnhancedSamplesGenerator *)getWaveform())->getDuration(frequency); if (this->_volumeEnvelope) { // subtract the "release" time from the duration this->_duration -= this->_volumeEnvelope->getRelease(); @@ -97,7 +98,7 @@ uint8_t AudioChannel::playNote(uint8_t volume, uint16_t frequency, int32_t durat } } this->_state = AudioState::Pending; - debug_log("AudioChannel: playNote %d,%d,%d,%d\n\r", this->_channel, this->_volume, this->_frequency, this->_duration); + debug_log("AudioChannel: playNote %d,%d,%d,%d\n\r", channel(), volume, frequency, this->_duration); return 1; } return 0; @@ -129,47 +130,54 @@ uint8_t AudioChannel::getStatus() { return status; } -std::unique_ptr AudioChannel::getSampleWaveform(uint16_t sampleId, std::shared_ptr channelRef) { +std::shared_ptr AudioChannel::getSampleWaveform(uint16_t sampleId, std::shared_ptr channelRef) { if (samples.find(sampleId) != samples.end()) { auto sample = samples.at(sampleId); - // remove this channel from other samples - for (auto samplePair : samples) { - if (samplePair.second) { - samplePair.second->channels.erase(_channel); - } - } - sample->channels[_channel] = channelRef; + // if (sample->channels.find(_channel) != sample->channels.end()) { + // // this channel is already playing this sample, so do nothing + // debug_log("AudioChannel: already playing sample %d on channel %d\n\r", sampleId, channel()); + // return nullptr; + // } - return make_unique_psram(sample); + // TODO remove channel tracking?? + // remove this channel from other samples + // for (auto samplePair : samples) { + // if (samplePair.second) { + // samplePair.second->channels.erase(_channel); + // } + // } + // sample->channels[_channel] = channelRef; + + return std::make_shared(sample); } return nullptr; } void AudioChannel::setWaveform(int8_t waveformType, std::shared_ptr channelRef, uint16_t sampleId) { - std::unique_ptr newWaveform = nullptr; + std::shared_ptr newWaveform = nullptr; switch (waveformType) { case AUDIO_WAVE_SAWTOOTH: - newWaveform = make_unique_psram(); + newWaveform = std::make_shared(); break; case AUDIO_WAVE_SQUARE: - newWaveform = make_unique_psram(); + newWaveform = std::make_shared(); break; case AUDIO_WAVE_SINE: - newWaveform = make_unique_psram(); + newWaveform = std::make_shared(); break; case AUDIO_WAVE_TRIANGLE: - newWaveform = make_unique_psram(); + newWaveform = std::make_shared(); break; case AUDIO_WAVE_NOISE: - newWaveform = make_unique_psram(); + newWaveform = std::make_shared(); break; case AUDIO_WAVE_VICNOISE: - newWaveform = make_unique_psram(); + newWaveform = std::make_shared(); break; case AUDIO_WAVE_SAMPLE: // Buffer-based sample playback - debug_log("AudioChannel: using sample buffer %d for waveform\n\r", sampleId); + debug_log("AudioChannel: using sample buffer %d for waveform on channel %d\n\r", sampleId, channel()); newWaveform = getSampleWaveform(sampleId, channelRef); break; default: @@ -177,17 +185,17 @@ void AudioChannel::setWaveform(int8_t waveformType, std::shared_ptr_state != AudioState::Idle) { debug_log("AudioChannel: aborting current playback\n\r"); // some kind of playback is happening, so abort any current task delay to allow playback to end @@ -197,17 +205,17 @@ void AudioChannel::setWaveform(int8_t waveformType, std::shared_ptr_waveform) { debug_log("AudioChannel: detaching old waveform\n\r"); - soundGenerator->detach(this->_waveform.get()); + detachSoundGenerator(); } - this->_waveform = std::move(newWaveform); + this->_waveform = newWaveform; _waveformType = waveformType; attachSoundGenerator(); - debug_log("AudioChannel: setWaveform %d done\n\r", waveformType); + debug_log("AudioChannel: setWaveform %d done on channel %d\n\r", waveformType, channel()); } } void AudioChannel::setVolume(uint8_t volume) { - debug_log("AudioChannel: setVolume %d\n\r", volume); + debug_log("AudioChannel: setVolume %d on channel %d\n\r", volume, channel()); if (this->_waveform) { waitForAbort(); @@ -254,7 +262,7 @@ void AudioChannel::setVolume(uint8_t volume) { } void AudioChannel::setFrequency(uint16_t frequency) { - debug_log("AudioChannel: setFrequency %d\n\r", frequency); + debug_log("AudioChannel: setFrequency %d on channel %d\n\r", frequency, channel()); this->_frequency = frequency; if (this->_waveform) { @@ -272,7 +280,7 @@ void AudioChannel::setFrequency(uint16_t frequency) { } void AudioChannel::setDuration(int32_t duration) { - debug_log("AudioChannel: setDuration %d\n\r", duration); + debug_log("AudioChannel: setDuration %d on channel %d\n\r", duration, channel()); if (duration == 0xFFFFFF) { duration = -1; } @@ -321,13 +329,19 @@ void AudioChannel::setSampleRate(uint16_t sampleRate) { void AudioChannel::attachSoundGenerator() { if (this->_waveform) { - soundGenerator->attach(this->_waveform.get()); + soundGenerator->attach(getWaveform()); + } +} + +void AudioChannel::detachSoundGenerator() { + if (this->_waveform) { + soundGenerator->detach(getWaveform()); } } void AudioChannel::seekTo(uint32_t position) { if (this->_waveformType == AUDIO_WAVE_SAMPLE) { - ((EnhancedSamplesGenerator *)this->_waveform.get())->seekTo(position); + ((EnhancedSamplesGenerator *)getWaveform())->seekTo(position); } } @@ -373,9 +387,11 @@ bool AudioChannel::isFinished(uint32_t elapsed) { } void AudioChannel::loop() { + int delay = 0; + switch (this->_state) { case AudioState::Pending: - debug_log("AudioChannel: play %d,%d,%d,%d\n\r", this->_channel, this->_volume, this->_frequency, this->_duration); + debug_log("AudioChannel: play %d,%d,%d,%d\n\r", channel(), this->_volume, this->_frequency, this->_duration); // we have a new note to play this->_startTime = millis(); // set our initial volume and frequency @@ -389,33 +405,35 @@ void AudioChannel::loop() { } else { this->_state = AudioState::Playing; // if delay value is negative then this delays for a super long time - vTaskDelay(pdMS_TO_TICKS(this->_duration)); + delay = this->_duration; } break; + case AudioState::Playing: if (this->_duration >= 0) { // simple playback - delay until we have reached our duration uint32_t elapsed = millis() - this->_startTime; - debug_log("AudioChannel: elapsed %d\n\r", elapsed); + debug_log("AudioChannel: %d elapsed %d\n\r", channel(), elapsed); if (elapsed >= this->_duration) { this->_waveform->enable(false); - debug_log("AudioChannel: end\n\r"); + debug_log("AudioChannel: %d end\n\r", channel()); this->_state = AudioState::Idle; } else { - debug_log("AudioChannel: loop (%d)\n\r", this->_duration - elapsed); - vTaskDelay(pdMS_TO_TICKS(this->_duration - elapsed)); + debug_log("AudioChannel: %d loop (%d)\n\r", channel(), this->_duration - elapsed); + delay = this->_duration - elapsed; } } else { // our duration is indefinite, so delay for a long time - debug_log("AudioChannel: loop (indefinite playback)\n\r"); - vTaskDelay(pdMS_TO_TICKS(-1)); + debug_log("AudioChannel: %d loop (indefinite playback)\n\r", channel()); + delay = -1; } break; + // loop and release states used for envelopes case AudioState::PlayLoop: { uint32_t elapsed = millis() - this->_startTime; if (isReleasing(elapsed)) { - debug_log("AudioChannel: releasing...\n\r"); + debug_log("AudioChannel: releasing %d...\n\r", channel()); this->_state = AudioState::Release; } // update volume and frequency as appropriate @@ -425,6 +443,7 @@ void AudioChannel::loop() { this->_waveform->setFrequency(this->getFrequency(elapsed)); break; } + case AudioState::Release: { uint32_t elapsed = millis() - this->_startTime; // update volume and frequency as appropriate @@ -435,17 +454,22 @@ void AudioChannel::loop() { if (isFinished(elapsed)) { this->_waveform->enable(false); - debug_log("AudioChannel: end (released)\n\r"); + debug_log("AudioChannel: end (released %d)\n\r", channel()); this->_state = AudioState::Idle; } break; } + case AudioState::Abort: this->_waveform->enable(false); - debug_log("AudioChannel: abort\n\r"); + debug_log("AudioChannel: abort %d\n\r", channel()); this->_state = AudioState::Idle; break; } + + if (delay != 0) { + vTaskDelay(pdMS_TO_TICKS(delay)); + } } #endif // AUDIO_CHANNEL_H diff --git a/video/audio_sample.h b/video/audio_sample.h index 283bf7a..0d54eae 100644 --- a/video/audio_sample.h +++ b/video/audio_sample.h @@ -9,7 +9,7 @@ #include "buffer_stream.h" struct AudioSample { - AudioSample(std::vector>& streams, uint8_t format, uint32_t sampleRate = AUDIO_DEFAULT_SAMPLE_RATE, uint16_t frequency = 0) : + AudioSample(std::vector> streams, uint8_t format, uint32_t sampleRate = AUDIO_DEFAULT_SAMPLE_RATE, uint16_t frequency = 0) : blocks(streams), format(format), sampleRate(sampleRate), baseFrequency(frequency) {} ~AudioSample(); @@ -17,27 +17,26 @@ struct AudioSample { void seekTo(uint32_t position, uint32_t & index, uint32_t & blockIndex, int32_t & repeatCount); uint32_t getSize(); - std::vector>& blocks; + std::vector> blocks; uint8_t format; // Format of the sample data uint32_t sampleRate; // Sample rate of the sample uint16_t baseFrequency = 0; // Base frequency of the sample int32_t repeatStart = 0; // Start offset for repeat, in samples int32_t repeatLength = -1; // Length of the repeat section in samples, -1 means to end of sample - std::unordered_map> channels; // Channels playing this sample + // std::unordered_map> channels; // Channels playing this sample }; AudioSample::~AudioSample() { // iterate over channels - for (auto channelPair : this->channels) { - auto channelRef = channelPair.second; - if (!channelRef.expired()) { - auto channel = channelRef.lock(); - debug_log("AudioSample: removing sample from channel %d\n\r", channel->channel()); - // Remove sample from channel - channel->setWaveform(AUDIO_WAVE_DEFAULT, nullptr); - } - } - debug_log("AudioSample cleared\n\r"); + // for (auto channelPair : this->channels) { + // auto channel = channelPair.second.lock(); + // if (channel) { + // // Remove sample from channel + // debug_log("AudioSample: removing sample from channel %d\n\r", channel->channel()); + // // TODO change so only removes if channel is definitely set to this sample + // channel->setWaveform(AUDIO_WAVE_DEFAULT, nullptr); + // } + // } } int8_t AudioSample::getSample(uint32_t & index, uint32_t & blockIndex) { diff --git a/video/enhanced_samples_generator.h b/video/enhanced_samples_generator.h index bc00b63..2f2f1f7 100644 --- a/video/enhanced_samples_generator.h +++ b/video/enhanced_samples_generator.h @@ -23,26 +23,26 @@ class EnhancedSamplesGenerator : public WaveformGenerator { void seekTo(uint32_t position); private: - std::weak_ptr _sample; + std::shared_ptr _sample; - uint32_t index; // Current index inside the current sample block - uint32_t blockIndex; // Current index into the sample data blocks - int32_t repeatCount = 0; // Sample count when repeating + uint32_t index; // Current index inside the current sample block + uint32_t blockIndex; // Current index into the sample data blocks + int32_t repeatCount; // Sample count when repeating // TODO consider whether repeatStart and repeatLength may need to be here // which would allow for per-channel repeat settings - int frequency = 0; - int previousSample = 0; - int currentSample = 0; - double samplesPerGet = 1.0; - double fractionalSampleOffset = 0.0; + int frequency; + int previousSample; + int currentSample; + double samplesPerGet; + double fractionalSampleOffset; double calculateSamplerate(uint16_t frequency); int8_t getNextSample(); }; EnhancedSamplesGenerator::EnhancedSamplesGenerator(std::shared_ptr sample) - : _sample(sample) + : _sample(sample), repeatCount(0), index(0), blockIndex(0), frequency(0), previousSample(0), currentSample(0), samplesPerGet(1.0), fractionalSampleOffset(0.0) {} void EnhancedSamplesGenerator::setFrequency(int value) { @@ -56,23 +56,21 @@ void EnhancedSamplesGenerator::setSampleRate(int value) { } int EnhancedSamplesGenerator::getSample() { - if (duration() == 0 || _sample.expired()) { + if (duration() == 0) { return 0; } - auto samplePtr = _sample.lock(); - // if we've moved far enough along, read the next sample while (fractionalSampleOffset >= 1.0) { previousSample = currentSample; currentSample = getNextSample(); - fractionalSampleOffset -= 1.0; + fractionalSampleOffset = fractionalSampleOffset - 1.0; } // Interpolate between the samples to reduce aliasing int sample = currentSample * fractionalSampleOffset + previousSample * (1.0-fractionalSampleOffset); - fractionalSampleOffset += samplesPerGet; + fractionalSampleOffset = fractionalSampleOffset + samplesPerGet; // process volume sample = sample * volume() / 127; @@ -86,46 +84,35 @@ int EnhancedSamplesGenerator::getDuration(uint16_t frequency) { // TODO this will produce an incorrect duration if the sample rate for the channel has been // adjusted to differ from the underlying audio system sample rate // At this point it's not clear how to resolve this, so we'll assume it hasn't been adjusted - return _sample.expired() ? 0 : (_sample.lock()->getSize() * 1000 / sampleRate()) / calculateSamplerate(frequency); + return !_sample ? 0 : (_sample->getSize() * 1000 / sampleRate()) / calculateSamplerate(frequency); } void EnhancedSamplesGenerator::seekTo(uint32_t position) { - if (!_sample.expired()) { - auto samplePtr = _sample.lock(); - samplePtr->seekTo(position, index, blockIndex, repeatCount); - - // prepare our fractional sample data for playback - fractionalSampleOffset = 0.0; - previousSample = samplePtr->getSample(index, blockIndex); - currentSample = samplePtr->getSample(index, blockIndex); - } + _sample->seekTo(position, index, blockIndex, repeatCount); + + // prepare our fractional sample data for playback + fractionalSampleOffset = 0.0; + previousSample = _sample->getSample(index, blockIndex); + currentSample = _sample->getSample(index, blockIndex); } double EnhancedSamplesGenerator::calculateSamplerate(uint16_t frequency) { - if (!_sample.expired()) { - auto samplePtr = _sample.lock(); - auto baseFrequency = samplePtr->baseFrequency; - auto frequencyAdjust = baseFrequency > 0 ? (double)frequency / (double)baseFrequency : 1.0; - return frequencyAdjust * ((double)samplePtr->sampleRate / (double)(sampleRate())); - } - return 1.0; + auto baseFrequency = _sample->baseFrequency; + auto frequencyAdjust = baseFrequency > 0 ? (double)frequency / (double)baseFrequency : 1.0; + return frequencyAdjust * ((double)_sample->sampleRate / (double)(sampleRate())); } int8_t EnhancedSamplesGenerator::getNextSample() { - if (!_sample.expired()) { - auto samplePtr = _sample.lock(); - auto sample = samplePtr->getSample(index, blockIndex); - - // looping magic - repeatCount--; - if (repeatCount == 0) { - // we've reached the end of the repeat section, so loop back - seekTo(samplePtr->repeatStart); - } - - return sample; + auto sample = _sample->getSample(index, blockIndex); + + // looping magic + repeatCount--; + if (repeatCount == 0) { + // we've reached the end of the repeat section, so loop back + seekTo(_sample->repeatStart); } - return 0; + + return sample; } #endif // ENHANCED_SAMPLES_GENERATOR_H diff --git a/video/vdu_audio.h b/video/vdu_audio.h index b83c42e..aa4b266 100644 --- a/video/vdu_audio.h +++ b/video/vdu_audio.h @@ -137,7 +137,7 @@ void VDUStreamProcessor::vdu_sys_audio() { debug_log(" samples count: %d\n\r", samples.size()); debug_log(" free mem: %d\n\r", heap_caps_get_free_size(MALLOC_CAP_8BIT)); auto sample = samples[sampleNum]; - if (sample == nullptr) { + if (!sample) { debug_log(" sample is null\n\r"); break; } @@ -254,8 +254,8 @@ uint8_t VDUStreamProcessor::createSampleFromBuffer(uint16_t bufferId, uint8_t fo } clearSample(bufferId); auto sample = (format & AUDIO_FORMAT_WITH_RATE) ? - make_shared_psram(buffers[bufferId], format & AUDIO_FORMAT_DATA_MASK, sampleRate) - : make_shared_psram(buffers[bufferId], format & AUDIO_FORMAT_DATA_MASK); + std::make_shared(buffers[bufferId], format & AUDIO_FORMAT_DATA_MASK, sampleRate) + : std::make_shared(buffers[bufferId], format & AUDIO_FORMAT_DATA_MASK); if (sample) { if (format & AUDIO_FORMAT_TUNEABLE) { sample->baseFrequency = AUDIO_DEFAULT_FREQUENCY; diff --git a/video/vdu_buffered.h b/video/vdu_buffered.h index b06f5a3..3d13897 100644 --- a/video/vdu_buffered.h +++ b/video/vdu_buffered.h @@ -244,7 +244,10 @@ void VDUStreamProcessor::bufferCall(uint16_t callBufferId, uint32_t offset) { void VDUStreamProcessor::bufferClear(uint16_t bufferId) { debug_log("bufferClear: buffer %d\n\r", bufferId); if (bufferId == 65535) { - buffers.clear(); + // iterate thru our buffers and clear their vectors + for (auto bufferPair : buffers) { + bufferPair.second.clear(); + } resetBitmaps(); resetSamples(); return; @@ -253,7 +256,7 @@ void VDUStreamProcessor::bufferClear(uint16_t bufferId) { debug_log("bufferClear: buffer %d not found\n\r", bufferId); return; } - buffers.erase(bufferId); + buffers[bufferId].clear(); clearBitmap(bufferId); clearSample(bufferId); debug_log("bufferClear: cleared buffer %d\n\r", bufferId); @@ -732,7 +735,7 @@ void VDUStreamProcessor::bufferConsolidate(uint16_t bufferId) { void clearTargets(std::vector targets) { for (auto target : targets) { if (buffers.find(target) != buffers.end()) { - buffers.erase(target); + buffers[target].clear(); } clearBitmap(target); }