@@ -190,6 +190,12 @@ float voiceGainFromVolume(uint8_t volume) {
190190// Keep disabled by default to preserve percussive transients.
191191constexpr uint32_t kVoiceFadeInUs = 0 ;
192192
193+ enum class VoiceSourceType : uint8_t {
194+ None,
195+ StreamPath,
196+ RamData,
197+ };
198+
193199} // namespace
194200
195201struct Audio ::Impl {
@@ -204,6 +210,15 @@ struct Audio::Impl {
204210 AudioOutputMixerStub *stub = nullptr ;
205211 float targetGain = 0 .0f ;
206212 float currentGain = 0 .0f ;
213+ uint8_t volume = 100 ;
214+ bool loopEnabled = false ;
215+ VoiceSourceType sourceType = VoiceSourceType::None;
216+ char path[128 ] = {0 };
217+ const uint8_t *ramData = nullptr ;
218+ uint32_t ramDataBytes = 0 ;
219+ uint16_t channelCount = 0 ;
220+ uint32_t sampleRate = 0 ;
221+ uint16_t bitsPerSample = 0 ;
207222 };
208223
209224 StableAudioOutputI2S *out = nullptr ;
@@ -243,6 +258,15 @@ void stopVoice(Audio::Impl::Voice &voice) {
243258 voice.retriggerGroupId = -1 ;
244259 voice.targetGain = 0 .0f ;
245260 voice.currentGain = 0 .0f ;
261+ voice.volume = 100 ;
262+ voice.loopEnabled = false ;
263+ voice.sourceType = VoiceSourceType::None;
264+ voice.path [0 ] = ' \0 ' ;
265+ voice.ramData = nullptr ;
266+ voice.ramDataBytes = 0 ;
267+ voice.channelCount = 0 ;
268+ voice.sampleRate = 0 ;
269+ voice.bitsPerSample = 0 ;
246270}
247271
248272void refreshStats (Audio::Impl *impl) {
@@ -378,7 +402,8 @@ bool beginVoiceFromPath(Audio::Impl *impl,
378402 int voiceIndex,
379403 const String &samplePath,
380404 uint8_t volume,
381- int16_t retriggerGroupId) {
405+ int16_t retriggerGroupId,
406+ bool loopEnabled) {
382407 if (!impl || voiceIndex < 0 || voiceIndex >= Audio::kVoiceCount ) return false ;
383408
384409 Audio::Impl::Voice &voice = impl->voices [voiceIndex];
@@ -407,6 +432,15 @@ bool beginVoiceFromPath(Audio::Impl *impl,
407432 impl->nextStartOrder = 1 ;
408433 }
409434 voice.active = true ;
435+ voice.volume = volume;
436+ voice.loopEnabled = loopEnabled;
437+ voice.sourceType = VoiceSourceType::StreamPath;
438+ samplePath.toCharArray (voice.path , sizeof (voice.path ));
439+ voice.ramData = nullptr ;
440+ voice.ramDataBytes = 0 ;
441+ voice.channelCount = 0 ;
442+ voice.sampleRate = 0 ;
443+ voice.bitsPerSample = 0 ;
410444 voice.retriggerGroupId = retriggerGroupId;
411445 return true ;
412446}
@@ -419,7 +453,8 @@ bool beginVoiceFromRam(Audio::Impl *impl,
419453 uint32_t sampleRate,
420454 uint16_t bitsPerSample,
421455 uint8_t volume,
422- int16_t retriggerGroupId) {
456+ int16_t retriggerGroupId,
457+ bool loopEnabled) {
423458 if (!impl || voiceIndex < 0 || voiceIndex >= Audio::kVoiceCount ) return false ;
424459
425460 Audio::Impl::Voice &voice = impl->voices [voiceIndex];
@@ -445,10 +480,56 @@ bool beginVoiceFromRam(Audio::Impl *impl,
445480 impl->nextStartOrder = 1 ;
446481 }
447482 voice.active = true ;
483+ voice.volume = volume;
484+ voice.loopEnabled = loopEnabled;
485+ voice.sourceType = VoiceSourceType::RamData;
486+ voice.path [0 ] = ' \0 ' ;
487+ voice.ramData = pcmData;
488+ voice.ramDataBytes = dataBytes;
489+ voice.channelCount = channelCount;
490+ voice.sampleRate = sampleRate;
491+ voice.bitsPerSample = bitsPerSample;
448492 voice.retriggerGroupId = retriggerGroupId;
449493 return true ;
450494}
451495
496+ bool restartVoiceLoop (Audio::Impl *impl, int voiceIndex) {
497+ if (!impl || voiceIndex < 0 || voiceIndex >= Audio::kVoiceCount ) return false ;
498+
499+ const Audio::Impl::Voice &voice = impl->voices [voiceIndex];
500+ if (!voice.loopEnabled ) return false ;
501+
502+ const VoiceSourceType sourceType = voice.sourceType ;
503+ const String path (voice.path );
504+ const uint8_t volume = voice.volume ;
505+ const int16_t retriggerGroupId = voice.retriggerGroupId ;
506+ const bool loopEnabled = voice.loopEnabled ;
507+ const uint8_t *ramData = voice.ramData ;
508+ const uint32_t ramDataBytes = voice.ramDataBytes ;
509+ const uint16_t channelCount = voice.channelCount ;
510+ const uint32_t sampleRate = voice.sampleRate ;
511+ const uint16_t bitsPerSample = voice.bitsPerSample ;
512+
513+ stopVoice (impl->voices [voiceIndex]);
514+
515+ if (sourceType == VoiceSourceType::StreamPath && path.length () > 0 ) {
516+ return beginVoiceFromPath (impl, voiceIndex, path, volume, retriggerGroupId, loopEnabled);
517+ }
518+ if (sourceType == VoiceSourceType::RamData && ramData && ramDataBytes > 0 ) {
519+ return beginVoiceFromRam (impl,
520+ voiceIndex,
521+ ramData,
522+ ramDataBytes,
523+ channelCount,
524+ sampleRate,
525+ bitsPerSample,
526+ volume,
527+ retriggerGroupId,
528+ loopEnabled);
529+ }
530+ return false ;
531+ }
532+
452533} // namespace
453534
454535Audio::Audio () = default;
@@ -566,8 +647,10 @@ void Audio::update() {
566647 updateVoiceFadeIn (voice, nowUs);
567648
568649 if (!voice.wav ->loop ()) {
569- stopVoice (voice);
570- stateChanged = true ;
650+ if (!voice.loopEnabled || !restartVoiceLoop (impl_, i)) {
651+ stopVoice (voice);
652+ stateChanged = true ;
653+ }
571654 }
572655 }
573656
@@ -581,7 +664,10 @@ void Audio::update() {
581664 maybeLogRuntimeDiagnostics (impl_);
582665}
583666
584- void Audio::playSamplePath (const String &samplePath, uint8_t volume, int16_t retriggerGroupId) {
667+ void Audio::playSamplePath (const String &samplePath,
668+ uint8_t volume,
669+ int16_t retriggerGroupId,
670+ bool loopEnabled) {
585671 if (!impl_ || samplePath.length () == 0 ) return ;
586672 const uint32_t playStartUs = micros ();
587673
@@ -601,7 +687,7 @@ void Audio::playSamplePath(const String &samplePath, uint8_t volume, int16_t ret
601687 }
602688
603689 stopVoice (voice);
604- if (!beginVoiceFromPath (impl_, voiceIndex, samplePath, volume, retriggerGroupId)) {
690+ if (!beginVoiceFromPath (impl_, voiceIndex, samplePath, volume, retriggerGroupId, loopEnabled )) {
605691 Serial.println (" Sample open/start failed" );
606692 refreshStats (impl_);
607693 recordPlayCost (impl_, micros () - playStartUs);
@@ -620,13 +706,39 @@ void Audio::stopAllVoices() {
620706 refreshStats (impl_);
621707}
622708
709+ void Audio::stopLoopingVoicesForGroup (int16_t retriggerGroupId) {
710+ if (!impl_ || retriggerGroupId < 0 ) return ;
711+ bool changed = false ;
712+ for (int i = 0 ; i < kVoiceCount ; i++) {
713+ Impl::Voice &voice = impl_->voices [i];
714+ if (!voice.active || !voice.loopEnabled ) continue ;
715+ if (voice.retriggerGroupId != retriggerGroupId) continue ;
716+ stopVoice (voice);
717+ changed = true ;
718+ }
719+ if (changed) {
720+ refreshStats (impl_);
721+ }
722+ }
723+
724+ void Audio::setLoopEnabledForGroup (int16_t retriggerGroupId, bool loopEnabled) {
725+ if (!impl_ || retriggerGroupId < 0 ) return ;
726+ for (int i = 0 ; i < kVoiceCount ; i++) {
727+ Impl::Voice &voice = impl_->voices [i];
728+ if (!voice.active ) continue ;
729+ if (voice.retriggerGroupId != retriggerGroupId) continue ;
730+ voice.loopEnabled = loopEnabled;
731+ }
732+ }
733+
623734bool Audio::playSampleRam (const uint8_t *pcmData,
624735 uint32_t dataBytes,
625736 uint16_t channelCount,
626737 uint32_t sampleRate,
627738 uint16_t bitsPerSample,
628739 uint8_t volume,
629- int16_t retriggerGroupId) {
740+ int16_t retriggerGroupId,
741+ bool loopEnabled) {
630742 if (!impl_ || !pcmData || dataBytes == 0 ) return false ;
631743 const uint32_t playStartUs = micros ();
632744
@@ -653,7 +765,8 @@ bool Audio::playSampleRam(const uint8_t *pcmData,
653765 sampleRate,
654766 bitsPerSample,
655767 volume,
656- retriggerGroupId);
768+ retriggerGroupId,
769+ loopEnabled);
657770 refreshStats (impl_);
658771 recordPlayCost (impl_, micros () - playStartUs);
659772 return started;
0 commit comments