SoundTouch resampler integration

This commit is contained in:
Vestral
2022-01-05 17:26:12 +09:00
committed by Megamouse
parent 3a804674c9
commit 107107107c
28 changed files with 576 additions and 402 deletions
+187 -238
View File
@@ -8,10 +8,6 @@
#include <cmath>
#if defined(ARCH_X64)
#include "emmintrin.h"
#endif
LOG_CHANNEL(cellAudio);
vm::gvar<char, AUDIO_PORT_OFFSET * AUDIO_PORT_COUNT> g_audio_buffer;
@@ -60,11 +56,7 @@ void cell_audio_config::reset(bool backend_changed)
backend = Emu.GetCallbacks().get_audio();
}
{
std::string str;
backend->dump_capabilities(str);
cellAudio.notice("cellAudio initializing. Backend: %s, Capabilities: %s", backend->GetName(), str.c_str());
}
cellAudio.notice("cellAudio initializing. Backend: %s", backend->GetName());
const AudioFreq freq = AudioFreq::FREQ_48K;
const AudioSampleSize sample_size = raw.convert_to_s16 ? AudioSampleSize::S16 : AudioSampleSize::FLOAT;
@@ -85,14 +77,14 @@ void cell_audio_config::reset(bool backend_changed)
audio_channels = backend->get_channels();
audio_sampling_rate = backend->get_sampling_rate();
audio_block_period = AUDIO_BUFFER_SAMPLES * 1000000 / audio_sampling_rate;
audio_block_period = AUDIO_BUFFER_SAMPLES * 1'000'000 / audio_sampling_rate;
audio_sample_size = backend->get_sample_size();
audio_min_buffer_duration = backend->GetCallbackFrameLen();
audio_min_buffer_duration = backend->GetCallbackFrameLen() + u32{AUDIO_BUFFER_SAMPLES} * 2.0 / audio_sampling_rate; // Add 2 blocks to allow jitter compensation
audio_buffer_length = AUDIO_BUFFER_SAMPLES * audio_channels;
audio_buffer_size = audio_buffer_length * audio_sample_size;
desired_buffer_duration = raw.desired_buffer_duration * 1000llu;
desired_buffer_duration = std::max(static_cast<s64>(audio_min_buffer_duration * 1000), raw.desired_buffer_duration) * 1000llu;
buffering_enabled = raw.buffering_enabled && raw.renderer != audio_renderer::null;
minimum_block_period = audio_block_period / 2;
@@ -106,18 +98,18 @@ void cell_audio_config::reset(bool backend_changed)
const bool raw_time_stretching_enabled = buffering_enabled && raw.enable_time_stretching && (raw.time_stretching_threshold > 0);
time_stretching_enabled = raw_time_stretching_enabled && backend->has_capability(AudioBackend::SET_FREQUENCY_RATIO);
time_stretching_enabled = raw_time_stretching_enabled;
time_stretching_threshold = raw.time_stretching_threshold / 100.0f;
// Warn if audio backend does not support all requested features
if (raw.buffering_enabled && !buffering_enabled)
{
cellAudio.error("Audio backend %s does not support buffering, this option will be ignored.", backend->GetName());
}
if (raw_time_stretching_enabled && !time_stretching_enabled)
{
cellAudio.error("Audio backend %s does not support time stretching, this option will be ignored.", backend->GetName());
if (raw.enable_time_stretching)
{
cellAudio.error("Audio backend %s does not support time stretching, this option will be ignored.", backend->GetName());
}
}
}
@@ -125,7 +117,6 @@ audio_ringbuffer::audio_ringbuffer(cell_audio_config& _cfg)
: backend(_cfg.backend)
, cfg(_cfg)
, buf_sz(AUDIO_BUFFER_SAMPLES * _cfg.audio_channels)
, emu_paused(Emu.IsPaused())
{
// Initialize buffers
if (cfg.num_allocated_buffers > MAX_AUDIO_BUFFERS)
@@ -139,23 +130,26 @@ audio_ringbuffer::audio_ringbuffer(cell_audio_config& _cfg)
}
// Init audio dumper if enabled
if (g_cfg.audio.dump_to_file)
if (cfg.raw.dump_to_file)
{
m_dump.reset(new AudioDumper(cfg.audio_channels, cfg.audio_sampling_rate, cfg.audio_sample_size));
m_dump.Open(cfg.audio_channels, cfg.audio_sampling_rate, cfg.audio_sample_size);
}
// Configure resampler
resampler.set_params(static_cast<AudioChannelCnt>(cfg.audio_channels), AudioFreq::FREQ_48K);
resampler.set_tempo(RESAMPLER_MAX_FREQ_VAL);
const f64 buffer_dur_mult = [&]()
{
const f64 min_buf_dur = _cfg.audio_min_buffer_duration + 0.01; // Add 10ms to allow jitter compensation
if (cfg.raw.buffering_enabled)
if (cfg.buffering_enabled)
{
return std::max<f64>(min_buf_dur, cfg.raw.desired_buffer_duration / 1000.0 * 2); // Allocate 2x buffer to keep buffering algorithm happy
return cfg.desired_buffer_duration / 1'000'000.0 + 0.02; // Add 20ms to buffer to keep buffering algorithm happy
}
return min_buf_dur;
return cfg.audio_min_buffer_duration;
}();
cb_ringbuf.set_buf_size(static_cast<u32>(_cfg.audio_channels * _cfg.audio_sampling_rate * _cfg.audio_sample_size * buffer_dur_mult));
cb_ringbuf.set_buf_size(static_cast<u32>(cfg.audio_channels * cfg.audio_sampling_rate * cfg.audio_sample_size * buffer_dur_mult));
backend->SetWriteCallback(std::bind(&audio_ringbuffer::backend_write_callback, this, std::placeholders::_1, std::placeholders::_2));
}
@@ -171,19 +165,18 @@ audio_ringbuffer::~audio_ringbuffer()
f32 audio_ringbuffer::set_frequency_ratio(f32 new_ratio)
{
if (!has_capability(AudioBackend::SET_FREQUENCY_RATIO))
{
ensure(new_ratio == 1.0f);
frequency_ratio = 1.0f;
}
else
{
frequency_ratio = backend->SetFrequencyRatio(new_ratio);
//cellAudio.trace("set_frequency_ratio(%1.2f) -> %1.2f", new_ratio, frequency_ratio);
}
frequency_ratio = resampler.set_tempo(new_ratio);
return frequency_ratio;
}
float* audio_ringbuffer::get_buffer(u32 num) const
{
AUDIT(num < cfg.num_allocated_buffers);
AUDIT(buffer[num]);
return buffer[num].get();
}
u32 audio_ringbuffer::backend_write_callback(u32 size, void *buf)
{
if (!backend_active.observe()) backend_active = true;
@@ -196,50 +189,96 @@ u64 audio_ringbuffer::get_timestamp()
return get_system_time();
}
void audio_ringbuffer::enqueue(const float* in_buffer)
float* audio_ringbuffer::get_current_buffer() const
{
return get_buffer(cur_pos);
}
u64 audio_ringbuffer::get_enqueued_samples() const
{
AUDIT(cfg.buffering_enabled);
const u64 ringbuf_samples = cb_ringbuf.get_used_size() / (cfg.audio_sample_size * cfg.audio_channels);
if (cfg.time_stretching_enabled)
{
return ringbuf_samples + resampler.samples_available();
}
return ringbuf_samples;
}
u64 audio_ringbuffer::get_enqueued_playtime() const
{
AUDIT(cfg.buffering_enabled);
return get_enqueued_samples() * 1'000'000 / cfg.audio_sampling_rate;
}
void audio_ringbuffer::enqueue(bool enqueue_silence, bool force)
{
AUDIT(cur_pos < cfg.num_allocated_buffers);
// Prepare buffer
const void* buf = in_buffer;
static float silence_buffer[u32{AUDIO_MAX_CHANNELS_COUNT} * u32{AUDIO_BUFFER_SAMPLES}]{};
float* buf = silence_buffer;
if (buf == nullptr)
if (!enqueue_silence)
{
buf = buffer[cur_pos].get();
cur_pos = (cur_pos + 1) % cfg.num_allocated_buffers;
}
// Dump audio if enabled
if (m_dump)
{
m_dump->WriteData(buf, cfg.audio_buffer_size);
}
m_dump.WriteData(buf, cfg.audio_buffer_size);
enqueued_samples += AUDIO_BUFFER_SAMPLES;
// Start playing audio
play();
if (!backend_active.observe())
if (!backend_active.observe() && !force)
{
// backend is not ready yet
return;
}
// Enqueue audio
const u32 data_size = AUDIO_BUFFER_SAMPLES * cfg.audio_sample_size * cfg.audio_channels;
if (cb_ringbuf.get_free_size() >= data_size)
if (cfg.time_stretching_enabled)
{
cb_ringbuf.push(buf, data_size);
resampler.put_samples(buf, AUDIO_BUFFER_SAMPLES);
}
else
{
// Since time stretching step is skipped, we can commit to buffer directly
commit_data(buf, AUDIO_BUFFER_SAMPLES);
}
}
void audio_ringbuffer::enqueue_silence(u32 buf_count)
void audio_ringbuffer::enqueue_silence(u32 buf_count, bool force)
{
for (u32 i = 0; i < buf_count; i++)
{
enqueue(silence_buffer);
enqueue(true, force);
}
}
void audio_ringbuffer::process_resampled_data()
{
if (!cfg.time_stretching_enabled) return;
const auto samples = resampler.get_samples(cb_ringbuf.get_free_size() / (cfg.audio_sample_size * cfg.audio_channels));
commit_data(samples.first, samples.second);
}
void audio_ringbuffer::commit_data(f32* buf, u32 sample_cnt)
{
sample_cnt *= cfg.audio_channels;
if (cfg.backend->get_convert_to_s16())
{
AudioBackend::convert_to_s16(sample_cnt, buf, buf);
}
sample_cnt *= cfg.audio_sample_size;
if (cb_ringbuf.get_free_size() >= sample_cnt)
{
cb_ringbuf.push(buf, sample_cnt);
}
}
@@ -250,82 +289,44 @@ void audio_ringbuffer::play()
return;
}
if (frequency_ratio != 1.0f)
{
set_frequency_ratio(1.0f);
}
playing = true;
ensure(enqueued_samples > 0);
play_timestamp = get_timestamp();
backend->Play();
}
void audio_ringbuffer::flush()
{
//cellAudio.trace("Flushing an estimated %llu enqueued samples", enqueued_samples);
backend->Pause();
cb_ringbuf.flush();
resampler.flush();
backend_active = false;
playing = false;
if (frequency_ratio != 1.0f)
if (frequency_ratio != RESAMPLER_MAX_FREQ_VAL)
{
set_frequency_ratio(1.0f);
frequency_ratio = set_frequency_ratio(RESAMPLER_MAX_FREQ_VAL);
}
enqueued_samples = 0;
}
u64 audio_ringbuffer::update()
u64 audio_ringbuffer::update(bool emu_is_paused)
{
// Check emulator pause state
if (Emu.IsPaused())
if (emu_is_paused)
{
// Emulator paused
if (playing)
{
flush();
}
emu_paused = true;
}
else if (emu_paused)
else
{
// Emulator unpaused
if (enqueued_samples > 0)
{
play();
}
emu_paused = false;
play();
}
// Prepare timestamp and playing status
// Prepare timestamp
const u64 timestamp = get_timestamp();
const bool new_playing = !emu_paused && get_backend_playing();
// Calculate how many audio samples have played since last time
if (cfg.buffering_enabled && (playing || new_playing))
{
enqueued_samples = cb_ringbuf.get_used_size() / (cfg.audio_sample_size * cfg.audio_channels);
}
// Update playing state
if (playing != new_playing)
{
if (!new_playing)
{
cellAudio.warning("Audio backend stopped unexpectedly, likely due to a buffer underrun");
flush();
playing = false;
}
else
{
playing = true;
}
}
// Store and return timestamp
update_timestamp = timestamp;
@@ -445,15 +446,14 @@ void cell_audio_thread::reset_ports(s32 offset)
}
}
void cell_audio_thread::advance(u64 timestamp, bool reset)
void cell_audio_thread::advance(u64 timestamp)
{
ringbuffer->process_resampled_data();
std::unique_lock lock(mutex);
// update ports
if (reset)
{
reset_ports(0);
}
reset_ports(0);
for (auto& port : ports)
{
@@ -470,9 +470,7 @@ void cell_audio_thread::advance(u64 timestamp, bool reset)
if (cfg.buffering_enabled)
{
// Calculate rolling average of enqueued playtime
const u64 enqueued_playtime = ringbuffer->get_enqueued_playtime(/* raw */ true);
m_average_playtime = cfg.period_average_alpha * enqueued_playtime + (1.0f - cfg.period_average_alpha) * m_average_playtime;
//cellAudio.error("m_average_playtime=%4.2f, enqueued_playtime=%u", m_average_playtime, enqueued_playtime);
m_average_playtime = cfg.period_average_alpha * ringbuffer->get_enqueued_playtime() + (1.0f - cfg.period_average_alpha) * m_average_playtime;
}
m_counter++;
@@ -539,6 +537,7 @@ namespace audio
.enable_time_stretching = static_cast<bool>(g_cfg.audio.enable_time_stretching),
.time_stretching_threshold = g_cfg.audio.time_stretching_threshold,
.convert_to_s16 = static_cast<bool>(g_cfg.audio.convert_to_s16),
.dump_to_file = static_cast<bool>(g_cfg.audio.dump_to_file),
.downmix = g_cfg.audio.audio_channel_downmix,
.renderer = g_cfg.audio.renderer,
.provider = g_cfg.audio.provider
@@ -564,7 +563,8 @@ namespace audio
raw.enable_time_stretching != new_raw.enable_time_stretching ||
raw.convert_to_s16 != new_raw.convert_to_s16 ||
raw.downmix != new_raw.downmix ||
raw.renderer != new_raw.renderer)
raw.renderer != new_raw.renderer ||
raw.dump_to_file != new_raw.dump_to_file)
{
g_audio.cfg.raw = new_raw;
g_audio.m_update_configuration = raw.renderer != new_raw.renderer ? audio_backend_update::ALL : audio_backend_update::PARAM;
@@ -597,6 +597,7 @@ void cell_audio_thread::reset_counters()
m_last_period_end = m_start_time;
m_dynamic_period = 0;
m_backend_failed = false;
m_audio_should_restart = true;
}
cell_audio_thread::cell_audio_thread()
@@ -658,10 +659,13 @@ void cell_audio_thread::operator()()
m_backend_failed = false;
}
const u64 timestamp = ringbuffer->update();
const bool emu_paused = Emu.IsPaused();
const u64 timestamp = ringbuffer->update(emu_paused);
if (Emu.IsPaused())
if (emu_paused)
{
m_audio_should_restart = true;
ringbuffer->flush();
thread_ctrl::wait_for(10000);
continue;
}
@@ -670,6 +674,30 @@ void cell_audio_thread::operator()()
const u64 time_since_last_period = timestamp - m_last_period_end;
// Handle audio restart
if (m_audio_should_restart)
{
// align to 5.(3)ms on global clock - some games seem to prefer this
const s64 audio_period_alignment_delta = (timestamp - m_start_time) % cfg.audio_block_period;
if (audio_period_alignment_delta > cfg.period_comparison_margin)
{
thread_ctrl::wait_for(audio_period_alignment_delta - cfg.period_comparison_margin);
}
if (cfg.buffering_enabled)
{
// Restart algorithm
cellAudio.trace("restarting audio");
ringbuffer->enqueue_silence(cfg.desired_full_buffers, true);
finish_port_volume_stepping();
m_average_playtime = static_cast<f32>(ringbuffer->get_enqueued_playtime());
untouched_expected = 0;
}
m_audio_should_restart = false;
continue;
}
if (!cfg.buffering_enabled)
{
const u64 period_end = (m_counter * cfg.audio_block_period) + m_start_time;
@@ -684,95 +712,72 @@ void cell_audio_thread::operator()()
else
{
const u64 enqueued_samples = ringbuffer->get_enqueued_samples();
f32 frequency_ratio = ringbuffer->get_frequency_ratio();
u64 enqueued_playtime = ringbuffer->get_enqueued_playtime();
const f32 frequency_ratio = ringbuffer->get_frequency_ratio();
const u64 enqueued_playtime = ringbuffer->get_enqueued_playtime();
const u64 enqueued_buffers = enqueued_samples / AUDIO_BUFFER_SAMPLES;
const bool playing = ringbuffer->is_playing();
const auto tag_info = count_port_buffer_tags();
const u32 active_ports = std::get<0>(tag_info);
const u32 in_progress = std::get<1>(tag_info);
const u32 untouched = std::get<2>(tag_info);
const u32 incomplete = std::get<3>(tag_info);
// Wait for a dynamic period - try to maintain an average as close as possible to 5.(3)ms
if (!playing)
// Ratio between the rolling average of the audio period, and the desired audio period
const f32 average_playtime_ratio = m_average_playtime / cfg.audio_buffer_length;
// Use the above average ratio to decide how much buffer we should be aiming for
f32 desired_duration_adjusted = cfg.desired_buffer_duration + (cfg.audio_block_period / 2.0f);
if (average_playtime_ratio < 1.0f)
{
// When the buffer is empty, always use the correct block period
m_dynamic_period = cfg.audio_block_period;
desired_duration_adjusted /= std::max(average_playtime_ratio, 0.25f);
}
else
if (cfg.time_stretching_enabled)
{
// Ratio between the rolling average of the audio period, and the desired audio period
const f32 average_playtime_ratio = m_average_playtime / cfg.audio_buffer_length;
// Use the above average ratio to decide how much buffer we should be aiming for
f32 desired_duration_adjusted = cfg.desired_buffer_duration + (cfg.audio_block_period / 2.0f);
if (average_playtime_ratio < 1.0f)
{
desired_duration_adjusted /= std::max(average_playtime_ratio, 0.25f);
}
if (cfg.time_stretching_enabled)
{
// Calculate what the playtime is without a frequency ratio
const u64 raw_enqueued_playtime = ringbuffer->get_enqueued_playtime(/* raw= */ true);
// 1.0 means exactly as desired
// <1.0 means not as full as desired
// >1.0 means more full than desired
const f32 desired_duration_rate = raw_enqueued_playtime / desired_duration_adjusted;
// update frequency ratio if necessary
f32 new_ratio = frequency_ratio;
if (desired_duration_rate < cfg.time_stretching_threshold)
{
const f32 normalized_desired_duration_rate = desired_duration_rate / cfg.time_stretching_threshold;
const f32 request_ratio = normalized_desired_duration_rate * cfg.time_stretching_scale;
AUDIT(request_ratio <= 1.0f);
// change frequency ratio in steps
if (std::abs(frequency_ratio - request_ratio) > cfg.time_stretching_step)
{
new_ratio = ringbuffer->set_frequency_ratio(request_ratio);
}
}
else if (frequency_ratio != 1.0f)
{
new_ratio = ringbuffer->set_frequency_ratio(1.0f);
}
if (new_ratio != frequency_ratio)
{
// ratio changed, calculate new dynamic period
frequency_ratio = new_ratio;
enqueued_playtime = ringbuffer->get_enqueued_playtime();
m_dynamic_period = 0;
}
}
// 1.0 means exactly as desired
// <1.0 means not as full as desired
// >1.0 means more full than desired
const f32 desired_duration_rate = enqueued_playtime / desired_duration_adjusted;
if (desired_duration_rate >= 1.0f)
// update frequency ratio if necessary
if (desired_duration_rate < cfg.time_stretching_threshold)
{
// more full than desired
const f32 multiplier = 1.0f / desired_duration_rate;
m_dynamic_period = cfg.maximum_block_period - static_cast<u64>((cfg.maximum_block_period - cfg.audio_block_period) * multiplier);
const f32 normalized_desired_duration_rate = desired_duration_rate / cfg.time_stretching_threshold;
const f32 request_ratio = normalized_desired_duration_rate * cfg.time_stretching_scale;
AUDIT(request_ratio <= RESAMPLER_MAX_FREQ_VAL);
// change frequency ratio in steps
const f32 req_time_stretching_step = (request_ratio + frequency_ratio) / 2.0f;
if (req_time_stretching_step > cfg.time_stretching_step)
{
ringbuffer->set_frequency_ratio(req_time_stretching_step);
}
}
else
else if (frequency_ratio != RESAMPLER_MAX_FREQ_VAL)
{
// not as full as desired
const f32 multiplier = desired_duration_rate * desired_duration_rate; // quite aggressive, but helps more times than it hurts
m_dynamic_period = cfg.minimum_block_period + static_cast<u64>((cfg.audio_block_period - cfg.minimum_block_period) * multiplier);
ringbuffer->set_frequency_ratio(RESAMPLER_MAX_FREQ_VAL);
}
}
s64 time_left = m_dynamic_period - time_since_last_period;
// 1.0 means exactly as desired
// <1.0 means not as full as desired
// >1.0 means more full than desired
const f32 desired_duration_rate = enqueued_playtime / desired_duration_adjusted;
if (desired_duration_rate >= 1.0f)
{
// more full than desired
const f32 multiplier = 1.0f / desired_duration_rate;
m_dynamic_period = cfg.maximum_block_period - static_cast<u64>((cfg.maximum_block_period - cfg.audio_block_period) * multiplier);
}
else
{
// not as full as desired
const f32 multiplier = desired_duration_rate * desired_duration_rate; // quite aggressive, but helps more times than it hurts
m_dynamic_period = cfg.minimum_block_period + static_cast<u64>((cfg.audio_block_period - cfg.minimum_block_period) * multiplier);
}
const s64 time_left = m_dynamic_period - time_since_last_period;
if (time_left > cfg.period_comparison_margin)
{
thread_ctrl::wait_for(get_thread_wait_delay(time_left));
@@ -784,10 +789,7 @@ void cell_audio_thread::operator()()
{
// no need to mix, just enqueue silence and advance time
cellAudio.trace("enqueuing silence: no active ports, enqueued_buffers=%llu", enqueued_buffers);
if (playing)
{
ringbuffer->enqueue_silence(1);
}
ringbuffer->enqueue_silence();
untouched_expected = 0;
advance(timestamp);
continue;
@@ -797,16 +799,6 @@ void cell_audio_thread::operator()()
//cellAudio.error("active=%u, in_progress=%u, untouched=%u, incomplete=%u", active_ports, in_progress, untouched, incomplete);
if (untouched > untouched_expected)
{
if (!playing)
{
// We ran out of buffer, probably because we waited too long
// Don't enqueue anything, just advance time
cellAudio.trace("advancing time: untouched=%u/%u (expected=%u), enqueued_buffers=0", untouched, active_ports, untouched_expected);
untouched_expected = untouched;
advance(timestamp);
continue;
}
// Games may sometimes "skip" audio periods entirely if they're falling behind (a sort of "frameskip" for audio)
// As such, if the game doesn't touch buffers for too long we advance time hoping the game recovers
if (
@@ -831,10 +823,7 @@ void cell_audio_thread::operator()()
{
// There's no audio in the buffers, simply advance time
cellAudio.trace("enqueuing silence: untouched=%u/%u (expected=%u), enqueued_buffers=%llu", untouched, active_ports, untouched_expected, enqueued_buffers);
if (playing)
{
ringbuffer->enqueue_silence(1);
}
ringbuffer->enqueue_silence();
untouched_expected = untouched;
advance(timestamp);
continue;
@@ -858,25 +847,6 @@ void cell_audio_thread::operator()()
{
cellAudio.trace("enqueueing: untouched=%u/%u (expected=%u), incomplete=%u/%u enqueued_buffers=%llu", untouched, active_ports, untouched_expected, incomplete, active_ports, enqueued_buffers);
}
// Handle audio restart
if (!playing)
{
// We are not playing (likely buffer underrun)
// align to 5.(3)ms on global clock - some games seem to prefer this
const s64 audio_period_alignment_delta = (timestamp - m_start_time) % cfg.audio_block_period;
if (audio_period_alignment_delta > cfg.period_comparison_margin)
{
thread_ctrl::wait_for(audio_period_alignment_delta - cfg.period_comparison_margin);
}
// Flush, add silence, restart algorithm
cellAudio.trace("play/resume audio: received first audio buffer");
ringbuffer->flush();
ringbuffer->enqueue_silence(cfg.desired_full_buffers);
finish_port_volume_stepping();
m_average_playtime = static_cast<f32>(ringbuffer->get_enqueued_playtime());
}
}
// Mix
@@ -1112,27 +1082,6 @@ void cell_audio_thread::mix(float *out_buffer, s32 offset)
{
std::memset(out_buffer, 0, out_buffer_sz * sizeof(float));
}
else if (cfg.backend->get_convert_to_s16())
{
// convert the data from float to s16 with clipping:
// 2x MULPS
// 2x MAXPS (optional)
// 2x MINPS (optional)
// 2x CVTPS2DQ (converts float to s32)
// PACKSSDW (converts s32 to s16 with signed saturation)
#if defined(ARCH_X64)
for (usz i = 0; i < out_buffer_sz; i += 8)
{
const auto scale = _mm_set1_ps(0x8000);
_mm_store_ps(out_buffer + i / 2, _mm_castsi128_ps(_mm_packs_epi32(
_mm_cvtps_epi32(_mm_mul_ps(_mm_load_ps(out_buffer + i), scale)),
_mm_cvtps_epi32(_mm_mul_ps(_mm_load_ps(out_buffer + i + 4), scale)))));
}
#else
fmt::throw_exception("Not supported");
#endif
}
}
void cell_audio_thread::finish_port_volume_stepping()