Improved the fade in and fade out effect
This commit is contained in:
		
							parent
							
								
									84a82a8b27
								
							
						
					
					
						commit
						a2c6cba7f4
					
				| @ -51,18 +51,22 @@ bool merge::merge_n_sources(void *dest, void **srcs, size_t src_length, size_t c | ||||
| 		srcs++; | ||||
| 		src_length--; | ||||
| 
 | ||||
| 		if(src_length == 0) | ||||
| 			return false; | ||||
| 		if(src_length == 0) { | ||||
|             return false; | ||||
| 		} | ||||
| 	} | ||||
| 	if(srcs[0] != dest) | ||||
| 	    memcpy(dest, srcs[0], channels * samples * 4); | ||||
| 	if(srcs[0] != dest) { | ||||
|         memcpy(dest, srcs[0], channels * samples * 4); | ||||
| 	} | ||||
| 
 | ||||
| 	srcs++; | ||||
| 	src_length--; | ||||
| 
 | ||||
| 	while(src_length > 0) { | ||||
| 		/* only invoke is srcs is not null! */ | ||||
| 		if(srcs[0] && !merge::merge_sources(dest, srcs[0], dest, channels, samples)) | ||||
| 			return false; | ||||
| 		if(srcs[0] && !merge::merge_sources(dest, srcs[0], dest, channels, samples)) { | ||||
|             return false; | ||||
| 		} | ||||
| 
 | ||||
| 		srcs++; | ||||
| 		src_length--; | ||||
|  | ||||
| @ -12,242 +12,309 @@ using namespace tc; | ||||
| using namespace tc::audio; | ||||
| 
 | ||||
| void AudioOutputSource::clear() { | ||||
|     std::lock_guard buffer_lock{this->buffer_mutex}; | ||||
|     this->buffer.clear(); | ||||
|     this->buffering = true; | ||||
|     this->fade_in_start = this->buffer.write_ptr(); | ||||
|     this->buffer_state = buffer_state::buffering; | ||||
|     this->fadeout_samples_left = 0; | ||||
| } | ||||
| 
 | ||||
| void AudioOutputSource::do_fade_out(size_t pop_count) { | ||||
|     if(this->will_buffer_in != -1) return; | ||||
| 
 | ||||
|     _test_for_fade: | ||||
|     const auto samples_left = this->current_latency(); | ||||
|     if(samples_left < this->fadeout_sample_length + pop_count) { | ||||
|         if(auto fn = this->on_underflow; fn && fn(0)) | ||||
|             goto _test_for_fade; | ||||
| 
 | ||||
|         auto total_samples = std::min(samples_left, this->fadeout_sample_length); | ||||
|         if(total_samples == 0) return; //TODO Test against min_buffered_samples
 | ||||
| 
 | ||||
|         auto wptr = (float*) this->buffer.calculate_backward_write_ptr(total_samples * this->channel_count * sizeof(float)); | ||||
|         for(size_t index{0}; index <= total_samples; index++) { | ||||
|             const auto offset = (float) ((float) index / (float) total_samples); | ||||
|             const auto volume = log10f(offset) / -2.71828182845904f; | ||||
|             for(int channel{0}; channel < this->channel_count; channel++) | ||||
|                 *wptr++ *= volume; | ||||
|         } | ||||
| 
 | ||||
|         log_trace(category::audio, tr("Will buffer due to fade out ({} | {})"), total_samples, *(float*) this->buffer.write_ptr()); | ||||
|         this->will_buffer_in = total_samples; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void AudioOutputSource::do_fade_in() { | ||||
|     if(!this->fade_in_start) | ||||
|         return; | ||||
| 
 | ||||
|     const auto samples_available = this->current_latency(); | ||||
|     auto wptr = (float*) this->fade_in_start; | ||||
|     auto total_samples = std::min(samples_available, this->fadeout_sample_length); | ||||
|     if(total_samples == 0) { | ||||
|         log_trace(category::audio, tr("Ignoring fade in 0: {} {}"), samples_available, this->fadeout_sample_length); | ||||
| void AudioOutputSource::apply_fadeout() { | ||||
|     const auto samples_available = this->currently_buffered_samples(); | ||||
|     auto fade_samples = std::min(samples_available, this->fadeout_frame_samples_); | ||||
|     if(!fade_samples) { | ||||
|         this->fadeout_samples_left = 0; | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     for(size_t index{0}; index < total_samples; index++) { | ||||
|         const auto offset = (float) ((float) index / (float) total_samples); | ||||
|         const auto volume = log10f(1 - offset) / -2.71828182845904f; | ||||
|         for(int channel{0}; channel < this->channel_count; channel++) | ||||
|             *wptr++ *= volume; | ||||
|     } | ||||
|     const auto sample_byte_size = this->channel_count * sizeof(float) * fade_samples; | ||||
|     assert(this->buffer.fill_count() >= sample_byte_size); | ||||
|     auto write_ptr = (float*) ((char*) this->buffer.read_ptr() + (this->buffer.fill_count() - sample_byte_size)); | ||||
| 
 | ||||
|     log_trace(category::audio, tr("Fade in to new buffer ({})"), total_samples); | ||||
|     for(size_t index{0}; index < fade_samples; index++) { | ||||
|         const auto offset = (float) ((float) (index + 1) / (float) fade_samples); | ||||
|         const auto volume = std::min(log10f(offset) / -2.71828182845904f, 1.f); | ||||
| 
 | ||||
|     this->fade_in_start = nullptr; | ||||
| } | ||||
| 
 | ||||
| ssize_t AudioOutputSource::pop_samples(void *buffer, size_t samples) { | ||||
|     size_t written{0}, written_bytes{0}; | ||||
| 
 | ||||
|     load_buffer: | ||||
|     auto available_bytes = this->buffer.fill_count(); | ||||
|     if(available_bytes < sizeof(float) * this->channel_count) return written; | ||||
|     auto available_samples = available_bytes / sizeof(float) / this->channel_count; | ||||
| 
 | ||||
|     if(this->buffering && available_samples < this->min_buffered_samples) return -2; | ||||
|     this->do_fade_in(); | ||||
|     this->do_fade_out(samples); /* will also call for underflow */ | ||||
|     //log_trace(category::audio, tr("Min: {}, Max: {}, Current: {}, Buffering: {} Required: {}, left: {}, will buffer in {}"), this->min_buffered_samples, this->max_buffered_samples, available_samples, this->buffering, samples, (int) available_samples - samples, this->will_buffer_in);
 | ||||
| 
 | ||||
|     if(this->will_buffer_in > 0) { | ||||
|         if(samples > (size_t) this->will_buffer_in) { | ||||
|             samples = this->will_buffer_in; | ||||
|             this->buffering = true; | ||||
|             this->fade_in_start = this->buffer.calculate_advanced_write_ptr(samples * sizeof(float) * this->channel_count); | ||||
|             this->will_buffer_in = -1; | ||||
|             log_trace(category::audio, tr("Start buffering due to fade out. Fade in ptr {}"), (void*) this->fade_in_start); | ||||
|         } else { | ||||
|             this->will_buffer_in -= samples; | ||||
|         } | ||||
|     } else { | ||||
|         this->buffering = false; | ||||
|     } | ||||
| 
 | ||||
|     if(available_samples >= samples - written) { | ||||
|         const auto byte_length = (samples - written) * sizeof(float) * this->channel_count; | ||||
|         if(buffer)memcpy((char*) buffer + written_bytes, this->buffer.read_ptr(), byte_length); | ||||
|         this->buffer.advance_read_ptr(byte_length); | ||||
| 
 | ||||
|         if(this->on_read) | ||||
|             this->on_read(); | ||||
| 
 | ||||
|         return samples; | ||||
|     } else { | ||||
|         const auto byte_length = available_samples * sizeof(float) * this->channel_count; | ||||
|         if(buffer) memcpy((char*) buffer + written_bytes, this->buffer.read_ptr(), byte_length); | ||||
|         this->buffer.advance_read_ptr(byte_length); | ||||
|         written += available_samples; | ||||
|         written_bytes += byte_length; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     if(auto fn = this->on_underflow; fn) | ||||
|         if(fn(samples - written)) | ||||
|             goto load_buffer; | ||||
| 
 | ||||
|     if(buffer) | ||||
|         memset((char*) buffer + written_bytes, 0, (samples - written) * sizeof(float) * this->channel_count); | ||||
| 
 | ||||
|     this->buffering = true; | ||||
|     this->fade_in_start = this->buffer.write_ptr(); | ||||
|     log_trace(category::audio, tr("Start buffering due to underflow."), (void*) this->fade_in_start); | ||||
|     this->will_buffer_in = -1; | ||||
| 
 | ||||
| 	if(this->on_read) | ||||
| 		this->on_read(); | ||||
| 	return written; /* return the written samples */ | ||||
| } | ||||
| 
 | ||||
| ssize_t AudioOutputSource::enqueue_silence(size_t samples) { | ||||
|     size_t enqueued{0}; | ||||
| 
 | ||||
|     auto free_bytes = this->buffer.free_count(); | ||||
|     auto free_samples = free_bytes / sizeof(float) / this->channel_count; | ||||
|     if(this->max_buffered_samples && free_samples > this->max_buffered_samples) free_samples = this->max_buffered_samples; | ||||
| 
 | ||||
|     if(free_samples >= samples) { | ||||
|         const auto byte_length = samples * sizeof(float) * this->channel_count; | ||||
|         memset(this->buffer.write_ptr(), 0, byte_length); | ||||
|         this->buffer.advance_write_ptr(byte_length); | ||||
|         return samples; | ||||
|     } else { | ||||
|         const auto byte_length = free_samples * sizeof(float) * this->channel_count; | ||||
|         memset(this->buffer.write_ptr(), 0, byte_length); | ||||
|         this->buffer.advance_write_ptr(byte_length); | ||||
|         enqueued += free_samples; | ||||
|     } | ||||
| 
 | ||||
|     if(auto fn = this->on_overflow; fn) | ||||
|         fn(samples - enqueued); | ||||
| 
 | ||||
|     switch (this->overflow_strategy) { | ||||
|         case overflow_strategy::discard_input: | ||||
|             return -2; | ||||
|         case overflow_strategy::discard_buffer_all: | ||||
|             this->buffer.clear(); | ||||
|             break; | ||||
|         case overflow_strategy::discard_buffer_half: | ||||
|             this->buffer.advance_read_ptr(this->buffer.fill_count() / 2); | ||||
|             break; | ||||
|         case overflow_strategy::ignore: | ||||
|             break; | ||||
|     } | ||||
| 
 | ||||
|     this->fade_in_start = this->buffer.write_ptr(); /* so we fade in from silence */ | ||||
|     return enqueued; | ||||
| } | ||||
| 
 | ||||
| ssize_t AudioOutputSource::enqueue_samples(const void *buffer, size_t samples) { | ||||
|     size_t enqueued{0}; | ||||
| 
 | ||||
|     auto free_bytes = this->buffer.free_count(); | ||||
|     auto free_samples = free_bytes / sizeof(float) / this->channel_count; | ||||
|     if(this->max_buffered_samples && free_samples > this->max_buffered_samples) free_samples = this->max_buffered_samples; | ||||
| 
 | ||||
|     if(free_samples >= samples) { | ||||
|         const auto byte_length = samples * sizeof(float) * this->channel_count; | ||||
|         memcpy(this->buffer.write_ptr(), buffer, byte_length); | ||||
|         this->buffer.advance_write_ptr(byte_length); | ||||
|         return samples; | ||||
|     } else { | ||||
|         const auto byte_length = free_samples * sizeof(float) * this->channel_count; | ||||
|         memcpy(this->buffer.write_ptr(), buffer, byte_length); | ||||
|         this->buffer.advance_write_ptr(byte_length); | ||||
|         enqueued += free_samples; | ||||
|     } | ||||
| 
 | ||||
|     if(auto fn = this->on_overflow; fn) | ||||
|         fn(samples - enqueued); | ||||
| 
 | ||||
|     switch (this->overflow_strategy) { | ||||
|         case overflow_strategy::discard_input: | ||||
|             return -2; | ||||
|         case overflow_strategy::discard_buffer_all: | ||||
|             this->buffer.clear(); | ||||
|             break; | ||||
|         case overflow_strategy::discard_buffer_half: | ||||
|             this->buffer.advance_read_ptr(this->buffer.fill_count() / 2); | ||||
|             break; | ||||
|         case overflow_strategy::ignore: | ||||
|             break; | ||||
|     } | ||||
| 
 | ||||
| 	return enqueued; | ||||
| } | ||||
| 
 | ||||
| ssize_t AudioOutputSource::enqueue_samples_no_interleave(const void *buffer, size_t samples) { | ||||
|     auto free_bytes = this->buffer.free_count(); | ||||
|     auto free_samples = free_bytes / sizeof(float) / this->channel_count; | ||||
|     if(this->max_buffered_samples && free_samples > this->max_buffered_samples) free_samples = this->max_buffered_samples; | ||||
| 
 | ||||
|     auto samples_to_write{samples}; | ||||
|     if(samples_to_write > free_samples) samples_to_write = free_samples; | ||||
|     const auto enqueued{samples_to_write}; | ||||
|     { //FIXME: This only works for two channels!
 | ||||
|         auto src_buffer = (const float*) buffer; | ||||
|         auto target_buffer = (float*) this->buffer.write_ptr(); | ||||
| 
 | ||||
|         while (samples_to_write-- > 0) { | ||||
|             *target_buffer = *src_buffer; | ||||
|             *(target_buffer + 1) = *(src_buffer + samples); | ||||
| 
 | ||||
|             target_buffer += 2; | ||||
|             src_buffer++; | ||||
|         for(int channel{0}; channel < this->channel_count; channel++) { | ||||
|             *write_ptr++ *= volume; | ||||
|         } | ||||
|     } | ||||
|     this->buffer.advance_write_ptr(enqueued * this->channel_count * sizeof(float)); | ||||
|     if(enqueued == samples) return enqueued; | ||||
| 
 | ||||
|     if(auto fn = this->on_overflow; fn) | ||||
|         fn(samples - enqueued); | ||||
| 
 | ||||
|     switch (this->overflow_strategy) { | ||||
|         case overflow_strategy::discard_input: | ||||
|             return -2; | ||||
|         case overflow_strategy::discard_buffer_all: | ||||
|             this->buffer.clear(); | ||||
|             break; | ||||
|         case overflow_strategy::discard_buffer_half: | ||||
|             this->buffer.advance_read_ptr(this->buffer.fill_count() / 2); | ||||
|             break; | ||||
|         case overflow_strategy::ignore: | ||||
|             break; | ||||
|     } | ||||
| 
 | ||||
|     return enqueued; | ||||
|     this->fadeout_samples_left = fade_samples; | ||||
| } | ||||
| 
 | ||||
| AudioOutput::AudioOutput(size_t channels, size_t rate) : _channel_count(channels), _sample_rate(rate) { } | ||||
| void AudioOutputSource::apply_fadein() { | ||||
|     assert(this->currently_buffered_samples() >= this->fadeout_samples_left); | ||||
|     const auto samples_available = this->currently_buffered_samples(); | ||||
|     auto fade_samples = std::min(samples_available - this->fadeout_samples_left, this->fadein_frame_samples_); | ||||
|     if(!fade_samples) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     /*
 | ||||
|      * Note: We're using the read_ptr() here in order to correctly apply the effect. | ||||
|      *       This isn't really best practice but works. | ||||
|      */ | ||||
|     auto write_ptr = (float*) this->buffer.read_ptr() + this->fadeout_samples_left * this->channel_count; | ||||
|     for(size_t index{0}; index < fade_samples; index++) { | ||||
|         const auto offset = (float) ((float) (index + 1) / (float) fade_samples); | ||||
|         const auto volume = std::min(log10f(1 - offset) / -2.71828182845904f, 1.f); | ||||
| 
 | ||||
|         for(int channel{0}; channel < this->channel_count; channel++) { | ||||
|             *write_ptr++ *= volume; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| bool AudioOutputSource::pop_samples(void *target_buffer, size_t target_sample_count) { | ||||
|     std::unique_lock buffer_lock{this->buffer_mutex}; | ||||
|     auto result = this->pop_samples_(target_buffer, target_sample_count); | ||||
|     buffer_lock.unlock(); | ||||
| 
 | ||||
|     if(auto callback{this->on_read}; callback) { | ||||
|         callback(); | ||||
|     } | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| bool AudioOutputSource::pop_samples_(void *target_buffer, size_t target_sample_count) { | ||||
|     switch(this->buffer_state) { | ||||
|         case buffer_state::fadeout: { | ||||
|             /* Write as much we can */ | ||||
|             const auto write_samples = std::min(this->fadeout_samples_left, target_sample_count); | ||||
|             const auto write_byte_size = write_samples * this->channel_count * sizeof(float); | ||||
|             memcpy(target_buffer, this->buffer.read_ptr(), write_byte_size); | ||||
|             this->buffer.advance_read_ptr(write_byte_size); | ||||
| 
 | ||||
|             /* Fill the rest with silence */ | ||||
|             const auto empty_samples = target_sample_count - write_samples; | ||||
|             const auto empty_byte_size = empty_samples * this->channel_count * sizeof(float); | ||||
|             memset((char*) target_buffer + write_byte_size, 0, empty_byte_size); | ||||
| 
 | ||||
|             this->fadeout_samples_left -= write_samples; | ||||
|             if(!this->fadeout_samples_left) { | ||||
|                 log_trace(category::audio, tr("{} Successfully replayed fadeout sequence."), (void*) this); | ||||
|                 this->buffer_state = buffer_state::buffering; | ||||
|             } | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         case buffer_state::playing: { | ||||
|             const auto buffered_samples = this->currently_buffered_samples(); | ||||
|             if(buffered_samples < target_sample_count + this->fadeout_frame_samples_) { | ||||
|                 const auto missing_samples = target_sample_count + this->fadeout_frame_samples_ - buffered_samples; | ||||
|                 if(auto callback{this->on_underflow}; callback) { | ||||
|                     if(callback(missing_samples)) { | ||||
|                         /* We've been filled up again. Trying again to fill the output buffer. */ | ||||
|                         return this->pop_samples(target_buffer, target_sample_count); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 /*
 | ||||
|                  * When consuming target_sample_count amount samples of our buffer we could not | ||||
|                  * apply the fadeout effect any more. Instead we're applying it now and returning to buffering state. | ||||
|                  */ | ||||
|                 this->apply_fadeout(); | ||||
| 
 | ||||
|                 /* Write the rest of unmodified buffer */ | ||||
|                 const auto write_samples = buffered_samples - this->fadeout_samples_left; | ||||
|                 assert(write_samples <= target_sample_count); | ||||
|                 const auto write_byte_size = write_samples * this->channel_count * sizeof(float); | ||||
|                 memcpy(target_buffer, this->buffer.read_ptr(), write_byte_size); | ||||
|                 this->buffer.advance_read_ptr(write_byte_size); | ||||
| 
 | ||||
|                 log_trace(category::audio, tr("{} Starting stream fadeout. Requested samples {}, Buffered samples: {}, Fadeout frame samples: {}, Returned normal samples: {}"), | ||||
|                           (void*) this, target_sample_count, buffered_samples, this->fadeout_frame_samples_, write_samples | ||||
|                 ); | ||||
| 
 | ||||
|                 this->buffer_state = buffer_state::fadeout; | ||||
|                 if(write_samples < target_sample_count) { | ||||
|                     /* Fill the rest of the buffer with the fadeout content */ | ||||
|                     this->pop_samples((char*) target_buffer + write_byte_size, target_sample_count - write_samples); | ||||
|                 } | ||||
|             } else { | ||||
|                 /* We can just normally copy the buffer */ | ||||
|                 const auto write_byte_size = target_sample_count * this->channel_count * sizeof(float); | ||||
|                 memcpy(target_buffer, this->buffer.read_ptr(), write_byte_size); | ||||
|                 this->buffer.advance_read_ptr(write_byte_size); | ||||
|             } | ||||
| 
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         case buffer_state::buffering: | ||||
|             /* Nothing to replay */ | ||||
|             return false; | ||||
| 
 | ||||
|         default: | ||||
|             assert(false); | ||||
|             return false; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ssize_t AudioOutputSource::enqueue_samples(const void *source_buffer, size_t sample_count) { | ||||
|     std::lock_guard buffer_lock{this->buffer_mutex}; | ||||
|     return this->enqueue_samples_(source_buffer, sample_count); | ||||
| } | ||||
| 
 | ||||
| ssize_t AudioOutputSource::enqueue_samples_(const void *source_buffer, size_t sample_count) { | ||||
|     switch(this->buffer_state) { | ||||
|         case buffer_state::fadeout: | ||||
|         case buffer_state::buffering: { | ||||
|             assert(this->currently_buffered_samples() >= this->fadeout_samples_left); | ||||
|             assert(this->min_buffered_samples_ >= this->currently_buffered_samples() - this->fadeout_samples_left); | ||||
|             const auto missing_samples = this->min_buffered_samples_ - (this->currently_buffered_samples() - this->fadeout_samples_left); | ||||
|             const auto write_sample_count = std::min(missing_samples, sample_count); | ||||
|             const auto write_byte_size = write_sample_count * this->channel_count * sizeof(float); | ||||
| 
 | ||||
|             assert(write_sample_count <= this->max_supported_buffering()); | ||||
|             memcpy(this->buffer.write_ptr(), source_buffer, write_byte_size); | ||||
|             this->buffer.advance_write_ptr(write_byte_size); | ||||
| 
 | ||||
|             if(sample_count < missing_samples) { | ||||
|                 /* we still need to buffer */ | ||||
|                 return sample_count; | ||||
|             } | ||||
| 
 | ||||
|             /*
 | ||||
|              * Even though we still have fadeout samples left we don't declare them as such since we've already fulled | ||||
|              * our future buffer. | ||||
|              */ | ||||
|             this->fadeout_samples_left = 0; | ||||
| 
 | ||||
|             /* buffering finished */ | ||||
|             log_trace(category::audio, tr("{} Finished buffering {} samples. Fading them in."), (void*) this, this->min_buffered_samples_); | ||||
|             this->apply_fadein(); | ||||
|             this->buffer_state = buffer_state::playing; | ||||
|             if(sample_count > missing_samples) { | ||||
|                 /* we've more data to write */ | ||||
|                 return this->enqueue_samples((const char*) source_buffer + write_byte_size, sample_count - missing_samples) + write_sample_count; | ||||
|             } else { | ||||
|                 return write_sample_count; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         case buffer_state::playing: { | ||||
|             const auto buffered_samples = this->currently_buffered_samples(); | ||||
| 
 | ||||
|             const auto write_sample_count = std::min(this->max_supported_buffering() - buffered_samples, sample_count); | ||||
|             const auto write_byte_size = write_sample_count * this->channel_count * sizeof(float); | ||||
| 
 | ||||
|             memcpy(this->buffer.write_ptr(), source_buffer, write_byte_size); | ||||
|             this->buffer.advance_write_ptr(write_byte_size); | ||||
| 
 | ||||
|             if(write_sample_count < sample_count) { | ||||
|                 if(auto callback{this->on_overflow}; callback) { | ||||
|                     callback(sample_count - write_sample_count); | ||||
|                 } | ||||
| 
 | ||||
|                 switch (this->overflow_strategy) { | ||||
|                     case overflow_strategy::discard_input: | ||||
|                         return -2; | ||||
| 
 | ||||
|                     case overflow_strategy::discard_buffer_all: | ||||
|                         this->buffer.clear(); | ||||
|                         break; | ||||
| 
 | ||||
|                     case overflow_strategy::discard_buffer_half: | ||||
|                         /* FIXME: This implementation is wrong! */ | ||||
|                         this->buffer.advance_read_ptr(this->buffer.fill_count() / 2); | ||||
|                         break; | ||||
| 
 | ||||
|                     case overflow_strategy::ignore: | ||||
|                         break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return write_sample_count; | ||||
|         } | ||||
| 
 | ||||
|         default: | ||||
|             assert(false); | ||||
|             return false; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| constexpr static auto kMaxStackBuffer{1024 * 8 * sizeof(float)}; | ||||
| ssize_t AudioOutputSource::enqueue_samples_no_interleave(const void *source_buffer, size_t samples) { | ||||
|     if(this->channel_count == 1) { | ||||
|         return this->enqueue_samples(source_buffer, samples); | ||||
|     } else if(this->channel_count == 2) { | ||||
|         const auto buffer_byte_size = samples * this->channel_count * sizeof(float); | ||||
|         if(buffer_byte_size > kMaxStackBuffer) { | ||||
|             /* We can't convert to interleave */ | ||||
|             return 0; | ||||
|         } | ||||
| 
 | ||||
|         uint8_t stack_buffer[kMaxStackBuffer]; | ||||
|         { | ||||
|             auto src_buffer = (const float*) source_buffer; | ||||
|             auto target_buffer = (float*) stack_buffer; | ||||
| 
 | ||||
|             auto samples_to_write = samples; | ||||
|             while (samples_to_write-- > 0) { | ||||
|                 *target_buffer = *src_buffer; | ||||
|                 *(target_buffer + 1) = *(src_buffer + samples); | ||||
| 
 | ||||
|                 target_buffer += 2; | ||||
|                 src_buffer++; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return this->enqueue_samples(stack_buffer, samples); | ||||
|     } else { | ||||
|         /* TODO: Generalize to interleave algo */ | ||||
|         return 0; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| bool AudioOutputSource::set_max_buffered_samples(size_t samples) { | ||||
|     samples = std::max(samples, (size_t) this->fadein_frame_samples_); | ||||
|     if(samples > this->max_supported_buffering()) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     std::lock_guard buffer_lock{this->buffer_mutex}; | ||||
|     if(samples < this->min_buffered_samples_) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     this->max_buffered_samples_ = samples; | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| bool AudioOutputSource::set_min_buffered_samples(size_t samples) { | ||||
|     samples = std::max(samples, (size_t) this->fadein_frame_samples_); | ||||
| 
 | ||||
|     std::lock_guard buffer_lock{this->buffer_mutex}; | ||||
|     if(samples > this->max_buffered_samples_) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     this->min_buffered_samples_ = samples; | ||||
|     switch(this->buffer_state) { | ||||
|         case buffer_state::fadeout: | ||||
|         case buffer_state::buffering: { | ||||
|             assert(this->currently_buffered_samples() >= this->fadeout_samples_left); | ||||
|             const auto buffered_samples = this->currently_buffered_samples() - this->fadeout_samples_left; | ||||
|             if(buffered_samples > this->min_buffered_samples_) { | ||||
|                 log_trace(category::audio, tr("{} Finished buffering {} samples (due to min buffered sample reduce). Fading them in."), (void*) this, this->min_buffered_samples_); | ||||
|                 this->apply_fadein(); | ||||
|                 this->buffer_state = buffer_state::playing; | ||||
|             } | ||||
| 
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         case buffer_state::playing: | ||||
|             return true; | ||||
| 
 | ||||
|         default: | ||||
|             assert(false); | ||||
|             return false; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| AudioOutput::AudioOutput(size_t channels, size_t rate) : channel_count_(channels), sample_rate_(rate) { } | ||||
| 
 | ||||
| AudioOutput::~AudioOutput() { | ||||
| 	this->close_device(); | ||||
| @ -255,25 +322,14 @@ AudioOutput::~AudioOutput() { | ||||
| } | ||||
| 
 | ||||
| std::shared_ptr<AudioOutputSource> AudioOutput::create_source(ssize_t buf) { | ||||
| 	auto result = shared_ptr<AudioOutputSource>(new AudioOutputSource(this, this->_channel_count, this->_sample_rate, buf)); | ||||
| 	auto result = std::shared_ptr<AudioOutputSource>(new AudioOutputSource(this->channel_count_, this->sample_rate_, buf)); | ||||
| 	{ | ||||
| 		lock_guard lock(this->sources_lock); | ||||
| 		this->_sources.push_back(result); | ||||
|         std::lock_guard source_lock{this->sources_mutex}; | ||||
| 		this->sources_.push_back(result); | ||||
| 	} | ||||
| 	return result; | ||||
| } | ||||
| 
 | ||||
| void AudioOutput::delete_source(const std::shared_ptr<tc::audio::AudioOutputSource> &source) { | ||||
| 	{ | ||||
| 		lock_guard lock(this->sources_lock); | ||||
| 		auto it = find(this->_sources.begin(), this->_sources.end(), source); | ||||
| 		if(it != this->_sources.end()) | ||||
| 			this->_sources.erase(it); | ||||
| 	} | ||||
| 
 | ||||
| 	source->handle = nullptr; | ||||
| } | ||||
| 
 | ||||
| void AudioOutput::cleanup_buffers() { | ||||
|     free(this->source_buffer); | ||||
|     free(this->source_merge_buffer); | ||||
| @ -290,50 +346,67 @@ void AudioOutput::cleanup_buffers() { | ||||
| } | ||||
| 
 | ||||
| void AudioOutput::fill_buffer(void *output, size_t out_frame_count, size_t out_channels) { | ||||
|     if(out_channels != this->_channel_count) { | ||||
|         log_critical(category::audio, tr("Channel count miss match (output)! Expected: {} Received: {}. Fixme!"), this->_channel_count, out_channels); | ||||
|     if(out_channels != this->channel_count_) { | ||||
|         log_critical(category::audio, tr("Channel count miss match (output)! Expected: {} Received: {}. Fixme!"), this->channel_count_, out_channels); | ||||
|         return; | ||||
|     } | ||||
|     auto local_frame_count = this->_resampler ? this->_resampler->input_size(out_frame_count) : out_frame_count; | ||||
|     auto local_frame_count = this->resampler_ ? this->resampler_->input_size(out_frame_count) : out_frame_count; | ||||
|     void* const original_output{output}; | ||||
| 
 | ||||
|     if(this->resample_overhead_samples > 0) { | ||||
|         const auto samples_to_write = this->resample_overhead_samples > out_frame_count ? out_frame_count : this->resample_overhead_samples; | ||||
|         const auto byte_length = samples_to_write * sizeof(float) * out_channels; | ||||
| 
 | ||||
|         if(output) memcpy(output, this->resample_overhead_buffer, byte_length); | ||||
|         if(output) { | ||||
|             memcpy(output, this->resample_overhead_buffer, byte_length); | ||||
|         } | ||||
| 
 | ||||
|         if(samples_to_write == out_frame_count) { | ||||
|             this->resample_overhead_samples -= samples_to_write; | ||||
|             memcpy(this->resample_overhead_buffer, (char*) this->resample_overhead_buffer + byte_length, this->resample_overhead_samples * this->_channel_count * sizeof(float)); | ||||
|             memcpy(this->resample_overhead_buffer, (char*) this->resample_overhead_buffer + byte_length, this->resample_overhead_samples * this->channel_count_ * sizeof(float)); | ||||
|             return; | ||||
|         } else { | ||||
|             this->resample_overhead_samples = 0; | ||||
|             output = (char*) output + byte_length; | ||||
|             out_frame_count -= samples_to_write; | ||||
|             local_frame_count -= this->_resampler ? this->_resampler->input_size(samples_to_write) : samples_to_write; | ||||
|             local_frame_count -= this->resampler_ ? this->resampler_->input_size(samples_to_write) : samples_to_write; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if(!original_output) { | ||||
|         for(auto& source : this->_sources) | ||||
|         this->sources_.erase(std::remove_if(this->sources_.begin(), this->sources_.end(), [&](const std::weak_ptr<AudioOutputSource>& weak_source) { | ||||
|             auto source = weak_source.lock(); | ||||
|             if(!source) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             source->pop_samples(nullptr, local_frame_count); | ||||
|             return false; | ||||
|         }), this->sources_.end()); | ||||
|         return; | ||||
|     } else if(this->_volume <= 0) { | ||||
|         for(auto& source : this->_sources) | ||||
|     } else if(this->volume_modifier <= 0) { | ||||
|         this->sources_.erase(std::remove_if(this->sources_.begin(), this->sources_.end(), [&](const std::weak_ptr<AudioOutputSource>& weak_source) { | ||||
|             auto source = weak_source.lock(); | ||||
|             if(!source) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             source->pop_samples(nullptr, local_frame_count); | ||||
|             return false; | ||||
|         }), this->sources_.end()); | ||||
| 
 | ||||
|         memset(output, 0, local_frame_count * out_channels * sizeof(float)); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
| 	const size_t local_buffer_length = local_frame_count * 4 * this->_channel_count; | ||||
|     const size_t out_buffer_length = out_frame_count * 4 * this->_channel_count; | ||||
| 	const size_t local_buffer_length = local_frame_count * 4 * this->channel_count_; | ||||
|     const size_t out_buffer_length = out_frame_count * 4 * this->channel_count_; | ||||
| 	size_t sources = 0; | ||||
| 	size_t actual_sources = 0; | ||||
| 	size_t actual_sources; | ||||
| 
 | ||||
| 	{ | ||||
| 		lock_guard lock(this->sources_lock); | ||||
| 		sources = this->_sources.size(); | ||||
| 		actual_sources = sources; | ||||
| 		lock_guard sources_lock{this->sources_mutex}; | ||||
| 		sources = this->sources_.size(); | ||||
| 
 | ||||
| 		if(sources > 0) { | ||||
| 			 /* allocate the required space */ | ||||
| @ -343,59 +416,69 @@ void AudioOutput::fill_buffer(void *output, size_t out_frame_count, size_t out_c | ||||
|             { | ||||
| 
 | ||||
|                 if(this->source_buffer_length < required_source_buffer_length || !this->source_buffer) { | ||||
|                     if(this->source_buffer) | ||||
|                     if(this->source_buffer) { | ||||
|                         free(this->source_buffer); | ||||
|                     } | ||||
| 
 | ||||
|                     this->source_buffer = malloc(required_source_buffer_length); | ||||
|                     this->source_buffer_length = required_source_buffer_length; | ||||
|                 } | ||||
|                 if(this->source_merge_buffer_length < required_source_merge_buffer_length || !this->source_merge_buffer) { | ||||
|                     if (this->source_merge_buffer) | ||||
|                     if (this->source_merge_buffer) { | ||||
|                         free(this->source_merge_buffer); | ||||
|                     } | ||||
| 
 | ||||
|                     this->source_merge_buffer = (void **) malloc(required_source_merge_buffer_length); | ||||
|                     this->source_merge_buffer_length = required_source_merge_buffer_length; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
| 			for(size_t index = 0; index < sources; index++) { | ||||
| 				auto& source = this->_sources[index]; | ||||
|             size_t index{0}; | ||||
|             this->sources_.erase(std::remove_if(this->sources_.begin(), this->sources_.end(), [&](const std::weak_ptr<AudioOutputSource>& weak_source) { | ||||
|                 auto source = weak_source.lock(); | ||||
|                 if(!source) { | ||||
|                     return true; | ||||
|                 } | ||||
| 
 | ||||
|                 this->source_merge_buffer[index] = (char*) this->source_buffer + (local_buffer_length * index); | ||||
|                 auto written_frames = this->_sources[index]->pop_samples(this->source_merge_buffer[index], local_frame_count); | ||||
|                 if(written_frames != local_frame_count) { | ||||
|                     if(written_frames <= 0) { | ||||
|                         this->source_merge_buffer[index] = nullptr; | ||||
|                         actual_sources--; | ||||
|                     } else { | ||||
|                         /* fill up the rest with silence (0) */ | ||||
|                         auto written = written_frames * this->_channel_count * 4; | ||||
|                         memset((char*) this->source_merge_buffer[index] + written, 0, (local_frame_count - written_frames) * this->_channel_count * 4); | ||||
|                     } | ||||
|                 if(!source->pop_samples(this->source_merge_buffer[index], local_frame_count)) { | ||||
|                     this->source_merge_buffer[index] = nullptr; | ||||
|                     return false; | ||||
|                 } | ||||
| 			} | ||||
| 		} else | ||||
| 		    goto clear_buffer_exit; | ||||
| 
 | ||||
|                 index++; | ||||
|                 return false; | ||||
|             }), this->sources_.end()); | ||||
|             actual_sources = index; | ||||
| 		} else { | ||||
|             goto clear_buffer_exit; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if(actual_sources > 0) { | ||||
| 	    if(local_frame_count == out_frame_count) { | ||||
|             if(!merge::merge_n_sources(output, this->source_merge_buffer, sources, this->_channel_count, local_frame_count)) | ||||
| 	        /* Output */ | ||||
|             if(!merge::merge_n_sources(output, this->source_merge_buffer, sources, this->channel_count_, local_frame_count)) { | ||||
|                 log_warn(category::audio, tr("failed to merge buffers!")); | ||||
|             } | ||||
| 	    } else { | ||||
|             if(!merge::merge_n_sources(this->source_buffer, this->source_merge_buffer, sources, this->_channel_count, local_frame_count)) | ||||
|             if(!merge::merge_n_sources(this->source_buffer, this->source_merge_buffer, sources, this->channel_count_, local_frame_count)) { | ||||
|                 log_warn(category::audio, tr("failed to merge buffers!")); | ||||
|             } | ||||
| 
 | ||||
|             /* this->source_buffer could hold the amount of resampled data (checked above) */ | ||||
|             auto resampled_samples = this->_resampler->process(this->source_buffer, this->source_buffer, local_frame_count); | ||||
|             auto resampled_samples = this->resampler_->process(this->source_buffer, this->source_buffer, local_frame_count); | ||||
|             if(resampled_samples <= 0) { | ||||
|                 log_warn(category::audio, tr("Failed to resample audio data for client ({})")); | ||||
|                 goto clear_buffer_exit; | ||||
|             } | ||||
| 
 | ||||
|             if(resampled_samples != out_frame_count) { | ||||
|                 if((size_t) resampled_samples > out_frame_count) { | ||||
|                     const auto diff_length = resampled_samples - out_frame_count; | ||||
|                     log_warn(category::audio, tr("enqueuing {} samples"), diff_length); | ||||
|                     const auto overhead_buffer_offset = this->resample_overhead_samples * sizeof(float) * this->_channel_count; | ||||
|                     const auto diff_byte_length = diff_length * sizeof(float) * this->_channel_count; | ||||
|                     const auto overhead_buffer_offset = this->resample_overhead_samples * sizeof(float) * this->channel_count_; | ||||
|                     const auto diff_byte_length = diff_length * sizeof(float) * this->channel_count_; | ||||
| 
 | ||||
|                     if(this->resample_overhead_buffer_length < diff_byte_length + overhead_buffer_offset) { | ||||
|                         this->resample_overhead_buffer_length = diff_byte_length + overhead_buffer_offset; | ||||
| @ -407,22 +490,23 @@ void AudioOutput::fill_buffer(void *output, size_t out_frame_count, size_t out_c | ||||
|                     } | ||||
|                     memcpy( | ||||
|                             (char*) this->resample_overhead_buffer + overhead_buffer_offset, | ||||
|                             (char*) this->source_buffer + out_frame_count * sizeof(float) * this->_channel_count, | ||||
|                             (char*) this->source_buffer + out_frame_count * sizeof(float) * this->channel_count_, | ||||
|                             diff_byte_length | ||||
|                     ); | ||||
|                     this->resample_overhead_samples += diff_length; | ||||
|                 } else { | ||||
|                     log_warn(category::audio, tr("Resampled samples does not match requested sampeles: {} <> {}. Sampled from {} to {}"), resampled_samples, out_frame_count, this->_resampler->input_rate(), this->_resampler->output_rate()); | ||||
|                     log_warn(category::audio, tr("Resampled samples does not match requested sampeles: {} <> {}. Sampled from {} to {}"), resampled_samples, out_frame_count, this->resampler_->input_rate(), this->resampler_->output_rate()); | ||||
|                 } | ||||
|             } | ||||
|             memcpy(output, this->source_buffer, out_frame_count * sizeof(float) * this->_channel_count); | ||||
| 
 | ||||
|             memcpy(output, this->source_buffer, out_frame_count * sizeof(float) * this->channel_count_); | ||||
| 	    } | ||||
| 
 | ||||
| 	    /* lets apply the volume */ | ||||
|         audio::apply_gain(output, this->_channel_count, out_frame_count, this->_volume); | ||||
|         audio::apply_gain(output, this->channel_count_, out_frame_count, this->volume_modifier); | ||||
| 	} else { | ||||
|         clear_buffer_exit: | ||||
| 		memset(output, 0, this->_channel_count * sizeof(float) * out_frame_count); | ||||
| 		memset(output, 0, this->channel_count_ * sizeof(float) * out_frame_count); | ||||
| 		return; | ||||
| 	} | ||||
| } | ||||
| @ -437,13 +521,13 @@ void AudioOutput::set_device(const std::shared_ptr<AudioDevice> &new_device) { | ||||
| 
 | ||||
| void AudioOutput::close_device() { | ||||
|     lock_guard lock(this->device_lock); | ||||
|     if(this->_playback) { | ||||
|         this->_playback->remove_source(this); | ||||
|         this->_playback->stop_if_possible(); | ||||
|         this->_playback.reset(); | ||||
|     if(this->playback_) { | ||||
|         this->playback_->remove_source(this); | ||||
|         this->playback_->stop_if_possible(); | ||||
|         this->playback_.reset(); | ||||
|     } | ||||
| 
 | ||||
|     this->_resampler = nullptr; | ||||
|     this->resampler_ = nullptr; | ||||
|     this->device = nullptr; | ||||
| } | ||||
| 
 | ||||
| @ -453,23 +537,23 @@ bool AudioOutput::playback(std::string& error) { | ||||
| 	    error = "invalid device handle"; | ||||
|         return false; | ||||
| 	} | ||||
|     if(this->_playback) return true; | ||||
|     if(this->playback_) return true; | ||||
| 
 | ||||
| 	this->_playback = this->device->playback(); | ||||
| 	if(!this->_playback) { | ||||
| 	this->playback_ = this->device->playback(); | ||||
| 	if(!this->playback_) { | ||||
| 	    error = "failed to allocate memory"; | ||||
|         return false; | ||||
| 	} | ||||
| 
 | ||||
|     if(this->_playback->sample_rate() != this->sample_rate()) { | ||||
|         this->_resampler = std::make_unique<AudioResampler>(this->sample_rate(), this->_playback->sample_rate(), this->channel_count()); | ||||
|         if(!this->_resampler->valid()) { | ||||
|     if(this->playback_->sample_rate() != this->sample_rate()) { | ||||
|         this->resampler_ = std::make_unique<AudioResampler>(this->sample_rate(), this->playback_->sample_rate(), this->channel_count()); | ||||
|         if(!this->resampler_->valid()) { | ||||
|             error = "failed to allocate a resampler"; | ||||
|             this->_playback = nullptr; | ||||
|             this->playback_ = nullptr; | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 	this->_playback->register_source(this); | ||||
|     return this->_playback->start(error); | ||||
| 	this->playback_->register_source(this); | ||||
|     return this->playback_->start(error); | ||||
| } | ||||
| @ -25,61 +25,96 @@ namespace tc::audio { | ||||
| 				discard_input | ||||
| 			}; | ||||
| 		} | ||||
| 
 | ||||
|         class AudioOutputSource { | ||||
| 			friend class AudioOutput; | ||||
| 			public: | ||||
| 				AudioOutput* handle; | ||||
| 				size_t const channel_count; | ||||
| 				size_t const sample_rate; | ||||
| 
 | ||||
| 				size_t const channel_count = 0; | ||||
| 				size_t const sample_rate = 0; | ||||
| 
 | ||||
| 				[[nodiscard]] inline size_t max_supported_latency() const { | ||||
| 				/**
 | ||||
| 				 * The maximum amount of samples which could be buffered. | ||||
| 				 * @return | ||||
| 				 */ | ||||
| 				[[nodiscard]] inline size_t max_supported_buffering() const { | ||||
|                     return this->buffer.capacity() / this->channel_count / sizeof(float); | ||||
| 				} | ||||
| 
 | ||||
|                 [[nodiscard]] inline size_t max_latency() const { | ||||
| 				    const auto max_samples = this->max_supported_latency(); | ||||
| 				    if(this->max_buffered_samples && this->max_buffered_samples <= max_samples) return this->max_buffered_samples; | ||||
|                 [[nodiscard]] inline size_t max_buffering() const { | ||||
| 				    const auto max_samples = this->max_supported_buffering(); | ||||
| 				    if(this->max_buffered_samples_ && this->max_buffered_samples_ <= max_samples) { | ||||
|                         return this->max_buffered_samples_; | ||||
| 				    } | ||||
| 
 | ||||
|                     return max_samples; | ||||
|                 } | ||||
| 
 | ||||
|                 /* samples which needs to be played*/ | ||||
|                 [[nodiscard]] inline size_t current_latency() const { | ||||
|                 /**
 | ||||
|                  * Sample count which still need to be replayed before newly emplaced buffers will be played. | ||||
|                  * @return | ||||
|                  */ | ||||
|                 [[nodiscard]] inline size_t currently_buffered_samples() const { | ||||
| 				    return this->buffer.fill_count() / this->channel_count / sizeof(float); | ||||
| 				} | ||||
| 
 | ||||
| 				bool buffering{true}; | ||||
|                 char* fade_in_start{nullptr}; | ||||
|                 ssize_t will_buffer_in{-1}; | ||||
| 				size_t min_buffered_samples{0}; | ||||
| 				size_t max_buffered_samples{0}; | ||||
| 
 | ||||
| 				size_t fadeout_sample_length{360}; | ||||
| 				[[nodiscard]] inline size_t min_buffered_samples() const { return this->min_buffered_samples_; } | ||||
|                 [[nodiscard]] inline size_t max_buffered_samples() const { return this->max_buffered_samples_; } | ||||
| 
 | ||||
| 				overflow_strategy::value overflow_strategy = overflow_strategy::discard_buffer_half; | ||||
|                 bool set_min_buffered_samples(size_t /* target samples */); | ||||
|                 bool set_max_buffered_samples(size_t /* target samples */); | ||||
| 
 | ||||
| 				overflow_strategy::value overflow_strategy{overflow_strategy::discard_buffer_half}; | ||||
| 
 | ||||
| 				/* if it returns true then the it means that the buffer has been refilled, we have to test again */ | ||||
| 				std::function<bool(size_t /* sample count */)> on_underflow; | ||||
| 				std::function<void(size_t /* sample count */)> on_overflow; | ||||
| 				std::function<void()> on_read; /* will be invoked after sample read, e.g. for buffer fullup */ | ||||
| 
 | ||||
| 				void clear(); | ||||
| 				ssize_t pop_samples(void* /* output buffer */, size_t /* sample count */); | ||||
| 				ssize_t enqueue_silence(size_t /* sample count */); | ||||
|                 void clear(); | ||||
| 				ssize_t enqueue_samples(const void * /* input buffer */, size_t /* sample count */); | ||||
| 				ssize_t enqueue_samples_no_interleave(const void * /* input buffer */, size_t /* sample count */); | ||||
| 
 | ||||
| 				/* Consume N samples */ | ||||
|                 bool pop_samples(void* /* output buffer */, size_t /* sample count */); | ||||
| 			private: | ||||
| 				AudioOutputSource(AudioOutput* handle, size_t channel_count, size_t sample_rate, ssize_t max_buffer_sample_count = -1) : | ||||
| 				    handle(handle), channel_count(channel_count), sample_rate(sample_rate), | ||||
| 				    buffer{max_buffer_sample_count == -1 ? channel_count * sample_rate * sizeof(float) : max_buffer_sample_count * channel_count * sizeof(float)} { | ||||
| 		        enum struct buffer_state { | ||||
| 		            /* Awaiting enough samples to replay and apply the fadein effect */ | ||||
| 		            buffering, | ||||
| 		            /* We have encountered a buffer underflow. Applying fadeout effect and changing state to buffering. */ | ||||
| 		            fadeout, | ||||
| 		            /* We're just normally replaying audio */ | ||||
| 		            playing | ||||
| 		        }; | ||||
| 
 | ||||
| 				AudioOutputSource(size_t channel_count, size_t sample_rate, ssize_t max_buffer_sample_count = -1) : | ||||
| 				    channel_count{channel_count}, sample_rate{sample_rate}, | ||||
| 				    buffer{max_buffer_sample_count == -1 ? channel_count * sample_rate * sizeof(float) : max_buffer_sample_count * channel_count * sizeof(float)} | ||||
|                 { | ||||
| 					this->clear(); | ||||
| 				} | ||||
| 
 | ||||
| 				void do_fade_out(size_t /* pop count */); | ||||
| 				void do_fade_in(); | ||||
| 
 | ||||
| 				std::recursive_mutex buffer_mutex{}; | ||||
| 				enum buffer_state buffer_state{buffer_state::buffering}; | ||||
| 				tc::ring_buffer buffer; | ||||
| 
 | ||||
|                 size_t min_buffered_samples_{0}; | ||||
|                 size_t max_buffered_samples_{0}; | ||||
| 
 | ||||
| 				/*
 | ||||
| 				 * Fadeout and fadein properties. | ||||
| 				 * The fadeout sample count should always be lower than the fade in sample count. | ||||
| 				 */ | ||||
|                 size_t fadein_frame_samples_{960}; | ||||
| 				size_t fadeout_frame_samples_{(size_t) (960 * .9)}; | ||||
|                 size_t fadeout_samples_left{0}; | ||||
| 
 | ||||
| 				/* Methods bellow do not acquire the buffer_mutex mutex */ | ||||
|                 ssize_t enqueue_samples_(const void * /* input buffer */, size_t /* sample count */); | ||||
|                 bool pop_samples_(void* /* output buffer */, size_t /* sample count */); | ||||
| 
 | ||||
|                 void apply_fadeout(); | ||||
|                 void apply_fadein(); | ||||
| 		}; | ||||
| 
 | ||||
|         class AudioOutput : public AudioDevicePlayback::Source { | ||||
| @ -92,36 +127,35 @@ namespace tc::audio { | ||||
| 				void close_device(); | ||||
|                 std::shared_ptr<AudioDevice> current_device() { return this->device; } | ||||
| 
 | ||||
| 				std::deque<std::shared_ptr<AudioOutputSource>> sources() { | ||||
| 					std::lock_guard lock(this->sources_lock); | ||||
| 					return this->_sources; | ||||
| 				std::deque<std::weak_ptr<AudioOutputSource>> sources() { | ||||
| 					std::lock_guard sources_lock{this->sources_mutex}; | ||||
| 					return this->sources_; | ||||
| 				} | ||||
| 
 | ||||
| 				std::shared_ptr<AudioOutputSource> create_source(ssize_t /* buffer sample size */ = -1); | ||||
| 				void delete_source(const std::shared_ptr<AudioOutputSource>& /* source */); | ||||
| 
 | ||||
| 				inline size_t channel_count() { return this->_channel_count; } | ||||
| 				inline size_t sample_rate() { return this->_sample_rate; } | ||||
| 				[[nodiscard]] inline size_t channel_count() const { return this->channel_count_; } | ||||
| 				[[nodiscard]] inline size_t sample_rate() const { return this->sample_rate_; } | ||||
| 
 | ||||
| 				inline float volume() { return this->_volume; } | ||||
| 				inline void set_volume(float value) { this->_volume = value; } | ||||
| 				[[nodiscard]] inline float volume() const { return this->volume_modifier; } | ||||
| 				inline void set_volume(float value) { this->volume_modifier = value; } | ||||
| 			private: | ||||
| 				void fill_buffer(void *, size_t out_frame_count, size_t out_channels) override; | ||||
| 
 | ||||
| 				size_t const _channel_count; | ||||
| 				size_t const _sample_rate; | ||||
| 				size_t const channel_count_; | ||||
| 				size_t const sample_rate_; | ||||
| 
 | ||||
| 				std::mutex sources_lock; | ||||
| 				std::deque<std::shared_ptr<AudioOutputSource>> _sources; | ||||
| 				std::mutex sources_mutex{}; | ||||
| 				std::deque<std::weak_ptr<AudioOutputSource>> sources_{}; | ||||
| 
 | ||||
| 				std::recursive_mutex device_lock; | ||||
| 				std::recursive_mutex device_lock{}; | ||||
|                 std::shared_ptr<AudioDevice> device{nullptr}; | ||||
|                 std::shared_ptr<AudioDevicePlayback> _playback{nullptr}; | ||||
|                 std::unique_ptr<AudioResampler> _resampler{nullptr}; | ||||
|                 std::shared_ptr<AudioDevicePlayback> playback_{nullptr}; | ||||
|                 std::unique_ptr<AudioResampler> resampler_{nullptr}; | ||||
| 
 | ||||
|                 /* only access there buffers within the audio loop! */ | ||||
| 				void* source_buffer = nullptr; | ||||
| 				void** source_merge_buffer = nullptr; | ||||
| 				void* source_buffer{nullptr}; | ||||
| 				void** source_merge_buffer{nullptr}; | ||||
| 
 | ||||
|                 void* resample_overhead_buffer{nullptr}; | ||||
|                 size_t resample_overhead_buffer_length{0}; | ||||
| @ -131,6 +165,6 @@ namespace tc::audio { | ||||
| 				size_t source_merge_buffer_length = 0; | ||||
| 				void cleanup_buffers(); | ||||
| 
 | ||||
| 				float _volume = 1.f; | ||||
| 				float volume_modifier{1.f}; | ||||
| 		}; | ||||
| 	} | ||||
| @ -11,8 +11,8 @@ namespace tc::audio { | ||||
| 
 | ||||
|             void process(const void* /* source */, size_t /* samples */); | ||||
| 
 | ||||
|             inline size_t channels() { return this->_channels; } | ||||
|             inline size_t frame_size() { return this->_frame_size; } | ||||
|             inline size_t channels() const { return this->_channels; } | ||||
|             inline size_t frame_size() const { return this->_frame_size; } | ||||
| 
 | ||||
|             std::function<void(const void* /* buffer */)> on_frame; | ||||
|         private: | ||||
|  | ||||
| @ -16,16 +16,16 @@ namespace tc::audio { | ||||
| 				AudioResampler(size_t /* input rate */, size_t /* output rate */, size_t /* channels */); | ||||
| 				virtual ~AudioResampler(); | ||||
| 
 | ||||
|                 [[nodiscard]] inline size_t channels() { return this->_channels; } | ||||
|                 [[nodiscard]] inline size_t input_rate() { return this->_input_rate; } | ||||
|                 [[nodiscard]] inline size_t output_rate() { return this->_output_rate; } | ||||
|                 [[nodiscard]] inline size_t channels() const { return this->_channels; } | ||||
|                 [[nodiscard]] inline size_t input_rate() const { return this->_input_rate; } | ||||
|                 [[nodiscard]] inline size_t output_rate() const { return this->_output_rate; } | ||||
| 
 | ||||
|                 [[nodiscard]] inline long double io_ratio() { return (long double) this->_output_rate / (long double) this->_input_rate; } | ||||
|                 [[nodiscard]] inline long double io_ratio() const { return (long double) this->_output_rate / (long double) this->_input_rate; } | ||||
|                 [[nodiscard]] inline size_t estimated_output_size(size_t input_length) { | ||||
|                     if(!this->soxr_handle) return input_length; /* no resembling needed */ | ||||
| 					return (size_t) ceill(this->io_ratio() * input_length + *soxr_num_clips(this->soxr_handle)) + 1; | ||||
| 				} | ||||
|                 [[nodiscard]] inline size_t input_size(size_t output_length) { | ||||
|                 [[nodiscard]] inline size_t input_size(size_t output_length) const { | ||||
|                     return (size_t) ceill((long double) this->_input_rate / (long double) this->_output_rate * output_length); | ||||
|                 } | ||||
| 
 | ||||
|  | ||||
| @ -3,38 +3,35 @@ | ||||
| #include <cstdint> | ||||
| #include <memory> | ||||
| 
 | ||||
| namespace tc { | ||||
| 	namespace audio { | ||||
| 
 | ||||
| namespace tc::audio { | ||||
| #ifdef WIN32 | ||||
|         #pragma pack(push,1) | ||||
|         #define __attribute__packed_1 | ||||
|     #pragma pack(push,1) | ||||
|     #define __attribute__packed_1 | ||||
| #else | ||||
|         #define __attribute__packed_1 __attribute__((packed, aligned(1))) | ||||
|     #define __attribute__packed_1 __attribute__((packed, aligned(1))) | ||||
| #endif | ||||
| 		/* Every sample is a float (4byte) */ | ||||
| 		struct __attribute__packed_1 SampleBuffer { | ||||
| 			static constexpr size_t HEAD_LENGTH = 4; | ||||
|     /* Every sample is a float (4byte) */ | ||||
|     struct __attribute__packed_1 SampleBuffer { | ||||
|         static constexpr size_t HEAD_LENGTH = 4; | ||||
| 
 | ||||
| 			uint16_t sample_size; | ||||
| 			uint16_t sample_index; | ||||
|         uint16_t sample_size; | ||||
|         uint16_t sample_index; | ||||
| 
 | ||||
| 			char sample_data[ | ||||
|         char sample_data[ | ||||
| #ifndef WIN32 | ||||
| 					0 | ||||
|                 0 | ||||
| #else | ||||
| 					1 /* windows does not allow zero sized arrays */ | ||||
|                 1 /* windows does not allow zero sized arrays */ | ||||
| #endif | ||||
| 			]; | ||||
|         ]; | ||||
| 
 | ||||
| 			static std::shared_ptr<SampleBuffer> allocate(uint8_t /* channels */, uint16_t /* samples */); | ||||
| 		}; | ||||
|         static std::shared_ptr<SampleBuffer> allocate(uint8_t /* channels */, uint16_t /* samples */); | ||||
|     }; | ||||
| 
 | ||||
| #ifndef WIN32 | ||||
| 		static_assert(sizeof(SampleBuffer) == 4, "Invalid SampleBuffer packaging!"); | ||||
|     static_assert(sizeof(SampleBuffer) == 4, "Invalid SampleBuffer packaging!"); | ||||
| #else | ||||
|         #pragma pack(pop) | ||||
| 		static_assert(sizeof(SampleBuffer) == 5, "Invalid SampleBuffer packaging!"); | ||||
|     #pragma pack(pop) | ||||
|     static_assert(sizeof(SampleBuffer) == 5, "Invalid SampleBuffer packaging!"); | ||||
| #endif | ||||
| 	} | ||||
| } | ||||
| @ -50,10 +50,6 @@ AudioOutputStreamWrapper::~AudioOutputStreamWrapper() { | ||||
| 
 | ||||
| void AudioOutputStreamWrapper::drop_stream() { | ||||
| 	if(this->_own_handle) { | ||||
| 		auto handle = this->_own_handle->handle; | ||||
| 		if(handle) { | ||||
| 			handle->delete_source(this->_own_handle); | ||||
| 		} | ||||
| 		this->_own_handle->on_underflow = nullptr; | ||||
| 		this->_own_handle->on_overflow = nullptr; | ||||
| 	} | ||||
| @ -112,7 +108,7 @@ NAN_METHOD(AudioOutputStreamWrapper::_clear) { | ||||
| NAN_METHOD(AudioOutputStreamWrapper::_deleted) { | ||||
| 	auto client = ObjectWrap::Unwrap<AudioOutputStreamWrapper>(info.Holder()); | ||||
| 
 | ||||
| 	info.GetReturnValue().Set(!client->_own_handle || !client->_own_handle->handle); | ||||
| 	info.GetReturnValue().Set(!client->_own_handle); | ||||
| } | ||||
| 
 | ||||
| NAN_METHOD(AudioOutputStreamWrapper::_delete) { | ||||
| @ -121,10 +117,11 @@ NAN_METHOD(AudioOutputStreamWrapper::_delete) { | ||||
| } | ||||
| 
 | ||||
| ssize_t AudioOutputStreamWrapper::write_data(const std::shared_ptr<AudioOutputSource>& handle, void *source, size_t samples, bool interleaved) { | ||||
| 	if(interleaved) | ||||
| 		return handle->enqueue_samples(source, samples); | ||||
|     else | ||||
| 	    return handle->enqueue_samples_no_interleave(source, samples); | ||||
| 	if(interleaved) { | ||||
|         return handle->enqueue_samples(source, samples); | ||||
| 	} else { | ||||
|         return handle->enqueue_samples_no_interleave(source, samples); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| NAN_METHOD(AudioOutputStreamWrapper::_write_data) { | ||||
| @ -223,7 +220,7 @@ NAN_METHOD(AudioOutputStreamWrapper::_get_buffer_latency) { | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	info.GetReturnValue().Set((float) handle->min_buffered_samples / (float) handle->sample_rate); | ||||
| 	info.GetReturnValue().Set((float) handle->min_buffered_samples() / (float) handle->sample_rate); | ||||
| } | ||||
| 
 | ||||
| NAN_METHOD(AudioOutputStreamWrapper::_set_buffer_latency) { | ||||
| @ -240,7 +237,7 @@ NAN_METHOD(AudioOutputStreamWrapper::_set_buffer_latency) { | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	handle->min_buffered_samples = (size_t) ceil(handle->sample_rate * info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0)); | ||||
| 	handle->set_min_buffered_samples((size_t) ceil(handle->sample_rate * info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0))); | ||||
| } | ||||
| 
 | ||||
| NAN_METHOD(AudioOutputStreamWrapper::_get_buffer_max_latency) { | ||||
| @ -252,7 +249,7 @@ NAN_METHOD(AudioOutputStreamWrapper::_get_buffer_max_latency) { | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	info.GetReturnValue().Set((float) handle->max_latency() / (float) handle->sample_rate); | ||||
| 	info.GetReturnValue().Set((float) handle->max_buffering() / (float) handle->sample_rate); | ||||
| } | ||||
| 
 | ||||
| NAN_METHOD(AudioOutputStreamWrapper::_set_buffer_max_latency) { | ||||
| @ -269,7 +266,7 @@ NAN_METHOD(AudioOutputStreamWrapper::_set_buffer_max_latency) { | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	handle->max_buffered_samples = (size_t) ceil(handle->sample_rate * info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0)); | ||||
| 	handle->set_max_buffered_samples((size_t) ceil(handle->sample_rate * info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0))); | ||||
| } | ||||
| 
 | ||||
| NAN_METHOD(AudioOutputStreamWrapper::_flush_buffer) { | ||||
|  | ||||
| @ -72,8 +72,7 @@ namespace tc::audio::sounds { | ||||
|             } state_{PLAYER_STATE_UNSET}; | ||||
| 
 | ||||
|             void finalize(bool is_destructor_call) { | ||||
|                 if(this->output_source && global_audio_output) | ||||
|                     global_audio_output->delete_source(this->output_source); | ||||
|                 this->output_source = nullptr; | ||||
|                 if(this->file_handle) | ||||
|                     this->file_handle = nullptr; | ||||
|                 if(auto buffer{std::exchange(this->cache_buffer, nullptr)}; buffer) | ||||
| @ -160,7 +159,7 @@ namespace tc::audio::sounds { | ||||
|                     this->state_ = PLAYER_STATE_UNSET; | ||||
|                     return; | ||||
|                 } | ||||
|                 auto filled_samples = this->output_source->current_latency(); | ||||
|                 auto filled_samples = this->output_source->currently_buffered_samples(); | ||||
|             } | ||||
| 
 | ||||
|             void initialize_playback() { | ||||
| @ -170,8 +169,8 @@ namespace tc::audio::sounds { | ||||
|                 const auto max_buffer = (size_t) ceil(global_audio_output->sample_rate() * kBufferChunkTimespan * 3); | ||||
|                 this->output_source = global_audio_output->create_source(max_buffer); | ||||
|                 this->output_source->overflow_strategy = audio::overflow_strategy::ignore; | ||||
|                 this->output_source->max_buffered_samples = max_buffer; | ||||
|                 this->output_source->min_buffered_samples = (size_t) floor(this->output_source->sample_rate * 0.04); | ||||
|                 this->output_source->set_max_buffered_samples(max_buffer); | ||||
|                 this->output_source->set_min_buffered_samples((size_t) floor(this->output_source->sample_rate * 0.04)); | ||||
| 
 | ||||
|                 auto weak_this = this->weak_from_this(); | ||||
|                 this->output_source->on_underflow = [weak_this](size_t sample_count){ | ||||
| @ -208,8 +207,8 @@ namespace tc::audio::sounds { | ||||
|             [[nodiscard]] inline bool could_enqueue_next_buffer() const { | ||||
|                 if(!this->output_source) return false; | ||||
| 
 | ||||
|                 const auto current_size = this->output_source->current_latency(); | ||||
|                 const auto max_size = this->output_source->max_buffered_samples; | ||||
|                 const auto current_size = this->output_source->currently_buffered_samples(); | ||||
|                 const auto max_size = this->output_source->max_buffered_samples(); | ||||
|                 if(current_size > max_size) return false; | ||||
| 
 | ||||
|                 const auto size_left = max_size - current_size; | ||||
|  | ||||
| @ -223,7 +223,7 @@ VoiceClient::~VoiceClient() { | ||||
| 	this->cancel_replay(); /* cleanup all buffers */ | ||||
| 	if(this->output_source) { | ||||
|         this->output_source->on_underflow = nullptr; /* to ensure */ | ||||
|         global_audio_output->delete_source(this->output_source); | ||||
|         this->output_source = nullptr; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @ -232,19 +232,21 @@ void VoiceClient::initialize() { | ||||
| 
 | ||||
|     audio::initialize([weak_this]{ | ||||
|         auto client = weak_this.lock(); | ||||
|         if(!client) return; | ||||
|         if(!client) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         assert(global_audio_output); | ||||
|         client->output_source = global_audio_output->create_source(); | ||||
|         client->output_source->overflow_strategy = audio::overflow_strategy::ignore; | ||||
|         client->output_source->max_buffered_samples = (size_t) ceil(client->output_source->sample_rate * 0.5); | ||||
|         client->output_source->min_buffered_samples = (size_t) ceil(client->output_source->sample_rate * 0.04); | ||||
|         client->output_source->set_max_buffered_samples((size_t) ceil(client->output_source->sample_rate * 0.5)); | ||||
|         client->output_source->set_min_buffered_samples((size_t) ceil(client->output_source->sample_rate * 0.04)); | ||||
| 
 | ||||
|         const auto client_ptr = &*client; | ||||
|         client->output_source->on_underflow = [client_ptr](size_t sample_count){ /* this callback will never be called when the client has been deallocated */ | ||||
|             if(client_ptr->state_ == state::stopping) | ||||
|             if(client_ptr->state_ == state::stopping) { | ||||
|                 client_ptr->set_state(state::stopped); | ||||
|             else if(client_ptr->state_ != state::stopped) { | ||||
|             } else if(client_ptr->state_ != state::stopped) { | ||||
|                 if(client_ptr->_last_received_packet + chrono::seconds{1} < chrono::system_clock::now()) { | ||||
|                     client_ptr->set_state(state::stopped); | ||||
|                     log_warn(category::audio, tr("Client {} has a audio buffer underflow for {} samples and not received any data for one second. Stopping replay."), client_ptr->client_id_, sample_count); | ||||
| @ -406,21 +408,25 @@ void VoiceClient::process_packet(uint16_t packet_id, const pipes::buffer_view& b | ||||
| void VoiceClient::cancel_replay() { | ||||
| 	log_trace(category::voice_connection, tr("Cancel replay for client {}"), this->client_id_); | ||||
| 
 | ||||
| 	if(output_source) this->output_source->clear(); | ||||
| 	auto output = this->output_source; | ||||
| 	if(output) { | ||||
|         output->clear(); | ||||
| 	} | ||||
| 
 | ||||
| 	this->set_state(state::stopped); | ||||
| 	audio::decode_event_loop->cancel(static_pointer_cast<event::EventEntry>(this->ref())); | ||||
| 
 | ||||
| 	auto execute_lock = this->execute_lock(true); | ||||
| 	for(auto& codec : this->codec) { | ||||
| 		auto head = codec.pending_buffers; | ||||
| 	for(auto& codec_entry : this->codec) { | ||||
| 		auto head = codec_entry.pending_buffers; | ||||
| 		while(head) { | ||||
| 			auto tmp = head->next; | ||||
| 			delete head; | ||||
| 			head = tmp; | ||||
| 		} | ||||
| 
 | ||||
| 		codec.pending_buffers = nullptr; | ||||
| 		codec.force_replay = nullptr; | ||||
|         codec_entry.pending_buffers = nullptr; | ||||
|         codec_entry.force_replay = nullptr; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @ -437,8 +443,9 @@ void VoiceClient::event_execute(const std::chrono::system_clock::time_point &sch | ||||
| 	auto timeout = chrono::system_clock::now() + max_time; | ||||
| 
 | ||||
| 	for(auto& audio_codec : this->codec) { | ||||
| 		if(!audio_codec.process_pending) | ||||
| 			continue; | ||||
| 		if(!audio_codec.process_pending) { | ||||
|             continue; | ||||
| 		} | ||||
| 
 | ||||
| 		unique_lock lock{audio_codec.pending_lock}; | ||||
| 		do { | ||||
| @ -568,8 +575,9 @@ void VoiceClient::event_execute(const std::chrono::system_clock::time_point &sch | ||||
| 							log_warn(category::audio, tr("Failed to decode lost packets for client {}: {}"), this->_client_id, error); | ||||
| 						*/ | ||||
| 						auto decoded = this->decode_buffer(audio_codec.codec, replay_head->buffer, true); | ||||
| 						if(decoded) | ||||
| 							this->output_source->enqueue_samples(decoded->sample_data, decoded->sample_size); | ||||
| 						if(decoded) { | ||||
|                             this->output_source->enqueue_samples(decoded->sample_data, decoded->sample_size); | ||||
| 						} | ||||
| 					} | ||||
| 
 | ||||
| 					const auto is_new_audio_stream = this->state_ != state::buffering && this->state_ != state::playing; | ||||
| @ -716,7 +724,8 @@ std::shared_ptr<audio::SampleBuffer> VoiceClient::decode_buffer(const codec::val | ||||
| } | ||||
| 
 | ||||
| void VoiceClient::event_execute_dropped(const std::chrono::system_clock::time_point &point) { | ||||
| 	if(audio_decode_event_dropped.exchange(true)) | ||||
| 		//Is not really a warning, it happens all the time and isn't really an issue
 | ||||
| 		;//log_warn(category::voice_connection, tr("Dropped auto enqueue event execution two or more times in a row for client {}"), this->_client_id);
 | ||||
| 	if(audio_decode_event_dropped.exchange(true)) { | ||||
|         //Is not really a warning, it happens all the time and isn't really an issue
 | ||||
|         //log_warn(category::voice_connection, tr("Dropped auto enqueue event execution two or more times in a row for client {}"), this->_client_id);
 | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -69,7 +69,7 @@ namespace tc::connection { | ||||
| 
 | ||||
|             void initialize(); | ||||
| 
 | ||||
|             inline uint16_t client_id() { return this->client_id_; } | ||||
|             inline uint16_t client_id() const { return this->client_id_; } | ||||
| 
 | ||||
|             void initialize_js_object(); | ||||
|             void finalize_js_object(); | ||||
| @ -153,11 +153,14 @@ namespace tc::connection { | ||||
|             std::chrono::system_clock::time_point _last_received_packet; | ||||
|             state::value state_ = state::stopped; | ||||
|             inline void set_state(state::value value) { | ||||
|                 if(value == this->state_) | ||||
|                 if(value == this->state_) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 this->state_ = value; | ||||
|                 if(this->on_state_changed) | ||||
|                 if(this->on_state_changed) { | ||||
|                     this->on_state_changed(); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             std::atomic_bool audio_decode_event_dropped{false}; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user