diff options
Diffstat (limited to 'source/base/StarMixer.cpp')
-rw-r--r-- | source/base/StarMixer.cpp | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/source/base/StarMixer.cpp b/source/base/StarMixer.cpp new file mode 100644 index 0000000..96705b0 --- /dev/null +++ b/source/base/StarMixer.cpp @@ -0,0 +1,486 @@ +#include "StarMixer.hpp" +#include "StarIterator.hpp" +#include "StarInterpolation.hpp" +#include "StarTime.hpp" +#include "StarLogging.hpp" + +namespace Star { + +namespace { + float rateOfChangeFromRampTime(float rampTime) { + static const float MaxRate = 10000.0f; + + if (rampTime < 1.0f / MaxRate) + return MaxRate; + else + return 1.0f / rampTime; + } +} + +AudioInstance::AudioInstance(Audio const& audio) + : m_audio(audio) { + m_mixerGroup = MixerGroup::Effects; + + m_volume = {1.0f, 1.0f, 0}; + + m_pitchMultiplier = 1.0f; + m_pitchMultiplierTarget = 1.0f; + m_pitchMultiplierVelocity = 0; + + m_loops = 0; + m_stopping = false; + m_finished = false; + + m_rangeMultiplier = 1.0f; + + m_clockStopFadeOut = 0.0f; +} + +Maybe<Vec2F> AudioInstance::position() const { + MutexLocker locker(m_mutex); + return m_position; +} + +void AudioInstance::setPosition(Maybe<Vec2F> position) { + MutexLocker locker(m_mutex); + m_position = position; +} + +void AudioInstance::translate(Vec2F const& distance) { + MutexLocker locker(m_mutex); + if (m_position) + *m_position += distance; + else + m_position = distance; +} + +float AudioInstance::rangeMultiplier() const { + MutexLocker locker(m_mutex); + return m_rangeMultiplier; +} + +void AudioInstance::setRangeMultiplier(float rangeMultiplier) { + MutexLocker locker(m_mutex); + m_rangeMultiplier = rangeMultiplier; +} + +void AudioInstance::setVolume(float targetValue, float rampTime) { + starAssert(targetValue >= 0); + MutexLocker locker(m_mutex); + + if (m_stopping) + return; + + if (rampTime <= 0.0f) { + m_volume.value = targetValue; + m_volume.target = targetValue; + m_volume.velocity = 0.0f; + } else { + m_volume.target = targetValue; + m_volume.velocity = rateOfChangeFromRampTime(rampTime); + } +} + +void AudioInstance::setPitchMultiplier(float targetValue, float rampTime) { + starAssert(targetValue >= 0); + MutexLocker locker(m_mutex); + + if (m_stopping) + return; + + if (rampTime <= 0.0f) { + m_pitchMultiplier = targetValue; + m_pitchMultiplierTarget = targetValue; + m_pitchMultiplierVelocity = 0.0f; + } else { + m_pitchMultiplierTarget = targetValue; + m_pitchMultiplierVelocity = rateOfChangeFromRampTime(rampTime); + } +} + +int AudioInstance::loops() const { + MutexLocker locker(m_mutex); + return m_loops; +} + +void AudioInstance::setLoops(int loops) { + MutexLocker locker(m_mutex); + m_loops = loops; +} + +double AudioInstance::currentTime() const { + return m_audio.currentTime(); +} + +double AudioInstance::totalTime() const { + return m_audio.totalTime(); +} + +void AudioInstance::seekTime(double time) { + m_audio.seekTime(time); +} + +MixerGroup AudioInstance::mixerGroup() const { + MutexLocker locker(m_mutex); + return m_mixerGroup; +} + +void AudioInstance::setMixerGroup(MixerGroup mixerGroup) { + MutexLocker locker(m_mutex); + m_mixerGroup = mixerGroup; +} + +void AudioInstance::setClockStart(Maybe<int64_t> clockStartTime) { + MutexLocker locker(m_mutex); + m_clockStart = clockStartTime; +} + +void AudioInstance::setClockStop(Maybe<int64_t> clockStopTime, int64_t fadeOutTime) { + MutexLocker locker(m_mutex); + m_clockStop = clockStopTime; + m_clockStopFadeOut = fadeOutTime; +} + +void AudioInstance::stop(float rampTime) { + MutexLocker locker(m_mutex); + + if (rampTime <= 0.0f) { + m_volume.value = 0.0f; + m_volume.target = 0.0f; + m_volume.velocity = 0.0f; + + m_pitchMultiplierTarget = 0.0f; + m_pitchMultiplierVelocity = 0.0f; + } else { + m_volume.target = 0.0f; + m_volume.velocity = rateOfChangeFromRampTime(rampTime); + } + + m_stopping = true; +} + +bool AudioInstance::finished() const { + return m_finished; +} + +Mixer::Mixer(unsigned sampleRate, unsigned channels) { + m_sampleRate = sampleRate; + m_channels = channels; + + m_volume = {1.0f, 1.0f, 0}; + + m_groupVolumes[MixerGroup::Effects] = {1.0f, 1.0f, 0}; + m_groupVolumes[MixerGroup::Music] = {1.0f, 1.0f, 0}; + m_groupVolumes[MixerGroup::Cinematic] = {1.0f, 1.0f, 0}; +} + +unsigned Mixer::sampleRate() const { + return m_sampleRate; +} + +unsigned Mixer::channels() const { + return m_channels; +} + +void Mixer::addEffect(String const& effectName, EffectFunction effectFunction, float rampTime) { + MutexLocker locker(m_effectsMutex); + m_effects[effectName] = make_shared<EffectInfo>(EffectInfo{effectFunction, 0.0f, rateOfChangeFromRampTime(rampTime), false}); +} + +void Mixer::removeEffect(String const& effectName, float rampTime) { + MutexLocker locker(m_effectsMutex); + if (m_effects.contains(effectName)) + m_effects[effectName]->velocity = -rateOfChangeFromRampTime(rampTime); +} + +StringList Mixer::currentEffects() { + MutexLocker locker(m_effectsMutex); + return m_effects.keys(); +} + +bool Mixer::hasEffect(String const& effectName) { + MutexLocker locker(m_effectsMutex); + return m_effects.contains(effectName); +} + +void Mixer::setVolume(float volume, float rampTime) { + MutexLocker locker(m_mutex); + m_volume.target = volume; + m_volume.velocity = rateOfChangeFromRampTime(rampTime); +} + +void Mixer::play(AudioInstancePtr sample) { + MutexLocker locker(m_queueMutex); + m_audios.add(move(sample), AudioState{List<float>(m_channels, 1.0f)}); +} + +void Mixer::stopAll(float rampTime) { + MutexLocker locker(m_queueMutex); + float vel = rateOfChangeFromRampTime(rampTime); + for (auto const& p : m_audios) + p.first->stop(vel); +} + +void Mixer::read(int16_t* outBuffer, size_t frameCount) { + // Make this method as least locky as possible by copying all the needed + // member data before the expensive audio / effect stuff. + unsigned sampleRate; + unsigned channels; + float volume; + float volumeVelocity; + float targetVolume; + Map<MixerGroup, RampedValue> groupVolumes; + + { + MutexLocker locker(m_mutex); + sampleRate = m_sampleRate; + channels = m_channels; + volume = m_volume.value; + volumeVelocity = m_volume.velocity; + targetVolume = m_volume.target; + groupVolumes = m_groupVolumes; + } + + size_t bufferSize = frameCount * m_channels; + m_mixBuffer.resize(bufferSize, 0); + + float time = (float)frameCount / sampleRate; + float beginVolume = volume; + float endVolume = approach(targetVolume, volume, volumeVelocity * time); + + Map<MixerGroup, float> groupEndVolumes; + for (auto p : groupVolumes) + groupEndVolumes[p.first] = approach(p.second.target, p.second.value, p.second.velocity * time); + + auto sampleStartTime = Time::millisecondsSinceEpoch(); + unsigned millisecondsInBuffer = (bufferSize * 1000) / (channels * sampleRate); + auto sampleEndTime = sampleStartTime + millisecondsInBuffer; + + for (size_t i = 0; i < bufferSize; ++i) + outBuffer[i] = 0; + + { + MutexLocker locker(m_queueMutex); + // Mix all active sounds + for (auto& p : m_audios) { + auto& audioInstance = p.first; + auto& audioState = p.second; + + MutexLocker audioLocker(audioInstance->m_mutex); + + if (audioInstance->m_finished) + continue; + + if (audioInstance->m_clockStart && *audioInstance->m_clockStart > sampleEndTime) + continue; + + float groupVolume = groupVolumes[audioInstance->m_mixerGroup].value; + float groupEndVolume = groupEndVolumes[audioInstance->m_mixerGroup]; + + bool finished = false; + + float audioStopVolBegin = audioInstance->m_volume.value; + float audioStopVolEnd = (audioInstance->m_volume.velocity > 0) + ? approach(audioInstance->m_volume.target, audioStopVolBegin, audioInstance->m_volume.velocity * time) + : audioInstance->m_volume.value; + + float pitchMultiplier = (audioInstance->m_pitchMultiplierVelocity > 0) + ? approach(audioInstance->m_pitchMultiplierTarget, audioInstance->m_pitchMultiplier, audioInstance->m_pitchMultiplierVelocity * time) + : audioInstance->m_pitchMultiplier; + + if (audioStopVolEnd == 0.0f && audioInstance->m_stopping) + finished = true; + + size_t ramt = 0; + if (audioInstance->m_clockStart && *audioInstance->m_clockStart > sampleStartTime) { + int silentSamples = (*audioInstance->m_clockStart - sampleStartTime) * sampleRate / 1000; + for (unsigned i = 0; i < silentSamples * channels; ++i) + m_mixBuffer[i] = 0; + ramt += silentSamples * channels; + } + ramt += audioInstance->m_audio.resample(channels, sampleRate, m_mixBuffer.ptr() + ramt, bufferSize - ramt, pitchMultiplier); + while (ramt != bufferSize && !finished) { + // Only seek back to the beginning and read more data if loops is < 0 + // (loop forever), or we have more loops to go, otherwise, the sample is + // finished. + if (audioInstance->m_loops != 0) { + audioInstance->m_audio.seekSample(0); + ramt += audioInstance->m_audio.resample(channels, sampleRate, m_mixBuffer.ptr() + ramt, bufferSize - ramt, pitchMultiplier); + if (audioInstance->m_loops > 0) + --audioInstance->m_loops; + } else { + finished = true; + } + } + if (audioInstance->m_clockStop && *audioInstance->m_clockStop < sampleEndTime) { + for (size_t s = 0; s < ramt / channels; ++s) { + unsigned millisecondsInBuffer = (s * 1000) / sampleRate; + auto sampleTime = sampleStartTime + millisecondsInBuffer; + if (sampleTime > *audioInstance->m_clockStop) { + float volume = 0.0f; + if (audioInstance->m_clockStopFadeOut > 0) + volume = 1.0f - (float)(sampleTime - *audioInstance->m_clockStop) / (float)audioInstance->m_clockStopFadeOut; + + if (volume <= 0) { + for (size_t c = 0; c < channels; ++c) + m_mixBuffer[s * channels + c] = 0; + } else { + for (size_t c = 0; c < channels; ++c) + m_mixBuffer[s * channels + c] = m_mixBuffer[s * channels + c] * volume; + } + } + } + if (sampleEndTime > *audioInstance->m_clockStop + audioInstance->m_clockStopFadeOut) + finished = true; + } + + for (size_t s = 0; s < ramt / channels; ++s) { + float vol = lerp((float)s / frameCount, beginVolume * groupVolume * audioStopVolBegin, endVolume * groupEndVolume * audioStopVolEnd); + for (size_t c = 0; c < channels; ++c) { + float sample = m_mixBuffer[s * channels + c] * vol * audioState.positionalChannelVolumes[c] * audioInstance->m_volume.value; + outBuffer[s * channels + c] = clamp(sample + outBuffer[s * channels + c], -32767.0f, 32767.0f); + } + } + + audioInstance->m_volume.value = audioStopVolEnd; + audioInstance->m_finished = finished; + } + } + + { + MutexLocker locker(m_effectsMutex); + // Apply all active effects + for (auto const& pair : m_effects) { + auto const& effectInfo = pair.second; + if (effectInfo->finished) + continue; + + float effectBegin = effectInfo->amount; + float effectEnd; + if (effectInfo->velocity < 0) + effectEnd = approach(0.0f, effectBegin, -effectInfo->velocity * time); + else + effectEnd = approach(1.0f, effectBegin, effectInfo->velocity * time); + + std::copy(outBuffer, outBuffer + bufferSize, m_mixBuffer.begin()); + effectInfo->effectFunction(m_mixBuffer.ptr(), frameCount, channels); + + for (size_t s = 0; s < frameCount; ++s) { + float amt = lerp((float)s / frameCount, effectBegin, effectEnd); + for (size_t c = 0; c < channels; ++c) { + int16_t prev = outBuffer[s * channels + c]; + outBuffer[s * channels + c] = lerp(amt, prev, m_mixBuffer[s * channels + c]); + } + } + + effectInfo->amount = effectEnd; + effectInfo->finished = effectInfo->amount <= 0.0f; + } + } + + { + MutexLocker locker(m_mutex); + + m_volume.value = endVolume; + + for (auto p : groupEndVolumes) + m_groupVolumes[p.first].value = p.second; + } +} + +Mixer::EffectFunction Mixer::lowpass(size_t avgSize) const { + struct LowPass { + LowPass(size_t avgSize) : avgSize(avgSize) {} + + size_t avgSize; + List<Deque<float>> filter; + + void operator()(int16_t* buffer, size_t frames, unsigned channels) { + filter.resize(channels); + for (size_t f = 0; f < frames; ++f) { + for (size_t c = 0; c < channels; ++c) { + auto& filterChannel = filter[c]; + filterChannel.append(buffer[f * channels + c] / 32767.0f); + while (filterChannel.size() > avgSize) + filterChannel.takeFirst(); + buffer[f * channels + c] = sum(filterChannel) / (float)avgSize * 32767.0f; + } + } + } + }; + + return LowPass(avgSize); +} + +Mixer::EffectFunction Mixer::echo(float time, float dry, float wet) const { + struct Echo { + unsigned echoLength; + float dry; + float wet; + List<Deque<float>> filter; + + void operator()(int16_t* buffer, size_t frames, unsigned channels) { + if (echoLength == 0) + return; + + filter.resize(channels); + for (size_t c = 0; c < channels; ++c) { + auto& filterChannel = filter[c]; + if (filterChannel.empty()) + filterChannel.resize(echoLength, 0); + } + + for (size_t f = 0; f < frames; ++f) { + for (size_t c = 0; c < channels; ++c) { + auto& filterChannel = filter[c]; + buffer[f * channels + c] = buffer[f * channels + c] * dry + filter[c][0] * wet; + filterChannel.append(buffer[f * channels + c]); + while (filterChannel.size() > echoLength) + filterChannel.takeFirst(); + } + } + } + }; + + return Echo{(unsigned)(time * m_sampleRate), dry, wet, {}}; +} + +void Mixer::setGroupVolume(MixerGroup group, float targetValue, float rampTime) { + MutexLocker locker(m_mutex); + if (rampTime <= 0.0f) { + m_groupVolumes[group].value = targetValue; + m_groupVolumes[group].target = targetValue; + m_groupVolumes[group].velocity = 0.0f; + } else { + m_groupVolumes[group].target = targetValue; + m_groupVolumes[group].velocity = rateOfChangeFromRampTime(rampTime); + } +} + +void Mixer::update(PositionalAttenuationFunction positionalAttenuationFunction) { + { + MutexLocker locker(m_queueMutex); + eraseWhere(m_audios, [&](auto& p) { + if (p.first->m_finished) + return true; + + if (positionalAttenuationFunction && p.first->m_position) { + for (unsigned c = 0; c < m_channels; ++c) + p.second.positionalChannelVolumes[c] = 1.0f - positionalAttenuationFunction(c, *p.first->m_position, p.first->m_rangeMultiplier); + } else { + for (unsigned c = 0; c < m_channels; ++c) + p.second.positionalChannelVolumes[c] = 1.0f; + } + return false; + }); + } + + { + MutexLocker locker(m_effectsMutex); + eraseWhere(m_effects, [](auto const& p) { + return p.second->finished; + }); + } +} + +} |