diff options
Diffstat (limited to 'src/emu/sound.cpp')
-rw-r--r-- | src/emu/sound.cpp | 3048 |
1 files changed, 2210 insertions, 838 deletions
diff --git a/src/emu/sound.cpp b/src/emu/sound.cpp index 805a6920fed..e64941f558c 100644 --- a/src/emu/sound.cpp +++ b/src/emu/sound.cpp @@ -1,1174 +1,2546 @@ // license:BSD-3-Clause -// copyright-holders:Aaron Giles +// copyright-holders:O. Galibert, Aaron Giles /*************************************************************************** - sound.c + sound.cpp Core sound functions and definitions. ***************************************************************************/ #include "emu.h" -#include "speaker.h" -#include "emuopts.h" -#include "osdepend.h" + +#include "audio_effects/aeffect.h" +#include "resampler.h" + #include "config.h" +#include "emuopts.h" +#include "main.h" +#include "speaker.h" + #include "wavwrite.h" +#include "xmlfile.h" + +#include "osdepend.h" +#include "util/language.h" +#include <algorithm> //************************************************************************** // DEBUGGING //************************************************************************** -#define VERBOSE (0) +#define LOG_OUTPUT_FUNC m_machine.logerror -#define VPRINTF(x) do { if (VERBOSE) osd_printf_debug x; } while (0) +#define LOG_OSD_INFO (1U << 1) +#define LOG_MAPPING (1U << 2) +#define LOG_OSD_STREAMS (1U << 3) +#define LOG_ORDER (1U << 4) +#define VERBOSE -1 - -//************************************************************************** -// CONSTANTS -//************************************************************************** +#include "logmacro.h" -//************************************************************************** -// GLOBAL VARIABLES -//************************************************************************** - const attotime sound_manager::STREAMS_UPDATE_ATTOTIME = attotime::from_hz(STREAMS_UPDATE_FREQUENCY); +//**// Output buffer management -//************************************************************************** -// INITIALIZATION -//************************************************************************** +// Output buffers store samples produced every system-wide update. +// They give access to a window of samples produced before the update, +// and ensure that enough space is available to fit the update. -//------------------------------------------------- -// sound_stream - constructor -//------------------------------------------------- -sound_stream::sound_stream(device_t &device, int inputs, int outputs, int sample_rate, stream_update_delegate callback) - : m_device(device), - m_next(nullptr), - m_sample_rate(sample_rate), - m_new_sample_rate(0xffffffff), - m_attoseconds_per_sample(0), - m_max_samples_per_update(0), - m_input(inputs), - m_input_array(inputs), - m_resample_bufalloc(0), - m_output(outputs), - m_output_array(outputs), - m_output_bufalloc(0), - m_output_sampindex(0), - m_output_update_sampindex(0), - m_output_base_sampindex(0), - m_callback(std::move(callback)) -{ - // get the device's sound interface - device_sound_interface *sound; - if (!device.interface(sound)) - throw emu_fatalerror("Attempted to create a sound_stream with a non-sound device"); - - if(m_callback.isnull()) - m_callback = stream_update_delegate(&device_sound_interface::sound_stream_update,(device_sound_interface *)sound); +template<typename S> emu::detail::output_buffer_interleaved<S>::output_buffer_interleaved(u32 buffer_size, u32 channels) : + m_buffer(channels*buffer_size, 0), + m_sync_sample(0), + m_write_position(0), + m_sync_position(0), + m_history(0), + m_channels(channels) +{ +} - // create a unique tag for saving - std::string state_tag = string_format("%d", m_device.machine().sound().m_stream_list.size()); - m_device.machine().save().save_item(&m_device, "stream", state_tag.c_str(), 0, NAME(m_sample_rate)); - m_device.machine().save().register_postload(save_prepost_delegate(FUNC(sound_stream::postload), this)); +template<typename S> void emu::detail::output_buffer_interleaved<S>::set_buffer_size(u32 buffer_size) +{ + m_buffer.resize(m_channels*buffer_size, 0); +} - // save the gain of each input and output - for (unsigned int inputnum = 0; inputnum < m_input.size(); inputnum++) - { - m_device.machine().save().save_item(&m_device, "stream", state_tag.c_str(), inputnum, NAME(m_input[inputnum].m_gain)); - m_device.machine().save().save_item(&m_device, "stream", state_tag.c_str(), inputnum, NAME(m_input[inputnum].m_user_gain)); - } - for (unsigned int outputnum = 0; outputnum < m_output.size(); outputnum++) - { - m_output[outputnum].m_stream = this; - m_device.machine().save().save_item(&m_device, "stream", state_tag.c_str(), outputnum, NAME(m_output[outputnum].m_gain)); - } +template<typename S> void emu::detail::output_buffer_interleaved<S>::prepare_space(u32 samples) +{ + if(!m_channels) + return; - // Mark synchronous streams as such - m_synchronous = m_sample_rate == STREAM_SYNC; - if (m_synchronous) - { - m_sample_rate = 0; - m_sync_timer = m_device.machine().scheduler().timer_alloc(timer_expired_delegate(FUNC(sound_stream::sync_update), this)); + // Check if potential overflow, bring data back up front if needed + u32 buffer_size = m_buffer.size() / m_channels; + if(m_write_position + samples > buffer_size) { + u32 source_start = (m_sync_position - m_history) * m_channels; + u32 source_end = m_write_position * m_channels; + std::copy(m_buffer.begin() + source_start, m_buffer.begin() + source_end, m_buffer.begin()); + m_write_position -= m_sync_position - m_history; + m_sync_position = m_history; } - else - m_sync_timer = nullptr; - // force an update to the sample rates; this will cause everything to be recomputed - // and will generate the initial resample buffers for our inputs - recompute_sample_rate_data(); + // Clear the destination range + u32 fill_start = m_write_position * m_channels; + u32 fill_end = (m_write_position + samples) * m_channels; + std::fill(m_buffer.begin() + fill_start, m_buffer.begin() + fill_end, 0.0); +} - // set up the initial output buffer positions now that we have data - m_output_base_sampindex = -m_max_samples_per_update; +template<typename S> void emu::detail::output_buffer_interleaved<S>::commit(u32 samples) +{ + m_write_position += samples; } +template<typename S> void emu::detail::output_buffer_interleaved<S>::sync() +{ + m_sync_sample += m_write_position - m_sync_position; + m_sync_position = m_write_position; +} -//------------------------------------------------- -// sample_time - return the emulation time of the -// next sample to be generated on the stream -//------------------------------------------------- +template<typename S> emu::detail::output_buffer_flat<S>::output_buffer_flat(u32 buffer_size, u32 channels) : + m_buffer(channels), + m_sync_sample(0), + m_write_position(0), + m_sync_position(0), + m_history(0), + m_channels(channels) +{ + for(auto &b : m_buffer) + b.resize(buffer_size, 0); +} -attotime sound_stream::sample_time() const +template<typename S> void emu::detail::output_buffer_flat<S>::register_save_state(device_t &device, const char *id1, const char *id2) { - return attotime(m_device.machine().sound().last_update().seconds(), 0) + attotime(0, m_output_sampindex * m_attoseconds_per_sample); + auto &save = device.machine().save(); + + for(unsigned int i=0; i != m_buffer.size(); i++) + save.save_item(&device, id1, id2, i, NAME(m_buffer[i])); + + save.save_item(&device, id1, id2, 0, NAME(m_sync_sample)); + save.save_item(&device, id1, id2, 0, NAME(m_write_position)); + save.save_item(&device, id1, id2, 0, NAME(m_sync_position)); + save.save_item(&device, id1, id2, 0, NAME(m_history)); } +template<typename S> void emu::detail::output_buffer_flat<S>::set_buffer_size(u32 buffer_size) +{ + for(auto &b : m_buffer) + b.resize(buffer_size, 0); +} -//------------------------------------------------- -// user_gain - return the user-controllable gain -// on a given stream's input -//------------------------------------------------- +template<typename S> void emu::detail::output_buffer_flat<S>::prepare_space(u32 samples) +{ + if(!m_channels) + return; -float sound_stream::user_gain(int inputnum) const + // Check if potential overflow, bring data back up front if needed + u32 buffer_size = m_buffer[0].size(); + if(m_write_position + samples > buffer_size) { + u32 source_start = m_sync_position - m_history; + u32 source_end = m_write_position; + for(u32 channel = 0; channel != m_channels; channel++) + std::copy(m_buffer[channel].begin() + source_start, m_buffer[channel].begin() + source_end, m_buffer[channel].begin()); + m_write_position -= source_start; + m_sync_position = m_history; + } + + // Clear the destination range + u32 fill_start = m_write_position; + u32 fill_end = m_write_position + samples; + for(u32 channel = 0; channel != m_channels; channel++) + std::fill(m_buffer[channel].begin() + fill_start, m_buffer[channel].begin() + fill_end, 0.0); +} + +template<typename S> void emu::detail::output_buffer_flat<S>::commit(u32 samples) { - assert(inputnum >= 0 && inputnum < m_input.size()); - return float(m_input[inputnum].m_user_gain) / 256.0f; + m_write_position += samples; } +template<typename S> void emu::detail::output_buffer_flat<S>::sync() +{ + m_sync_sample += m_write_position - m_sync_position; + m_sync_position = m_write_position; +} -//------------------------------------------------- -// input_gain - return the input gain on a -// given stream's input -//------------------------------------------------- +template<typename S> void emu::detail::output_buffer_flat<S>::set_history(u32 history) +{ + m_history = history; + if(m_sync_position < m_history) { + u32 delta = m_history - m_sync_position; + if(m_write_position) + for(u32 channel = 0; channel != m_channels; channel++) { + std::copy_backward(m_buffer[channel].begin(), m_buffer[channel].begin() + m_write_position, m_buffer[channel].begin() + m_write_position + delta); + std::fill(m_buffer[channel].begin() + 1, m_buffer[channel].begin() + delta, m_buffer[channel][0]); + } + else + for(u32 channel = 0; channel != m_channels; channel++) + std::fill(m_buffer[channel].begin(), m_buffer[channel].begin() + m_history, 0.0); -float sound_stream::input_gain(int inputnum) const + m_write_position += delta; + m_sync_position = m_history; + } +} + +template<typename S> void emu::detail::output_buffer_flat<S>::resample(u32 previous_rate, u32 next_rate, attotime sync_time, attotime now) { - assert(inputnum >= 0 && inputnum < m_input.size()); - return float(m_input[inputnum].m_gain) / 256.0f; + if(!m_write_position) + return; + + auto si = [](attotime time, u32 rate) -> s64 { + return time.m_seconds * rate + ((time.m_attoseconds / 100000000) * rate) / 10000000000LL; + }; + + auto cv = [](u32 source_rate, u32 dest_rate, s64 time) -> std::pair<s64, double> { + s64 sec = time / source_rate; + s64 prem = time % source_rate; + double nrem = double(prem * dest_rate) / double(source_rate); + s64 cyc = s64(nrem); + return std::make_pair(sec * dest_rate + cyc, nrem - cyc); + }; + + // Compute what will be the new start, sync and write positions (if it fits) + s64 nsync = si(sync_time, next_rate); + s64 nwrite = si(now, next_rate); + s64 pbase = m_sync_sample - m_sync_position; // Beware, pbase can be negative at startup due to history size + auto [nbase, nbase_dec] = cv(previous_rate, next_rate, pbase < 0 ? 0 : pbase); + nbase += 1; + if(nbase > nsync) + nbase = nsync; + + u32 space = m_buffer[0].size(); + if(nwrite - nbase > space) { + nbase = nwrite - space; + if(nbase > nsync) + fatalerror("Stream buffer too small, can't proceed, rate change %d -> %d, space=%d\n", previous_rate, next_rate, space); + } + + auto [ppos, pdec] = cv(next_rate, previous_rate, nbase); + if(ppos < pbase || ppos > pbase + m_write_position) + fatalerror("Something went very wrong, ppos=%d, pbase=%d, pbase+wp=%d\n", ppos, pbase, pbase + m_write_position); + + double step = double(previous_rate) / double(next_rate); + u32 pindex = ppos - pbase; + u32 nend = nwrite - nbase; + + // Warning: don't try to be too clever, the m_buffer storage is + // registered in the save state system, so it must not move or + // change size + + std::vector<S> copy(m_write_position); + for(u32 channel = 0; channel != m_channels; channel++) { + std::copy(m_buffer[channel].begin(), m_buffer[channel].begin() + m_write_position, copy.begin()); + + // Interpolate the buffer contents + + for(u32 nindex = 0; nindex != nend; nindex++) { + u32 pi0 = std::clamp(pindex, 0U, m_write_position - 1); + u32 pi1 = std::clamp(pindex + 1, 0U, m_write_position - 1); + m_buffer[channel][nindex] = copy[pi0] * (1-pdec) + copy[pi1] * pdec; + + pdec += step; + if(pdec >= 1) { + int s = s32(pdec); + pindex += s; + pdec -= s; + } + } + } + + m_sync_sample = nsync; + m_sync_position = m_sync_sample - nbase; + m_write_position = nend; + + // history and the associated resizes are taken into account later } +template class emu::detail::output_buffer_flat<sound_stream::sample_t>; +template class emu::detail::output_buffer_interleaved<s16>; -//------------------------------------------------- -// output_gain - return the output gain on a -// given stream's output -//------------------------------------------------- -float sound_stream::output_gain(int outputnum) const +// Not inline because with the unique_ptr it would require audio_effect in emu.h + +sound_manager::effect_step::effect_step(u32 buffer_size, u32 channels) : m_buffer(buffer_size, channels) { - assert(outputnum >= 0 && outputnum < m_output.size()); - return float(m_output[outputnum].m_gain) / 256.0f; } -//------------------------------------------------- -// input_name - return the original input gain -// on a given stream's input -//------------------------------------------------- +//**// Streams and routes + +sound_stream::sound_stream(device_t &device, u32 inputs, u32 outputs, u32 sample_rate, stream_update_delegate callback, sound_stream_flags flags) : + m_device(device), + m_output_buffer(0, outputs), + m_sample_rate(sample_rate == SAMPLE_RATE_INPUT_ADAPTIVE || sample_rate == SAMPLE_RATE_OUTPUT_ADAPTIVE || sample_rate == SAMPLE_RATE_ADAPTIVE ? 0 : sample_rate), + m_input_count(inputs), + m_output_count(outputs), + m_input_adaptive(sample_rate == SAMPLE_RATE_INPUT_ADAPTIVE || sample_rate == SAMPLE_RATE_ADAPTIVE), + m_output_adaptive(sample_rate == SAMPLE_RATE_OUTPUT_ADAPTIVE || sample_rate == SAMPLE_RATE_ADAPTIVE), + m_synchronous((flags & STREAM_SYNCHRONOUS) != 0), + m_started(false), + m_in_update(false), + m_sync_timer(nullptr), + m_callback(std::move(callback)) +{ + sound_assert(outputs > 0 || inputs > 0); + + // create a name + m_name = m_device.name(); + m_name += " '"; + m_name += m_device.tag(); + m_name += "'"; + + // create an update timer for synchronous streams + if(synchronous()) + m_sync_timer = m_device.timer_alloc(FUNC(sound_stream::sync_update), this); + + // create the gain vectors + m_input_channel_gain.resize(m_input_count, 1.0); + m_output_channel_gain.resize(m_output_count, 1.0); + m_user_output_channel_gain.resize(m_output_count, 1.0); + m_user_output_gain = 1.0; +} + +sound_stream::~sound_stream() +{ +} -std::string sound_stream::input_name(int inputnum) const +void sound_stream::add_bw_route(sound_stream *source, int output, int input, float gain) { - std::ostringstream str; + m_bw_routes.emplace_back(route_bw(source, output, input, gain)); +} - // start with our device name and tag - assert(inputnum >= 0 && inputnum < m_input.size()); - util::stream_format(str, "%s '%s': ", m_device.name(), m_device.tag()); +void sound_stream::add_fw_route(sound_stream *target, int input, int output) +{ + m_fw_routes.emplace_back(route_fw(target, input, output)); +} - // if we have a source, indicate where the sound comes from by device name and tag - if (m_input[inputnum].m_source != nullptr && m_input[inputnum].m_source->m_stream != nullptr) - { - device_t &source = m_input[inputnum].m_source->m_stream->device(); - util::stream_format(str, "%s '%s'", source.name(), source.tag()); - - // get the sound interface; if there is more than 1 output we need to figure out which one - device_sound_interface *sound; - if (source.interface(sound) && sound->outputs() > 1) - { - // iterate over outputs until we find the stream that matches our source - // then look for a match on the output number - sound_stream *outstream; - int streamoutputnum; - for (int outputnum = 0; (outstream = sound->output_to_stream_output(outputnum, streamoutputnum)) != nullptr; outputnum++) - if (outstream == m_input[inputnum].m_source->m_stream && m_input[inputnum].m_source == &outstream->m_output[streamoutputnum]) - { - util::stream_format(str, " Ch.%d", outputnum); - break; - } +bool sound_stream::set_route_gain(sound_stream *source, int source_channel, int target_channel, float gain) +{ + for(auto &r : m_bw_routes) + if(r.m_source == source && r.m_output == source_channel && r.m_input == target_channel) { + r.m_gain = gain; + return true; } - } - return str.str(); + return false; } +std::vector<sound_stream *> sound_stream::sources() const +{ + std::vector<sound_stream *> streams; + for(const route_bw &route : m_bw_routes) { + sound_stream *stream = route.m_source; + for(const sound_stream *s : streams) + if(s == stream) + goto already; + streams.push_back(stream); + already:; + } + return streams; +} -//------------------------------------------------- -// input_source_device - return the device -// attached as a given input's source -//------------------------------------------------- +std::vector<sound_stream *> sound_stream::targets() const +{ + std::vector<sound_stream *> streams; + for(const route_fw &route : m_fw_routes) { + sound_stream *stream = route.m_target; + for(const sound_stream *s : streams) + if(s == stream) + goto already; + streams.push_back(stream); + already:; + } + return streams; +} -device_t *sound_stream::input_source_device(int inputnum) const +void sound_stream::register_state() { - assert(inputnum >= 0 && inputnum < m_input.size()); - return (m_input[inputnum].m_source != nullptr) ? &m_input[inputnum].m_source->m_stream->device() : nullptr; + // create a unique tag for saving + m_state_tag = string_format("%d", m_device.machine().sound().unique_id()); + auto &save = m_device.machine().save(); + + save.save_item(&m_device, "stream.sound_stream", m_state_tag.c_str(), 0, NAME(m_sync_time)); + save.save_item(&m_device, "stream.sound_stream", m_state_tag.c_str(), 0, NAME(m_sample_rate)); + if(m_input_count) + save.save_item(&m_device, "stream.sound_stream", m_state_tag.c_str(), 0, NAME(m_input_channel_gain)); + if(m_output_count) + save.save_item(&m_device, "stream.sound_stream", m_state_tag.c_str(), 0, NAME(m_output_channel_gain)); + // user gains go to .cfg files, not state files + + m_output_buffer.register_save_state(m_device, "stream.sound_stream.output_buffer", m_state_tag.c_str()); + + for(unsigned int i=0; i != m_bw_routes.size(); i++) + save.save_item(&m_device, "stream.sound_stream", m_state_tag.c_str(), i, m_bw_routes[i].m_gain, "route_gain"); } -//------------------------------------------------- -// input_source_device - return the output number -// attached as a given input's source -//------------------------------------------------- +void sound_stream::compute_dependants() +{ + m_dependant_streams.clear(); + for(const route_bw &r : m_bw_routes) + r.m_source->add_dependants(m_dependant_streams); +} -int sound_stream::input_source_outputnum(int inputnum) const +void sound_stream::add_dependants(std::vector<sound_stream *> &deps) { - assert(inputnum >= 0 && inputnum < m_input.size()); - return (m_input[inputnum].m_source != nullptr) ? (m_input[inputnum].m_source - &m_input[inputnum].m_source->m_stream->m_output[0]) : -1; + for(const route_bw &r : m_bw_routes) + r.m_source->add_dependants(deps); + for(sound_stream *dep : deps) + if(dep == this) + return; + deps.push_back(this); } -//------------------------------------------------- -// set_input - configure a stream's input -//------------------------------------------------- +//**// Stream sample rate -void sound_stream::set_input(int index, sound_stream *input_stream, int output_index, float gain) +void sound_stream::set_sample_rate(u32 new_rate) { - VPRINTF(("stream_set_input(%p, '%s', %d, %p, %d, %f)\n", (void *)this, m_device.tag(), - index, (void *)input_stream, output_index, (double) gain)); + m_input_adaptive = m_output_adaptive = false; + internal_set_sample_rate(new_rate); +} - // make sure it's a valid input - if (index >= m_input.size()) - fatalerror("stream_set_input attempted to configure nonexistent input %d (%d max)\n", index, int(m_input.size())); +void sound_stream::internal_set_sample_rate(u32 new_rate) +{ + if(m_started) { + update(); + m_output_buffer.resample(m_sample_rate, new_rate, m_sync_time, m_device.machine().time()); + m_sample_rate = new_rate; + for(const route_fw &r : m_fw_routes) + r.m_target->create_resamplers(); + create_resamplers(); + lookup_history_sizes(); + + } else + m_sample_rate = new_rate; +} - // make sure it's a valid output - if (input_stream != nullptr && output_index >= input_stream->m_output.size()) - fatalerror("stream_set_input attempted to use a nonexistent output %d (%d max)\n", output_index, int(m_output.size())); +bool sound_stream::try_solving_frequency() +{ + if(frequency_is_solved()) + return false; + + if(input_adaptive() && !output_adaptive()) { + u32 freq = 0; + for(const route_bw &r : m_bw_routes) { + if(!r.m_source->frequency_is_solved()) + return false; + if(freq < r.m_source->sample_rate()) + freq = r.m_source->sample_rate(); + } + m_sample_rate = freq; + return true; + + } else if(output_adaptive() && !input_adaptive()) { + u32 freq = 0; + for(const route_fw &r : m_fw_routes) { + if(!r.m_target->frequency_is_solved()) + return false; + if(freq < r.m_target->sample_rate()) + freq = r.m_target->sample_rate(); + } + m_sample_rate = freq; + return true; + + } else { + u32 freqbw = 0; + for(const route_bw &r : m_bw_routes) { + if(!r.m_source->frequency_is_solved()) { + freqbw = 0; + break; + } + if(freqbw < r.m_source->sample_rate()) + freqbw = r.m_source->sample_rate(); + } + u32 freqfw = 0; + for(const route_fw &r : m_fw_routes) { + if(!r.m_target->frequency_is_solved()) { + freqfw = 0; + break; + } + if(freqfw < r.m_target->sample_rate()) + freqfw = r.m_target->sample_rate(); + } + if(!freqbw && !freqfw) + return false; - // if this input is already wired, update the dependent info - stream_input &input = m_input[index]; - if (input.m_source != nullptr) - input.m_source->m_dependents--; + m_sample_rate = freqfw > freqbw ? freqfw : freqbw; + return true; + } +} - // wire it up - input.m_source = (input_stream != nullptr) ? &input_stream->m_output[output_index] : nullptr; - input.m_gain = int(0x100 * gain); - input.m_user_gain = 0x100; - // update the dependent info - if (input.m_source != nullptr) - input.m_source->m_dependents++; +//**// Stream flow and updates - // update sample rates now that we know the input - recompute_sample_rate_data(); +void sound_stream::init() +{ + // Ensure the buffer size is non-zero, since a stream can be started at any time + u32 bsize = m_sample_rate ? m_sample_rate : 48000; + m_input_buffer.resize(m_input_count); + for(auto &b : m_input_buffer) + b.resize(bsize); + + m_output_buffer.set_buffer_size(bsize); + + m_samples_to_update = 0; + m_started = true; + if(synchronous()) + reprime_sync_timer(); } - -//------------------------------------------------- -// update - force a stream to update to -// the current emulated time -//------------------------------------------------- +u64 sound_stream::get_current_sample_index() const +{ + attotime now = m_device.machine().time(); + return now.m_seconds * m_sample_rate + ((now.m_attoseconds / 1'000'000'000) * m_sample_rate) / 1'000'000'000; +} void sound_stream::update() { - if (!m_attoseconds_per_sample) + if(!is_active() || m_in_update) return; - // determine the number of samples since the start of this second - attotime time = m_device.machine().time(); - s32 update_sampindex = s32(time.attoseconds() / m_attoseconds_per_sample); + // Find out where we are and how much we have to do + u64 idx = get_current_sample_index(); + m_samples_to_update = idx - m_output_buffer.write_sample(); - // if we're ahead of the last update, then adjust upwards - attotime last_update = m_device.machine().sound().last_update(); - if (time.seconds() > last_update.seconds()) - { - assert(time.seconds() == last_update.seconds() + 1); - update_sampindex += m_sample_rate; - } + if(m_samples_to_update > 0) { + m_in_update = true; - // if we're behind the last update, then adjust downwards - if (time.seconds() < last_update.seconds()) - { - assert(time.seconds() == last_update.seconds() - 1); - update_sampindex -= m_sample_rate; + // If there's anything to do, well, do it, starting with the dependencies + for(auto &stream : m_dependant_streams) + stream->update_nodeps(); + + do_update(); + m_in_update = false; } + m_samples_to_update = 0; +} - if (update_sampindex <= m_output_sampindex) +void sound_stream::update_nodeps() +{ + if(!is_active() || m_in_update) return; - // generate samples to get us up to the appropriate time - g_profiler.start(PROFILER_SOUND); - assert(m_output_sampindex - m_output_base_sampindex >= 0); - assert(update_sampindex - m_output_base_sampindex <= m_output_bufalloc); - generate_samples(update_sampindex - m_output_sampindex); - g_profiler.stop(); + // Find out where we are and how much we have to do + u64 idx = get_current_sample_index(); + m_samples_to_update = idx - m_output_buffer.write_sample(); - // remember this info for next time - m_output_sampindex = update_sampindex; -} + if(m_samples_to_update > 0) { + m_in_update = true; + // If there's anything to do, well, do it + do_update(); + m_in_update = false; + } + m_samples_to_update = 0; +} -void sound_stream::sync_update(void *, s32) +void sound_stream::create_resamplers() { - update(); - attotime time = m_device.machine().time(); - attoseconds_t next_edge = m_attoseconds_per_sample - (time.attoseconds() % m_attoseconds_per_sample); - m_sync_timer->adjust(attotime(0, next_edge)); + if(!is_active()) { + for(auto &r : m_bw_routes) + r.m_resampler = nullptr; + return; + } + + for(auto &r : m_bw_routes) + if(r.m_source->is_active() && r.m_source->sample_rate() != m_sample_rate) + r.m_resampler = m_device.machine().sound().get_resampler(r.m_source->sample_rate(), m_sample_rate); + else + r.m_resampler = nullptr; } +void sound_stream::lookup_history_sizes() +{ + u32 history = 0; + for(auto &r : m_fw_routes) { + u32 h = r.m_target->get_history_for_bw_route(this, r.m_output); + if(h > history) + history = h; + } -//------------------------------------------------- -// output_since_last_update - return a pointer to -// the output buffer and the number of samples -// since the last global update -//------------------------------------------------- + m_output_buffer.set_history(history); +} -const stream_sample_t *sound_stream::output_since_last_update(int outputnum, int &numsamples) +u32 sound_stream::get_history_for_bw_route(const sound_stream *source, u32 channel) const { - // force an update on the stream - update(); - - // compute the number of samples and a pointer to the output buffer - numsamples = m_output_sampindex - m_output_update_sampindex; - return &m_output[outputnum].m_buffer[m_output_update_sampindex - m_output_base_sampindex]; + u32 history = 0; + for(auto &r : m_bw_routes) + if(r.m_source == source && r.m_output == channel && r.m_resampler) { + u32 h = r.m_resampler->history_size(); + if(h > history) + history = h; + } + return history; } +void sound_stream::do_update() +{ + // Mix in all the inputs (if any) + if(m_input_count) { + for(auto &b : m_input_buffer) + std::fill(b.begin(), b.begin() + m_samples_to_update, 0.0); + for(const auto &r : m_bw_routes) { + if(!r.m_source->is_active()) + continue; + + float gain = r.m_source->m_user_output_gain * r.m_source->m_output_channel_gain[r.m_output] * r.m_source->m_user_output_channel_gain[r.m_output] * r.m_gain * m_input_channel_gain[r.m_input]; + auto &db = m_input_buffer[r.m_input]; + if(r.m_resampler) + r.m_resampler->apply(r.m_source->m_output_buffer, db, m_output_buffer.write_sample(), r.m_output, gain, m_samples_to_update); + + else { + const sample_t *sb = r.m_source->m_output_buffer.ptrs(r.m_output, m_output_buffer.write_sample() - r.m_source->m_output_buffer.sync_sample()); + for(u32 i = 0; i != m_samples_to_update; i++) + db[i] += sb[i] * gain; + } + } + } + + // Prepare the output space (if any) + m_output_buffer.prepare_space(m_samples_to_update); -//------------------------------------------------- -// set_sample_rate - set the sample rate on a -// given stream -//------------------------------------------------- + // Call the callback + m_callback(*this); -void sound_stream::set_sample_rate(int new_rate) + // Update the indexes + m_output_buffer.commit(m_samples_to_update); +} + +void sound_stream::sync(attotime now) { - // we will update this on the next global update - if (new_rate != sample_rate()) - m_new_sample_rate = new_rate; + m_sync_time = now; + m_output_buffer.sync(); } -//------------------------------------------------- -// set_user_gain - set the user-controllable gain -// on a given stream's input -//------------------------------------------------- -void sound_stream::set_user_gain(int inputnum, float gain) + +attotime sound_stream::sample_to_time(u64 index) const { - update(); - assert(inputnum >= 0 && inputnum < m_input.size()); - m_input[inputnum].m_user_gain = int(0x100 * gain); + attotime res = attotime::zero; + res.m_seconds = index / m_sample_rate; + u64 remain = index % m_sample_rate; + res.m_attoseconds = ((remain * 1'000'000'000) / m_sample_rate) * 1'000'000'000; + return res; } -//------------------------------------------------- -// set_input_gain - set the input gain on a -// given stream's input -//------------------------------------------------- +//**// Synchronous stream updating -void sound_stream::set_input_gain(int inputnum, float gain) +void sound_stream::reprime_sync_timer() +{ + if(!is_active()) + return; + + u64 next_sample = m_output_buffer.write_sample() + 1; + attotime next_time = sample_to_time(next_sample); + next_time.m_attoseconds += 1'000'000'000; // Go to the next nanosecond ' + m_sync_timer->adjust(next_time - m_device.machine().time()); +} + +void sound_stream::sync_update(s32) { update(); - assert(inputnum >= 0 && inputnum < m_input.size()); - m_input[inputnum].m_gain = int(0x100 * gain); + reprime_sync_timer(); } -//------------------------------------------------- -// set_output_gain - set the output gain on a -// given stream's output -//------------------------------------------------- +//**// Sound manager and stream allocation +sound_manager::sound_manager(running_machine &machine) : + m_machine(machine), + m_update_timer(nullptr), + m_last_sync_time(attotime::zero), + m_effects_thread(nullptr), + m_effects_done(false), + m_master_gain(1.0), + m_muted(0), + m_nosound_mode(machine.osd().no_sound()), + m_unique_id(0), + m_wavfile(), + m_resampler_type(RESAMPLER_LOFI), + m_resampler_hq_latency(0.005), + m_resampler_hq_length(400), + m_resampler_hq_phases(200) +{ + // register callbacks + machine.configuration().config_register( + "mixer", + configuration_manager::load_delegate(&sound_manager::config_load, this), + configuration_manager::save_delegate(&sound_manager::config_save, this)); + machine.add_notifier(MACHINE_NOTIFY_PAUSE, machine_notify_delegate(&sound_manager::pause, this)); + machine.add_notifier(MACHINE_NOTIFY_RESUME, machine_notify_delegate(&sound_manager::resume, this)); + machine.add_notifier(MACHINE_NOTIFY_RESET, machine_notify_delegate(&sound_manager::reset, this)); + machine.add_notifier(MACHINE_NOTIFY_EXIT, machine_notify_delegate(&sound_manager::stop_recording, this)); + + // register global states + machine.save().save_item(NAME(m_last_sync_time)); + + // start the periodic update flushing timer + m_update_timer = machine.scheduler().timer_alloc(timer_expired_delegate(FUNC(sound_manager::update), this)); + m_update_timer->adjust(STREAMS_UPDATE_ATTOTIME, 0, STREAMS_UPDATE_ATTOTIME); + + // mark the generation as "just starting" + m_osd_info.m_generation = 0xffffffff; +} -void sound_stream::set_output_gain(int outputnum, float gain) +sound_manager::~sound_manager() { - update(); - assert(outputnum >= 0 && outputnum < m_output.size()); - m_output[outputnum].m_gain = int(0x100 * gain); + if(m_effects_thread) { + m_effects_done = true; + m_effects_condition.notify_all(); + m_effects_thread->join(); + m_effects_thread = nullptr; + } +} + +sound_stream *sound_manager::stream_alloc(device_t &device, u32 inputs, u32 outputs, u32 sample_rate, stream_update_delegate callback, sound_stream_flags flags) +{ + m_stream_list.push_back(std::make_unique<sound_stream>(device, inputs, outputs, sample_rate, callback, flags)); + return m_stream_list.back().get(); } -//------------------------------------------------- -// update_with_accounting - do a regular update, -// but also do periodic accounting -//------------------------------------------------- +//**// Sound system initialization -void sound_stream::update_with_accounting(bool second_tick) +void sound_manager::before_devices_init() { - // do the normal update - update(); + // Inform the targets of the existence of the routes + for(device_sound_interface &sound : sound_interface_enumerator(machine().root_device())) + sound.sound_before_devices_init(); - // if we've ticked over another second, adjust all the counters that are relative to - // the current second - s32 output_bufindex = m_output_sampindex - m_output_base_sampindex; - if (second_tick) - { - m_output_sampindex -= m_sample_rate; - m_output_base_sampindex -= m_sample_rate; + m_machine.save().register_postload(save_prepost_delegate(FUNC(sound_manager::postload), this)); +} + +void sound_manager::postload() +{ + std::unique_lock<std::mutex> lock(m_effects_mutex); + attotime now = machine().time(); + for(osd_output_stream &stream : m_osd_output_streams) { + stream.m_last_sync = rate_and_time_to_index(now, stream.m_rate); + stream.m_samples = 0; } +} - // note our current output sample - m_output_update_sampindex = m_output_sampindex; +void sound_manager::after_devices_init() +{ + // Link all the streams together + for(device_sound_interface &sound : sound_interface_enumerator(machine().root_device())) + sound.sound_after_devices_init(); + + // Resolve the frequencies + int need_to_solve = 0; + for(auto &stream : m_stream_list) + if(!stream->frequency_is_solved()) + need_to_solve ++; + + while(need_to_solve) { + int prev_need_to_solve = need_to_solve; + for(auto &stream : m_stream_list) + if(!stream->frequency_is_solved() && stream->try_solving_frequency()) + need_to_solve --; + if(need_to_solve == prev_need_to_solve) + break; + } - // if we don't have enough output buffer space to hold two updates' worth of samples, - // we need to shuffle things down - if (m_output_bufalloc - output_bufindex < 2 * m_max_samples_per_update) - { - s32 samples_to_lose = output_bufindex - m_max_samples_per_update; - if (samples_to_lose > 0) - { - // if we have samples to move, do so for each output - if (output_bufindex > 0) - for (auto & output : m_output) - { - memmove(&output.m_buffer[0], &output.m_buffer[samples_to_lose], sizeof(output.m_buffer[0]) * (output_bufindex - samples_to_lose)); - } + if(need_to_solve) { + u32 def = machine().sample_rate(); + for(auto &stream : m_stream_list) + if(!stream->frequency_is_solved()) + stream->internal_set_sample_rate(def); + } - // update the base position - m_output_base_sampindex += samples_to_lose; + // Have all streams create their buffers and other initializations + for(auto &stream : m_stream_list) + stream->init(); + + // Detect loops and order streams for full update at the same time + // Check the number of sources for each stream + std::map<sound_stream *, int> depcounts; + for(auto &stream : m_stream_list) + depcounts[stream.get()] = stream->sources().size(); + + // Start from all the ones that don't depend on anything + std::vector<sound_stream *> ready_streams; + for(auto &dpc : depcounts) + if(dpc.second == 0) + ready_streams.push_back(dpc.first); + + // Handle all the ready streams in a lifo matter (better for cache when generating sound) + while(!ready_streams.empty()) { + sound_stream *stream = ready_streams.back(); + // add the stream to the update order + m_ordered_streams.push_back(stream); + ready_streams.resize(ready_streams.size() - 1); + // reduce the depcount for all the streams that depend on the updated stream + for(sound_stream *target : stream->targets()) + if(!--depcounts[target]) + // when the depcount is zero, a stream is ready to be updated + ready_streams.push_back(target); + } + + // If not all streams ended up in the sorted list, we have a loop + if(m_ordered_streams.size() != m_stream_list.size()) { + // Apply the same algorithm from the other side to the + // remaining streams to only keep the ones in the loop + + std::map<sound_stream *, int> inverted_depcounts; + for(auto &dpc : depcounts) + if(dpc.second) + inverted_depcounts[dpc.first] = dpc.first->targets().size(); + for(auto &dpc : inverted_depcounts) + if(dpc.second == 0) + ready_streams.push_back(dpc.first); + while(!ready_streams.empty()) { + sound_stream *stream = ready_streams.back(); + ready_streams.resize(ready_streams.size() - 1); + for(sound_stream *source : stream->sources()) + if(!--inverted_depcounts[source]) + ready_streams.push_back(source); } + std::string stream_names; + for(auto &dpc : inverted_depcounts) + if(dpc.second) + stream_names += ' ' + dpc.first->name(); + fatalerror("Loop detected in stream routes:%s", stream_names); } -} + if(VERBOSE & LOG_ORDER) { + LOG_OUTPUT_FUNC("Order:\n"); + for(sound_stream *s : m_ordered_streams) + LOG_OUTPUT_FUNC("- %s (%d)\n", s->name().c_str(), s->sample_rate()); + } -//------------------------------------------------- -// apply_sample_rate_changes - if there is a -// pending sample rate change, apply it now -//------------------------------------------------- + // Registrations for state saving + for(auto &stream : m_stream_list) + stream->register_state(); + + // Compute all the per-stream orders for update() + for(auto &stream : m_stream_list) + stream->compute_dependants(); + + // Create the default effect chain + for(u32 effect = 0; effect != audio_effect::COUNT; effect++) + m_default_effects.emplace_back(audio_effect::create(effect, machine().sample_rate(), nullptr)); + + // Inventory speakers and microphones + m_outputs_count = 0; + for(speaker_device &dev : speaker_device_enumerator(machine().root_device())) { + dev.set_id(m_speakers.size()); + m_speakers.emplace_back(speaker_info(dev, machine().sample_rate(), m_outputs_count)); + for(u32 effect = 0; effect != audio_effect::COUNT; effect++) + m_speakers.back().m_effects[effect].m_effect.reset(audio_effect::create(effect, machine().sample_rate(), m_default_effects[effect].get())); + m_outputs_count += dev.inputs(); + } -void sound_stream::apply_sample_rate_changes() -{ - // skip if nothing to do - if (m_new_sample_rate == 0xffffffff) - return; + for(microphone_device &dev : microphone_device_enumerator(machine().root_device())) { + dev.set_id(m_microphones.size()); + m_microphones.emplace_back(microphone_info(dev)); + } - // update to the new rate and remember the old rate - u32 old_rate = m_sample_rate; - m_sample_rate = m_new_sample_rate; - m_new_sample_rate = 0xffffffff; + // Allocate the buffer to pass for recording + m_record_buffer.resize(m_outputs_count * machine().sample_rate(), 0); + m_record_samples = 0; - // recompute all the data - recompute_sample_rate_data(); + // Have all streams create their initial resamplers + for(auto &stream : m_stream_list) + stream->create_resamplers(); - // reset our sample indexes to the current time - if (old_rate) - { - m_output_sampindex = s64(m_output_sampindex) * s64(m_sample_rate) / old_rate; - m_output_update_sampindex = s64(m_output_update_sampindex) * s64(m_sample_rate) / old_rate; - } - else - { - m_output_sampindex = m_attoseconds_per_sample ? m_device.machine().sound().last_update().attoseconds() / m_attoseconds_per_sample : 0; - m_output_update_sampindex = m_output_sampindex; - } + // Then get the initial history sizes + for(auto &stream : m_stream_list) + stream->lookup_history_sizes(); - m_output_base_sampindex = m_output_sampindex - m_max_samples_per_update; + m_effects_done = false; - // clear out the buffer - if (m_max_samples_per_update) - for (auto & elem : m_output) - memset(&elem.m_buffer[0], 0, m_max_samples_per_update * sizeof(elem.m_buffer[0])); + m_effects_thread = std::make_unique<std::thread>( + [this]{ run_effects(); }); } -//------------------------------------------------- -// recompute_sample_rate_data - recompute sample -// rate data, and all streams that are affected -// by this stream -//------------------------------------------------- +//**// Effects, input and output management -void sound_stream::recompute_sample_rate_data() +void sound_manager::input_get(int id, sound_stream &stream) { - if (m_synchronous) - { - m_sample_rate = 0; - // When synchronous, pick the sample rate for the inputs, if any - for (auto & input : m_input) - { - if (input.m_source != nullptr) - { - if (!m_sample_rate) - m_sample_rate = input.m_source->m_stream->m_sample_rate; - else if (m_sample_rate != input.m_source->m_stream->m_sample_rate) - throw emu_fatalerror("Incompatible sample rates as input of a synchronous stream: %d and %d\n", m_sample_rate, input.m_source->m_stream->m_sample_rate); + u32 samples = stream.samples(); + u64 end_pos = stream.sample_index(); + u32 skip = stream.output_count(); + + for(const auto &step : m_microphones[id].m_input_mixing_steps) { + auto get_source = [&istream = m_osd_input_streams[step.m_osd_index], this](u32 samples, u64 end_pos, u32 channel) -> const s16 * { + if(istream.m_buffer.write_sample() < end_pos) { + u32 needed = end_pos - istream.m_buffer.write_sample(); + istream.m_buffer.prepare_space(needed); + machine().osd().sound_stream_source_update(istream.m_id, istream.m_buffer.ptrw(0, 0), needed); + istream.m_buffer.commit(needed); + } + return istream.m_buffer.ptrs(channel, end_pos - samples - istream.m_buffer.sync_sample()); + }; + + switch(step.m_mode) { + case mixing_step::CLEAR: + case mixing_step::COPY: + fatalerror("Impossible step encountered in input\n"); + + case mixing_step::ADD: { + const s16 *src = get_source(samples, end_pos, step.m_osd_channel); + float gain = step.m_linear_volume / 32768.0; + for(u32 sample = 0; sample != samples; sample++) { + stream.add(step.m_device_channel, sample, *src * gain); + src += skip; } + break; + } } } +} +void sound_manager::output_push(int id, sound_stream &stream) +{ + auto &spk = m_speakers[id]; + auto &out = spk.m_buffer; + auto &inp = stream.m_input_buffer; + int samples = stream.samples(); + int channels = stream.input_count(); + out.prepare_space(samples); + for(int channel = 0; channel != channels; channel ++) + std::copy(inp[channel].begin(), inp[channel].begin() + samples, out.ptrw(channel, 0)); + out.commit(samples); + + m_record_samples = samples; + s16 *outb = m_record_buffer.data() + spk.m_first_output; + for(int channel = 0; channel != channels; channel ++) { + s16 *outb1 = outb; + const float *inb = inp[channel].data(); + for(int sample = 0; sample != samples; sample++) { + *outb1 = std::clamp(int(*inb++ * 32768), -32768, 32767); + outb1 += m_outputs_count; + } + } +} - // recompute the timing parameters - attoseconds_t update_attoseconds = m_device.machine().sound().update_attoseconds(); +void sound_manager::run_effects() +{ + std::unique_lock<std::mutex> lock(m_effects_mutex); + for(;;) { + m_effects_condition.wait(lock); + if(m_effects_done) + return; + + // Apply the effects + for(auto &si : m_speakers) + for(u32 i=0; i != si.m_effects.size(); i++) { + auto &source = i ? si.m_effects[i-1].m_buffer : si.m_buffer; + si.m_effects[i].m_effect->apply(source, si.m_effects[i].m_buffer); + source.sync(); + } - if (m_sample_rate) - { - m_attoseconds_per_sample = ATTOSECONDS_PER_SECOND / m_sample_rate; - m_max_samples_per_update = (update_attoseconds + m_attoseconds_per_sample - 1) / m_attoseconds_per_sample; - } - else - { - m_attoseconds_per_sample = 0; - m_max_samples_per_update = 0; - } + // Apply the mixing steps + for(const auto &step : m_output_mixing_steps) { + const sample_t *src = step.m_mode == mixing_step::CLEAR ? nullptr : m_speakers[step.m_device_index].m_effects.back().m_buffer.ptrs(step.m_device_channel, 0); - // update resample and output buffer sizes - allocate_resample_buffers(); - allocate_output_buffers(); + auto &ostream = m_osd_output_streams[step.m_osd_index]; + u32 samples = ostream.m_samples; + s16 *dest = ostream.m_buffer.data() + step.m_osd_channel; + u32 skip = ostream.m_channels; - // iterate over each input - for (auto & input : m_input) - { - // if we have a source, see if its sample rate changed - - if (input.m_source != nullptr && input.m_source->m_stream->m_sample_rate) - { - // okay, we have a new sample rate; recompute the latency to be the maximum - // sample period between us and our input - attoseconds_t new_attosecs_per_sample = ATTOSECONDS_PER_SECOND / input.m_source->m_stream->m_sample_rate; - attoseconds_t latency = std::max(new_attosecs_per_sample, m_attoseconds_per_sample); - - // if the input stream's sample rate is lower, we will use linear interpolation - // this requires an extra sample from the source - if (input.m_source->m_stream->m_sample_rate < m_sample_rate) - latency += new_attosecs_per_sample; - - // if our sample rates match exactly, we don't need any latency - else if (input.m_source->m_stream->m_sample_rate == m_sample_rate) - latency = 0; - - // we generally don't want to tweak the latency, so we just keep the greatest - // one we've computed thus far - input.m_latency_attoseconds = std::max(input.m_latency_attoseconds, latency); - assert(input.m_latency_attoseconds < update_attoseconds); - } - else - { - input.m_latency_attoseconds = 0; - } - } + switch(step.m_mode) { + case mixing_step::CLEAR: + for(u32 sample = 0; sample != samples; sample++) { + *dest = 0; + dest += skip; + } + break; - // If synchronous, prime the timer - if (m_synchronous) - { - attotime time = m_device.machine().time(); - if (m_attoseconds_per_sample) - { - attoseconds_t next_edge = m_attoseconds_per_sample - (time.attoseconds() % m_attoseconds_per_sample); - m_sync_timer->adjust(attotime(0, next_edge)); + case mixing_step::COPY: { + float gain = 32768 * step.m_linear_volume * m_master_gain; + for(u32 sample = 0; sample != samples; sample++) { + *dest = std::clamp(int(*src++ * gain), -32768, 32767); + dest += skip; + } + break; + } + + case mixing_step::ADD: { + float gain = 32768 * step.m_linear_volume * m_master_gain; + for(u32 sample = 0; sample != samples; sample++) { + *dest = std::clamp(int(*src++ * gain) + *dest, -32768, 32767); + dest += skip; + } + break; + } + } } - else - m_sync_timer->adjust(attotime::never); + + for(auto &si : m_speakers) + si.m_effects.back().m_buffer.sync(); + + // Send the result to the osd + for(auto &stream : m_osd_output_streams) + if(stream.m_samples) + machine().osd().sound_stream_sink_update(stream.m_id, stream.m_buffer.data(), stream.m_samples); } } +std::string sound_manager::effect_chain_tag(s32 index) const +{ + return m_speakers[index].m_dev.tag(); +} -//------------------------------------------------- -// allocate_resample_buffers - recompute the -// resample buffer sizes and expand if necessary -//------------------------------------------------- +std::vector<audio_effect *> sound_manager::effect_chain(s32 index) const +{ + std::vector<audio_effect *> res; + for(const auto &e : m_speakers[index].m_effects) + res.push_back(e.m_effect.get()); + return res; +} -void sound_stream::allocate_resample_buffers() +std::vector<audio_effect *> sound_manager::default_effect_chain() const { - // compute the target number of samples - s32 bufsize = 2 * m_max_samples_per_update; + std::vector<audio_effect *> res; + for(const auto &e : m_default_effects) + res.push_back(e.get()); + return res; +} - // if we don't have enough room, allocate more - if (m_resample_bufalloc < bufsize) - { - // this becomes the new allocation size - m_resample_bufalloc = bufsize; - - // iterate over outputs and realloc their buffers - for (auto & elem : m_input) { - unsigned int old_size = elem.m_resample.size(); - elem.m_resample.resize(m_resample_bufalloc); - memset(&elem.m_resample[old_size], 0, (m_resample_bufalloc - old_size)*sizeof(elem.m_resample[0])); - } - } +void sound_manager::default_effect_changed(u32 entry) +{ + u32 type = m_default_effects[entry]->type(); + for(const auto &s : m_speakers) + for(const auto &e : s.m_effects) + if(e.m_effect->type() == type) + e.m_effect->default_changed(); } + + //------------------------------------------------- -// allocate_output_buffers - recompute the -// output buffer sizes and expand if necessary +// start_recording - begin audio recording //------------------------------------------------- -void sound_stream::allocate_output_buffers() +bool sound_manager::start_recording(std::string_view filename) { - // if we don't have enough room, allocate more - s32 bufsize = OUTPUT_BUFFER_UPDATES * m_max_samples_per_update; - if (m_output_bufalloc < bufsize) - { - // this becomes the new allocation size - m_output_bufalloc = bufsize; - - // iterate over outputs and realloc their buffers - for (auto & elem : m_output) { - unsigned int old_size = elem.m_buffer.size(); - elem.m_buffer.resize(m_output_bufalloc); - memset(&elem.m_buffer[old_size], 0, (m_output_bufalloc - old_size)*sizeof(elem.m_buffer[0])); - } - } + if(m_wavfile) + return false; + m_wavfile = util::wav_open(filename, machine().sample_rate(), m_outputs_count); + return bool(m_wavfile); +} + +bool sound_manager::start_recording() +{ + // open the output WAV file if specified + char const *const filename = machine().options().wav_write(); + return *filename ? start_recording(filename) : false; } //------------------------------------------------- -// postload - save/restore callback +// stop_recording - end audio recording //------------------------------------------------- -void sound_stream::postload() +void sound_manager::stop_recording() { - // recompute the same rate information - recompute_sample_rate_data(); - - // make sure our output buffers are fully cleared - for (auto & elem : m_output) - memset(&elem.m_buffer[0], 0, m_output_bufalloc * sizeof(elem.m_buffer[0])); - - // recompute the sample indexes to make sense - m_output_sampindex = m_attoseconds_per_sample ? m_device.machine().sound().last_update().attoseconds() / m_attoseconds_per_sample : 0; - m_output_update_sampindex = m_output_sampindex; - m_output_base_sampindex = m_output_sampindex - m_max_samples_per_update; + // close any open WAV file + m_wavfile.reset(); } //------------------------------------------------- -// generate_samples - generate the requested -// number of samples for a stream, making sure -// all inputs have the appropriate number of -// samples generated +// mute - mute sound output //------------------------------------------------- -void sound_stream::generate_samples(int samples) +void sound_manager::mute(bool mute, u8 reason) { - stream_sample_t **inputs = nullptr; - stream_sample_t **outputs = nullptr; + if(mute) + m_muted |= reason; + else + m_muted &= ~reason; +} - VPRINTF(("generate_samples(%p, %d)\n", (void *) this, samples)); - assert(samples > 0); - // ensure all inputs are up to date and generate resampled data - for (unsigned int inputnum = 0; inputnum < m_input.size(); inputnum++) - { - // update the stream to the current time - stream_input &input = m_input[inputnum]; - if (input.m_source != nullptr) - input.m_source->m_stream->update(); +//------------------------------------------------- +// reset - reset all sound chips +//------------------------------------------------- - // generate the resampled data - m_input_array[inputnum] = generate_resampled_data(input, samples); - } +sound_manager::speaker_info::speaker_info(speaker_device &dev, u32 rate, u32 first_output) : m_dev(dev), m_first_output(first_output), m_buffer(rate, dev.inputs()) +{ + m_channels = dev.inputs(); + m_stream = dev.stream(); + for(u32 i=0; i != audio_effect::COUNT; i++) + m_effects.emplace_back(effect_step(rate, dev.inputs())); +} - if (!m_input.empty()) - { - inputs = &m_input_array[0]; - } +sound_manager::microphone_info::microphone_info(microphone_device &dev) : m_dev(dev) +{ + m_channels = dev.outputs(); +} - // loop over all outputs and compute the output pointer - for (unsigned int outputnum = 0; outputnum < m_output.size(); outputnum++) - { - stream_output &output = m_output[outputnum]; - m_output_array[outputnum] = &output.m_buffer[m_output_sampindex - m_output_base_sampindex]; - } +void sound_manager::reset() +{ + LOG_OUTPUT_FUNC("Sound reset\n"); +} - if (!m_output.empty()) - { - outputs = &m_output_array[0]; - } - // run the callback - VPRINTF((" callback(%p, %d)\n", (void *)this, samples)); - m_callback(*this, inputs, outputs, samples); - VPRINTF((" callback done\n")); +//------------------------------------------------- +// pause - pause sound output +//------------------------------------------------- + +void sound_manager::pause() +{ + mute(true, MUTE_REASON_PAUSE); } //------------------------------------------------- -// generate_resampled_data - generate the -// resample buffer for a given input +// resume - resume sound output //------------------------------------------------- -stream_sample_t *sound_stream::generate_resampled_data(stream_input &input, u32 numsamples) +void sound_manager::resume() { - // if we don't have an output to pull data from, generate silence - stream_sample_t *dest = &input.m_resample[0]; - if (input.m_source == nullptr || input.m_source->m_stream->m_attoseconds_per_sample == 0) - { - memset(dest, 0, numsamples * sizeof(*dest)); - return &input.m_resample[0]; - } - - // grab data from the output - stream_output &output = *input.m_source; - sound_stream &input_stream = *output.m_stream; - s64 gain = (input.m_gain * input.m_user_gain * output.m_gain) >> 16; + mute(false, MUTE_REASON_PAUSE); +} - // determine the time at which the current sample begins, accounting for the - // latency we calculated between the input and output streams - attoseconds_t basetime = m_output_sampindex * m_attoseconds_per_sample - input.m_latency_attoseconds; - // now convert that time into a sample in the input stream - s32 basesample; - if (basetime >= 0) - basesample = basetime / input_stream.m_attoseconds_per_sample; - else - basesample = -(-basetime / input_stream.m_attoseconds_per_sample) - 1; +//**// Configuration management - // compute a source pointer to the first sample - assert(basesample >= input_stream.m_output_base_sampindex); - stream_sample_t *source = &output.m_buffer[basesample - input_stream.m_output_base_sampindex]; +void sound_manager::config_load(config_type cfg_type, config_level cfg_level, util::xml::data_node const *parentnode) +{ + // If no config file, ignore + if(!parentnode) + return; - // determine the current fraction of a sample, expressed as a fraction of FRAC_ONE - // (Note: this formula is valid as long as input_stream.m_attoseconds_per_sample significantly exceeds FRAC_ONE > attoseconds = 4.2E-12 s) - u32 basefrac = (basetime - basesample * input_stream.m_attoseconds_per_sample) / ((input_stream.m_attoseconds_per_sample + FRAC_ONE - 1) >> FRAC_BITS); - assert(basefrac < FRAC_ONE); + switch(cfg_type) { + case config_type::INIT: + break; - // compute the stepping fraction - u32 step = (u64(input_stream.m_sample_rate) << FRAC_BITS) / m_sample_rate; + case config_type::CONTROLLER: + break; - // if we have equal sample rates, we just need to copy - if (step == FRAC_ONE) - { - while (numsamples--) - { - // compute the sample - s64 sample = *source++; - *dest++ = (sample * gain) >> 8; - } - } + case config_type::DEFAULT: { + // In the global config, get the default effect chain configuration - // input is undersampled: point sample except where our sample period covers a boundary - else if (step < FRAC_ONE) - { - while (numsamples != 0) - { - // fill in with point samples until we hit a boundary - int nextfrac; - while ((nextfrac = basefrac + step) < FRAC_ONE && numsamples--) - { - *dest++ = (source[0] * gain) >> 8; - basefrac = nextfrac; + util::xml::data_node const *efl_node = parentnode->get_child("default_audio_effects"); + if(efl_node) { + for(util::xml::data_node const *ef_node = efl_node->get_child("effect"); ef_node != nullptr; ef_node = ef_node->get_next_sibling("effect")) { + unsigned int id = ef_node->get_attribute_int("step", 0); + std::string type = ef_node->get_attribute_string("type", ""); + if(id >= 1 && id <= m_default_effects.size() && audio_effect::effect_names[m_default_effects[id-1]->type()] == type) + m_default_effects[id-1]->config_load(ef_node); } + } - // if we're done, we're done - if (s32(numsamples--) < 0) - break; + // and the resampler configuration + util::xml::data_node const *rs_node = parentnode->get_child("resampler"); + if(rs_node) { + m_resampler_type = rs_node->get_attribute_int("type", RESAMPLER_LOFI); + m_resampler_hq_latency = rs_node->get_attribute_float("hq_latency", 0.0050); + m_resampler_hq_length = rs_node->get_attribute_int("hq_length", 400); + m_resampler_hq_phases = rs_node->get_attribute_int("hq_phases", 200); + } + break; + } - // compute starting and ending fractional positions - int startfrac = basefrac >> (FRAC_BITS - 12); - int endfrac = nextfrac >> (FRAC_BITS - 12); + case config_type::SYSTEM: { + // In the per-driver file, get the specific configuration for everything + + // Effects configuration + for(util::xml::data_node const *efl_node = parentnode->get_child("audio_effects"); efl_node != nullptr; efl_node = efl_node->get_next_sibling("audio_effects")) { + std::string speaker_tag = efl_node->get_attribute_string("tag", ""); + for(auto &speaker : m_speakers) + if(speaker.m_dev.tag() == speaker_tag) { + auto &eff = speaker.m_effects; + for(util::xml::data_node const *ef_node = efl_node->get_child("effect"); ef_node != nullptr; ef_node = ef_node->get_next_sibling("effect")) { + unsigned int id = ef_node->get_attribute_int("step", 0); + std::string type = ef_node->get_attribute_string("type", ""); + if(id >= 1 && id <= m_default_effects.size() && audio_effect::effect_names[eff[id-1].m_effect->type()] == type) + eff[id-1].m_effect->config_load(ef_node); + } + break; + } + } - // blend between the two samples accordingly - s64 sample = (s64(source[0]) * (0x1000 - startfrac) + s64(source[1]) * (endfrac - 0x1000)) / (endfrac - startfrac); - *dest++ = (sample * gain) >> 8; + // All levels + const util::xml::data_node *lv_node = parentnode->get_child("master_volume"); + if(lv_node) + m_master_gain = lv_node->get_attribute_float("gain", 1.0); - // advance - basefrac = nextfrac & FRAC_MASK; - source++; + for(lv_node = parentnode->get_child("device_volume"); lv_node != nullptr; lv_node = lv_node->get_next_sibling("device_volume")) { + std::string device_tag = lv_node->get_attribute_string("device", ""); + device_sound_interface *intf = dynamic_cast<device_sound_interface *>(m_machine.root_device().subdevice(device_tag)); + if(intf) + intf->set_user_output_gain(lv_node->get_attribute_float("gain", 1.0)); } - } - // input is oversampled: sum the energy - else - { - // use 8 bits to allow some extra headroom - int smallstep = step >> (FRAC_BITS - 8); - while (numsamples--) - { - s64 remainder = smallstep; - int tpos = 0; - - // compute the sample - s64 scale = (FRAC_ONE - basefrac) >> (FRAC_BITS - 8); - s64 sample = s64(source[tpos++]) * scale; - remainder -= scale; - while (remainder > 0x100) - { - sample += s64(source[tpos++]) * s64(0x100); - remainder -= 0x100; - } - sample += s64(source[tpos]) * remainder; - sample /= smallstep; + for(lv_node = parentnode->get_child("device_channel_volume"); lv_node != nullptr; lv_node = lv_node->get_next_sibling("device_channel_volume")) { + std::string device_tag = lv_node->get_attribute_string("device", ""); + int channel = lv_node->get_attribute_int("channel", -1); + device_sound_interface *intf = dynamic_cast<device_sound_interface *>(m_machine.root_device().subdevice(device_tag)); + if(intf && channel >= 0 && channel < intf->outputs()) + intf->set_user_output_gain(channel, lv_node->get_attribute_float("gain", 1.0)); + } - *dest++ = (sample * gain) >> 8; - // advance - basefrac += step; - source += basefrac >> FRAC_BITS; - basefrac &= FRAC_MASK; + // Mapping configuration + m_configs.clear(); + for(util::xml::data_node const *node = parentnode->get_child("sound_map"); node != nullptr; node = node->get_next_sibling("sound_map")) { + m_configs.emplace_back(config_mapping { node->get_attribute_string("tag", "") }); + auto &config = m_configs.back(); + for(util::xml::data_node const *nmap = node->get_child("node_mapping"); nmap != nullptr; nmap = nmap->get_next_sibling("node_mapping")) + config.m_node_mappings.emplace_back(std::pair<std::string, float>(nmap->get_attribute_string("node", ""), nmap->get_attribute_float("db", 0))); + for(util::xml::data_node const *cmap = node->get_child("channel_mapping"); cmap != nullptr; cmap = cmap->get_next_sibling("channel_mapping")) + config.m_channel_mappings.emplace_back(std::tuple<u32, std::string, u32, float>(cmap->get_attribute_int("guest_channel", 0), + cmap->get_attribute_string("node", ""), + cmap->get_attribute_int("node_channel", 0), + cmap->get_attribute_float("db", 0))); } + break; } - return &input.m_resample[0]; + case config_type::FINAL: + break; + } } - -//************************************************************************** -// STREAM INPUT -//************************************************************************** - //------------------------------------------------- -// stream_input - constructor +// config_save - save data to the configuration +// file //------------------------------------------------- -sound_stream::stream_input::stream_input() - : m_source(nullptr), - m_latency_attoseconds(0), - m_gain(0x100), - m_user_gain(0x100) +void sound_manager::config_save(config_type cfg_type, util::xml::data_node *parentnode) { -} - + switch(cfg_type) { + case config_type::INIT: + break; + + case config_type::CONTROLLER: + break; + + case config_type::DEFAULT: { + // In the global config, save the default effect chain configuration + util::xml::data_node *const efl_node = parentnode->add_child("default_audio_effects", nullptr); + for(u32 ei = 0; ei != m_default_effects.size(); ei++) { + const audio_effect *e = m_default_effects[ei].get(); + util::xml::data_node *const ef_node = efl_node->add_child("effect", nullptr); + ef_node->set_attribute_int("step", ei+1); + ef_node->set_attribute("type", audio_effect::effect_names[e->type()]); + e->config_save(ef_node); + } + util::xml::data_node *const rs_node = parentnode->add_child("resampler", nullptr); + rs_node->set_attribute_int("type", m_resampler_type); + rs_node->set_attribute_float("hq_latency", m_resampler_hq_latency); + rs_node->set_attribute_int("hq_length", m_resampler_hq_length); + rs_node->set_attribute_int("hq_phases", m_resampler_hq_phases); + break; + } -//************************************************************************** -// STREAM OUTPUT -//************************************************************************** + case config_type::SYSTEM: { + // In the per-driver file, save the specific configuration for everything + + // Effects configuration + for(const auto &speaker : m_speakers) { + util::xml::data_node *const efl_node = parentnode->add_child("audio_effects", nullptr); + efl_node->set_attribute("tag", speaker.m_dev.tag()); + for(u32 ei = 0; ei != speaker.m_effects.size(); ei++) { + const audio_effect *e = speaker.m_effects[ei].m_effect.get(); + util::xml::data_node *const ef_node = efl_node->add_child("effect", nullptr); + ef_node->set_attribute_int("step", ei+1); + ef_node->set_attribute("type", audio_effect::effect_names[e->type()]); + e->config_save(ef_node); + } + } -//------------------------------------------------- -// stream_output - constructor -//------------------------------------------------- + // All levels + if(m_master_gain != 1.0) { + util::xml::data_node *const lv_node = parentnode->add_child("master_volume", nullptr); + lv_node->set_attribute_float("gain", m_master_gain); + } + for(device_sound_interface &snd : sound_interface_enumerator(m_machine.root_device())) { + // Don't add microphones, speakers or devices without outputs + if(dynamic_cast<sound_io_device *>(&snd) || !snd.outputs()) + continue; + if(snd.user_output_gain() != 1.0) { + util::xml::data_node *const lv_node = parentnode->add_child("device_volume", nullptr); + lv_node->set_attribute("device", snd.device().tag()); + lv_node->set_attribute_float("gain", snd.user_output_gain()); + } + for(int channel = 0; channel != snd.outputs(); channel ++) + if(snd.user_output_gain(channel) != 1.0) { + util::xml::data_node *const lv_node = parentnode->add_child("device_channel_volume", nullptr); + lv_node->set_attribute("device", snd.device().tag()); + lv_node->set_attribute_int("channel", channel); + lv_node->set_attribute_float("gain", snd.user_output_gain(channel)); + } + } -sound_stream::stream_output::stream_output() - : m_stream(nullptr), - m_dependents(0), - m_gain(0x100) -{ -} + // Mapping configuration + auto output_one = [this, parentnode](sound_io_device &dev) { + for(const auto &config : m_configs) + if(config.m_name == dev.tag()) { + util::xml::data_node *const sp_node = parentnode->add_child("sound_map", nullptr); + sp_node->set_attribute("tag", dev.tag()); + for(const auto &nmap : config.m_node_mappings) { + util::xml::data_node *const node = sp_node->add_child("node_mapping", nullptr); + node->set_attribute("node", nmap.first.c_str()); + node->set_attribute_float("db", nmap.second); + } + for(const auto &cmap : config.m_channel_mappings) { + util::xml::data_node *const node = sp_node->add_child("channel_mapping", nullptr); + node->set_attribute_int("guest_channel", std::get<0>(cmap)); + node->set_attribute("node", std::get<1>(cmap).c_str()); + node->set_attribute_int("node_channel", std::get<2>(cmap)); + node->set_attribute_float("db", std::get<3>(cmap)); + } + return; + } + }; + for(auto &spk : m_speakers) + output_one(spk.m_dev); + for(auto &mic : m_microphones) + output_one(mic.m_dev); + break; + } + case config_type::FINAL: + break; + } +} -//************************************************************************** -// SOUND MANAGER -//************************************************************************** -//------------------------------------------------- -// sound_manager - constructor -//------------------------------------------------- -sound_manager::sound_manager(running_machine &machine) - : m_machine(machine), - m_update_timer(nullptr), - m_finalmix_leftover(0), - m_finalmix(machine.sample_rate()), - m_leftmix(machine.sample_rate()), - m_rightmix(machine.sample_rate()), - m_samples_this_update(0), - m_muted(0), - m_attenuation(0), - m_nosound_mode(machine.osd().no_sound()), - m_wavfile(nullptr), - m_update_attoseconds(STREAMS_UPDATE_ATTOTIME.attoseconds()), - m_last_update(attotime::zero) -{ - // get filename for WAV file or AVI file if specified - const char *wavfile = machine.options().wav_write(); - const char *avifile = machine.options().avi_write(); - - // handle -nosound and lower sample rate if not recording WAV or AVI - if (m_nosound_mode && wavfile[0] == 0 && avifile[0] == 0) - machine.m_sample_rate = 11025; - - // count the mixers -#if VERBOSE - mixer_interface_iterator iter(machine.root_device()); - VPRINTF(("total mixers = %d\n", iter.count())); -#endif +//**// Mapping between speakers/microphones and OSD endpoints - // register callbacks - machine.configuration().config_register("mixer", config_load_delegate(&sound_manager::config_load, this), config_save_delegate(&sound_manager::config_save, this)); - machine.add_notifier(MACHINE_NOTIFY_PAUSE, machine_notify_delegate(&sound_manager::pause, this)); - machine.add_notifier(MACHINE_NOTIFY_RESUME, machine_notify_delegate(&sound_manager::resume, this)); - machine.add_notifier(MACHINE_NOTIFY_RESET, machine_notify_delegate(&sound_manager::reset, this)); - machine.add_notifier(MACHINE_NOTIFY_EXIT, machine_notify_delegate(&sound_manager::stop_recording, this)); +sound_manager::config_mapping &sound_manager::config_get_sound_io(sound_io_device *dev) +{ + for(auto &config : m_configs) + if(config.m_name == dev->tag()) + return config; + m_configs.emplace_back(config_mapping { dev->tag() }); + return m_configs.back(); +} - // register global states - machine.save().save_item(NAME(m_last_update)); +void sound_manager::config_add_sound_io_connection_node(sound_io_device *dev, std::string name, float db) +{ + internal_config_add_sound_io_connection_node(dev, name, db); + m_osd_info.m_generation --; +} - // set the starting attenuation - set_attenuation(machine.options().volume()); +void sound_manager::internal_config_add_sound_io_connection_node(sound_io_device *dev, std::string name, float db) +{ + auto &config = config_get_sound_io(dev); + for(auto &nmap : config.m_node_mappings) + if(nmap.first == name) + return; + config.m_node_mappings.emplace_back(std::pair<std::string, float>(name, db)); +} - // start the periodic update flushing timer - m_update_timer = machine.scheduler().timer_alloc(timer_expired_delegate(FUNC(sound_manager::update), this)); - m_update_timer->adjust(STREAMS_UPDATE_ATTOTIME, 0, STREAMS_UPDATE_ATTOTIME); +void sound_manager::config_add_sound_io_connection_default(sound_io_device *dev, float db) +{ + internal_config_add_sound_io_connection_default(dev, db); + m_osd_info.m_generation --; } +void sound_manager::internal_config_add_sound_io_connection_default(sound_io_device *dev, float db) +{ + auto &config = config_get_sound_io(dev); + for(auto &nmap : config.m_node_mappings) + if(nmap.first == "") + return; + config.m_node_mappings.emplace_back(std::pair<std::string, float>("", db)); +} -//------------------------------------------------- -// sound_manager - destructor -//------------------------------------------------- +void sound_manager::config_remove_sound_io_connection_node(sound_io_device *dev, std::string name) +{ + internal_config_remove_sound_io_connection_node(dev, name); + m_osd_info.m_generation --; +} -sound_manager::~sound_manager() +void sound_manager::internal_config_remove_sound_io_connection_node(sound_io_device *dev, std::string name) { + auto &config = config_get_sound_io(dev); + for(auto i = config.m_node_mappings.begin(); i != config.m_node_mappings.end(); i++) + if(i->first == name) { + config.m_node_mappings.erase(i); + return; + } } +void sound_manager::config_remove_sound_io_connection_default(sound_io_device *dev) +{ + internal_config_remove_sound_io_connection_default(dev); + m_osd_info.m_generation --; +} -//------------------------------------------------- -// start_recording - begin audio recording -//------------------------------------------------- +void sound_manager::internal_config_remove_sound_io_connection_default(sound_io_device *dev) +{ + auto &config = config_get_sound_io(dev); + for(auto i = config.m_node_mappings.begin(); i != config.m_node_mappings.end(); i++) + if(i->first == "") { + config.m_node_mappings.erase(i); + return; + } +} -void sound_manager::start_recording() +void sound_manager::config_set_volume_sound_io_connection_node(sound_io_device *dev, std::string name, float db) { - // open the output WAV file if specified - const char *wavfile = machine().options().wav_write(); - if (wavfile[0] != 0 && m_wavfile == nullptr) - m_wavfile = wav_open(wavfile, machine().sample_rate(), 2); + internal_config_set_volume_sound_io_connection_node(dev, name, db); + m_osd_info.m_generation --; } +void sound_manager::internal_config_set_volume_sound_io_connection_node(sound_io_device *dev, std::string name, float db) +{ + auto &config = config_get_sound_io(dev); + for(auto &nmap : config.m_node_mappings) + if(nmap.first == name) { + nmap.second = db; + return; + } +} -//------------------------------------------------- -// stop_recording - end audio recording -//------------------------------------------------- +void sound_manager::config_set_volume_sound_io_connection_default(sound_io_device *dev, float db) +{ + internal_config_set_volume_sound_io_connection_default(dev, db); + m_osd_info.m_generation --; +} -void sound_manager::stop_recording() +void sound_manager::internal_config_set_volume_sound_io_connection_default(sound_io_device *dev, float db) { - // close any open WAV file - if (m_wavfile != nullptr) - wav_close(m_wavfile); - m_wavfile = nullptr; + auto &config = config_get_sound_io(dev); + for(auto &nmap : config.m_node_mappings) + if(nmap.first == "") { + nmap.second = db; + return; + } } -//------------------------------------------------- -// stream_alloc - allocate a new stream -//------------------------------------------------- +void sound_manager::config_add_sound_io_channel_connection_node(sound_io_device *dev, u32 guest_channel, std::string name, u32 node_channel, float db) +{ + internal_config_add_sound_io_channel_connection_node(dev, guest_channel, name, node_channel, db); + m_osd_info.m_generation --; +} -sound_stream *sound_manager::stream_alloc(device_t &device, int inputs, int outputs, int sample_rate, stream_update_delegate callback) +void sound_manager::internal_config_add_sound_io_channel_connection_node(sound_io_device *dev, u32 guest_channel, std::string name, u32 node_channel, float db) { - m_stream_list.push_back(std::make_unique<sound_stream>(device, inputs, outputs, sample_rate, callback)); - return m_stream_list.back().get(); + auto &config = config_get_sound_io(dev); + for(auto &cmap : config.m_channel_mappings) + if(std::get<0>(cmap) == guest_channel && std::get<1>(cmap) == name && std::get<2>(cmap) == node_channel) + return; + config.m_channel_mappings.emplace_back(std::tuple<u32, std::string, u32, float>(guest_channel, name, node_channel, db)); } +void sound_manager::config_add_sound_io_channel_connection_default(sound_io_device *dev, u32 guest_channel, u32 node_channel, float db) +{ + internal_config_add_sound_io_channel_connection_default(dev, guest_channel, node_channel, db); + m_osd_info.m_generation --; +} -//------------------------------------------------- -// set_attenuation - set the global volume -//------------------------------------------------- +void sound_manager::internal_config_add_sound_io_channel_connection_default(sound_io_device *dev, u32 guest_channel, u32 node_channel, float db) +{ + auto &config = config_get_sound_io(dev); + for(auto &cmap : config.m_channel_mappings) + if(std::get<0>(cmap) == guest_channel && std::get<1>(cmap) == "" && std::get<2>(cmap) == node_channel) + return; + config.m_channel_mappings.emplace_back(std::tuple<u32, std::string, u32, float>(guest_channel, "", node_channel, db)); +} -void sound_manager::set_attenuation(int attenuation) +void sound_manager::config_remove_sound_io_channel_connection_node(sound_io_device *dev, u32 guest_channel, std::string name, u32 node_channel) { - m_attenuation = attenuation; - machine().osd().set_mastervolume(m_muted ? -32 : m_attenuation); + internal_config_remove_sound_io_channel_connection_node(dev, guest_channel, name, node_channel); + m_osd_info.m_generation --; } +void sound_manager::internal_config_remove_sound_io_channel_connection_node(sound_io_device *dev, u32 guest_channel, std::string name, u32 node_channel) +{ + auto &config = config_get_sound_io(dev); + for(auto i = config.m_channel_mappings.begin(); i != config.m_channel_mappings.end(); i++) + if(std::get<0>(*i) == guest_channel && std::get<1>(*i) == name && std::get<2>(*i) == node_channel) { + config.m_channel_mappings.erase(i); + return; + } +} -//------------------------------------------------- -// indexed_mixer_input - return the mixer -// device and input index of the global mixer -// input -//------------------------------------------------- +void sound_manager::config_remove_sound_io_channel_connection_default(sound_io_device *dev, u32 guest_channel, u32 node_channel) +{ + internal_config_remove_sound_io_channel_connection_default(dev, guest_channel, node_channel); + m_osd_info.m_generation --; +} -bool sound_manager::indexed_mixer_input(int index, mixer_input &info) const +void sound_manager::internal_config_remove_sound_io_channel_connection_default(sound_io_device *dev, u32 guest_channel, u32 node_channel) { - // scan through the mixers until we find the indexed input - for (device_mixer_interface &mixer : mixer_interface_iterator(machine().root_device())) - { - if (index < mixer.inputs()) - { - info.mixer = &mixer; - info.stream = mixer.input_to_stream_input(index, info.inputnum); - assert(info.stream != nullptr); - return true; + auto &config = config_get_sound_io(dev); + for(auto i = config.m_channel_mappings.begin(); i != config.m_channel_mappings.end(); i++) + if(std::get<0>(*i) == guest_channel && std::get<1>(*i) == "" && std::get<2>(*i) == node_channel) { + config.m_channel_mappings.erase(i); + return; } - index -= mixer.inputs(); - } +} - // didn't locate - info.mixer = nullptr; - return false; +void sound_manager::config_set_volume_sound_io_channel_connection_node(sound_io_device *dev, u32 guest_channel, std::string name, u32 node_channel, float db) +{ + internal_config_set_volume_sound_io_channel_connection_node(dev, guest_channel, name, node_channel, db); + m_osd_info.m_generation --; } +void sound_manager::internal_config_set_volume_sound_io_channel_connection_node(sound_io_device *dev, u32 guest_channel, std::string name, u32 node_channel, float db) +{ + auto &config = config_get_sound_io(dev); + for(auto &cmap : config.m_channel_mappings) + if(std::get<0>(cmap) == guest_channel && std::get<1>(cmap) == name && std::get<2>(cmap) == node_channel) { + std::get<3>(cmap) = db; + return; + } +} -//------------------------------------------------- -// mute - mute sound output -//------------------------------------------------- +void sound_manager::config_set_volume_sound_io_channel_connection_default(sound_io_device *dev, u32 guest_channel, u32 node_channel, float db) +{ + internal_config_set_volume_sound_io_channel_connection_default(dev, guest_channel, node_channel, db); + m_osd_info.m_generation --; +} -void sound_manager::mute(bool mute, u8 reason) +void sound_manager::internal_config_set_volume_sound_io_channel_connection_default(sound_io_device *dev, u32 guest_channel, u32 node_channel, float db) { - if (mute) - m_muted |= reason; - else - m_muted &= ~reason; - set_attenuation(m_attenuation); + auto &config = config_get_sound_io(dev); + for(auto &cmap : config.m_channel_mappings) + if(std::get<0>(cmap) == guest_channel && std::get<1>(cmap) == "" && std::get<2>(cmap) == node_channel) { + std::get<3>(cmap) = db; + return; + } } +void sound_manager::startup_cleanups() +{ + auto osd_info = machine().osd().sound_get_information(); + + // for every sound_io device that does not have a configuration entry, add a + // mapping to default + auto default_one = [this](sound_io_device &dev) { + for(const auto &config : m_configs) + if(config.m_name == dev.tag()) + return; + m_configs.emplace_back(config_mapping { dev.tag() }); + m_configs.back().m_node_mappings.emplace_back(std::pair<std::string, float>("", 0.0)); + }; + + for(sound_io_device &dev : speaker_device_enumerator(machine().root_device())) + default_one(dev); + for(sound_io_device &dev : microphone_device_enumerator(machine().root_device())) + default_one(dev); + + // If there's no default sink replace all the default sink config + // entries into the first sink available + if(!osd_info.m_default_sink) { + std::string first_sink_name; + for(const auto &node : osd_info.m_nodes) + if(node.m_sinks) { + first_sink_name = node.name(); + break; + } -//------------------------------------------------- -// reset - reset all sound chips -//------------------------------------------------- + if(first_sink_name != "") + for(auto &config : m_configs) { + for(auto &nmap : config.m_node_mappings) + if(nmap.first == "") + nmap.first = first_sink_name; + for(auto &cmap : config.m_channel_mappings) + if(std::get<1>(cmap) == "") + std::get<1>(cmap) = first_sink_name; + } + } -void sound_manager::reset() -{ - // reset all the sound chips - for (device_sound_interface &sound : sound_interface_iterator(machine().root_device())) - sound.device().reset(); + + // If there's no default source replace all the default source config + // entries into the first source available + if(!osd_info.m_default_source) { + std::string first_source_name; + for(const auto &node : osd_info.m_nodes) + if(node.m_sources) { + first_source_name = node.name(); + break; + } + + if(first_source_name != "") + for(auto &config : m_configs) { + for(auto &nmap : config.m_node_mappings) + if(nmap.first == "") + nmap.first = first_source_name; + for(auto &cmap : config.m_channel_mappings) + if(std::get<1>(cmap) == "") + std::get<1>(cmap) = first_source_name; + } + } } +template<bool is_output, typename S> void sound_manager::apply_osd_changes(std::vector<S> &streams) +{ + // Apply host system volume and routing changes to the internal structures + for(S &stream : streams) { + u32 sidx; + for(sidx = 0; sidx != m_osd_info.m_streams.size() && m_osd_info.m_streams[sidx].m_id != stream.m_id; sidx++); + // If the stream has been lost, continue. It will be cleared in update_osd_streams. + if(sidx == m_osd_info.m_streams.size()) + continue; + + // Check if the target and/or the volumes changed + bool node_changed = stream.m_node != m_osd_info.m_streams[sidx].m_node; + bool volume_changed = !std::equal(stream.m_volumes.begin(), stream.m_volumes.end(), m_osd_info.m_streams[sidx].m_volumes.begin(), m_osd_info.m_streams[sidx].m_volumes.end()); + + if(node_changed || volume_changed) { + // Check if a node change is just tracking the system default + bool system_default_tracking = node_changed && stream.m_is_system_default && m_osd_info.m_streams[sidx].m_node == (is_output ? m_osd_info.m_default_sink : m_osd_info.m_default_source); + + // Find the config entry for the sound_io + config_mapping *config = nullptr; + for(auto &conf : m_configs) + if(conf.m_name == stream.m_dev->tag()) { + config = &conf; + break; + } + if(!config) + continue; + + // Retrieve the old node name, and, if it's different, the new node name + std::string old_node_name = stream.m_node_name; + std::string new_node_name; + if(node_changed) { + for(const auto &node : m_osd_info.m_nodes) + if(node.m_id == m_osd_info.m_streams[sidx].m_node) { + new_node_name = node.name(); + break; + } + // That's really, really not supposed to happen + if(new_node_name.empty()) + continue; + } else + new_node_name = old_node_name; + + // Separate the cases on full mapping vs. channel mapping + if(!stream.m_is_channel_mapping) { + // Full mapping + // Find the index of the config mapping entry that generated the stream, if there's still one. + // Note that a default system stream has the empty string as a name + u32 index; + for(index = 0; index != config->m_node_mappings.size(); index++) + if(config->m_node_mappings[index].first == old_node_name) + break; + if(index == config->m_node_mappings.size()) + continue; + + // If the target node changed, write it down + if(node_changed) { + if(!system_default_tracking) { + config->m_node_mappings[index].first = new_node_name; + stream.m_node_name = new_node_name; + stream.m_is_system_default = false; + } + stream.m_node = m_osd_info.m_streams[sidx].m_node; + } -//------------------------------------------------- -// pause - pause sound output -//------------------------------------------------- + // If the volume changed, there are two + // possibilities: either the channels split, or + // they didn't. + if(volume_changed) { + // Check is all the channel volumes are the same + float new_volume = m_osd_info.m_streams[sidx].m_volumes[0]; + bool same = true; + for(u32 i = 1; i != m_osd_info.m_streams[sidx].m_volumes.size(); i++) + if(m_osd_info.m_streams[sidx].m_volumes[i] != new_volume) { + same = false; + break; + } + if(same) { + // All the same volume, just note down the new volume + stream.m_volumes = m_osd_info.m_streams[sidx].m_volumes; + config->m_node_mappings[index].second = new_volume; + + } else { + const osd::audio_info::node_info *node = nullptr; + for(const auto &n : m_osd_info.m_nodes) + if(n.m_id == stream.m_node) { + node = &n; + break; + } + for(u32 channel = 0; channel != stream.m_channels; channel++) { + std::vector<u32> targets = find_channel_mapping(stream.m_dev->get_position(channel), node); + for(u32 tchannel : targets) + if(stream.m_node_name == "") + internal_config_add_sound_io_channel_connection_default(stream.m_dev, channel, tchannel, m_osd_info.m_streams[sidx].m_volumes[tchannel]); + else + internal_config_add_sound_io_channel_connection_node(stream.m_dev, channel, stream.m_node_name, tchannel, m_osd_info.m_streams[sidx].m_volumes[tchannel]); + } + config->m_node_mappings.erase(config->m_node_mappings.begin() + index); + } + } + } else { + // Channel mapping + for(u32 channel = 0; channel != stream.m_channels; channel++) { + if(stream.m_unused_channels_mask & (1 << channel)) + continue; + + // Find the index of the config mapping entry that generated the stream channel, if there's still one. + // Note that a default system stream has the empty string as a name + u32 index; + for(index = 0; index != config->m_channel_mappings.size(); index++) + if(std::get<1>(config->m_channel_mappings[index]) == old_node_name && + std::get<2>(config->m_channel_mappings[index]) == channel) + break; + if(index == config->m_channel_mappings.size()) + continue; + + // If the target node changed, write it down + if(node_changed) { + if(!system_default_tracking) { + std::get<1>(config->m_channel_mappings[index]) = new_node_name; + stream.m_node_name = new_node_name; + stream.m_is_system_default = false; + } + stream.m_node = m_osd_info.m_streams[sidx].m_node; + } + + // If the volume changed, write in down too + if(volume_changed) { + std::get<3>(config->m_channel_mappings[index]) = m_osd_info.m_streams[sidx].m_volumes[channel]; + stream.m_volumes[channel] = m_osd_info.m_streams[sidx].m_volumes[channel]; + } + } + } + } + } +} -void sound_manager::pause() +void sound_manager::osd_information_update() { - mute(true, MUTE_REASON_PAUSE); + // Get a snapshot of the current information + m_osd_info = machine().osd().sound_get_information(); + + // Analyze the streams to see if anything changed, but only in the + // split stream case. + if(machine().osd().sound_split_streams_per_source()) { + apply_osd_changes<false, osd_input_stream >(m_osd_input_streams ); + apply_osd_changes<false, osd_output_stream>(m_osd_output_streams); + } } +void sound_manager::generate_mapping() +{ + auto find_node = [this](std::string name) -> u32 { + for(const auto &node : m_osd_info.m_nodes) + if(node.name() == name) + return node.m_id; + return 0; + }; + + m_mappings.clear(); + for(speaker_info &speaker : m_speakers) { + auto &config = config_get_sound_io(&speaker.m_dev); + m_mappings.emplace_back(mapping { &speaker.m_dev }); + auto &omap = m_mappings.back(); + + std::vector<std::string> node_to_remove; + for(auto &nmap : config.m_node_mappings) { + if(nmap.first == "") { + if(m_osd_info.m_default_sink) + omap.m_node_mappings.emplace_back(mapping::node_mapping { m_osd_info.m_default_sink, nmap.second, true }); + } else { + u32 node_id = find_node(nmap.first); + if(node_id != 0) + omap.m_node_mappings.emplace_back(mapping::node_mapping { node_id, nmap.second, false }); + else + node_to_remove.push_back(nmap.first); + } + } -//------------------------------------------------- -// resume - resume sound output -//------------------------------------------------- + for(auto &nmap: node_to_remove) + internal_config_remove_sound_io_connection_node(&speaker.m_dev, nmap); + + std::vector<std::tuple<u32, std::string, u32>> channel_map_to_remove; + for(auto &cmap : config.m_channel_mappings) { + if(std::get<1>(cmap) == "") { + if(m_osd_info.m_default_sink) + omap.m_channel_mappings.emplace_back(mapping::channel_mapping { std::get<0>(cmap), m_osd_info.m_default_sink, std::get<2>(cmap), std::get<3>(cmap), true }); + } else { + u32 node_id = find_node(std::get<1>(cmap)); + if(node_id != 0) + omap.m_channel_mappings.emplace_back(mapping::channel_mapping { std::get<0>(cmap), node_id, std::get<2>(cmap), std::get<3>(cmap), false }); + else + channel_map_to_remove.push_back(std::tuple<u32, std::string, u32>(std::get<0>(cmap), std::get<1>(cmap), std::get<2>(cmap))); + } + } -void sound_manager::resume() -{ - mute(false, MUTE_REASON_PAUSE); + for(auto &cmap : channel_map_to_remove) + internal_config_remove_sound_io_channel_connection_node(&speaker.m_dev, std::get<0>(cmap), std::get<1>(cmap), std::get<2>(cmap)); + } + + for(microphone_info &mic : m_microphones) { + auto &config = config_get_sound_io(&mic.m_dev); + m_mappings.emplace_back(mapping { &mic.m_dev }); + auto &omap = m_mappings.back(); + + std::vector<std::string> node_to_remove; + for(auto &nmap : config.m_node_mappings) { + if(nmap.first == "") { + if(m_osd_info.m_default_source) + omap.m_node_mappings.emplace_back(mapping::node_mapping { m_osd_info.m_default_source, nmap.second, true }); + } else { + u32 node_id = find_node(nmap.first); + if(node_id != 0) + omap.m_node_mappings.emplace_back(mapping::node_mapping { node_id, nmap.second, false }); + else + node_to_remove.push_back(nmap.first); + } + } + + for(auto &nmap: node_to_remove) + internal_config_remove_sound_io_connection_node(&mic.m_dev, nmap); + + std::vector<std::tuple<u32, std::string, u32>> channel_map_to_remove; + for(auto &cmap : config.m_channel_mappings) { + if(std::get<1>(cmap) == "") { + if(m_osd_info.m_default_source) + omap.m_channel_mappings.emplace_back(mapping::channel_mapping { std::get<0>(cmap), m_osd_info.m_default_source, std::get<2>(cmap), std::get<3>(cmap), true }); + } else { + u32 node_id = find_node(std::get<1>(cmap)); + if(node_id != 0) + omap.m_channel_mappings.emplace_back(mapping::channel_mapping { std::get<0>(cmap), node_id, std::get<2>(cmap), std::get<3>(cmap), false }); + else + channel_map_to_remove.push_back(std::tuple<u32, std::string, u32>(std::get<0>(cmap), std::get<1>(cmap), std::get<2>(cmap))); + } + } + + for(auto &cmap : channel_map_to_remove) + internal_config_remove_sound_io_channel_connection_node(&mic.m_dev, std::get<0>(cmap), std::get<1>(cmap), std::get<2>(cmap)); + } } +// Find where to map a sound_io channel into a node's channels depending on their positions + +std::vector<u32> sound_manager::find_channel_mapping(const std::array<double, 3> &position, const osd::audio_info::node_info *node) +{ + std::vector<u32> result; + if(position[0] == 0 && position[1] == 0 && position[2] == 0) + return result; + double best_dist = -1; + for(u32 port = 0; port != node->m_port_positions.size(); port++) + if(node->m_port_positions[port][0] || node->m_port_positions[port][1] || node->m_port_positions[port][2]) { + double dx = position[0] - node->m_port_positions[port][0]; + double dy = position[1] - node->m_port_positions[port][1]; + double dz = position[2] - node->m_port_positions[port][2]; + double dist = dx*dx + dy*dy + dz*dz; + if(best_dist == -1 || dist < best_dist) { + best_dist = dist; + result.clear(); + result.push_back(port); + } else if(best_dist == dist) + result.push_back(port); + } + return result; +} -//------------------------------------------------- -// config_load - read and apply data from the -// configuration file -//------------------------------------------------- -void sound_manager::config_load(config_type cfg_type, util::xml::data_node const *parentnode) +void sound_manager::update_osd_streams() { - // we only care about game files - if (cfg_type != config_type::GAME) - return; + std::unique_lock<std::mutex> lock(m_effects_mutex); + auto current_input_streams = std::move(m_osd_input_streams); + auto current_output_streams = std::move(m_osd_output_streams); + m_osd_input_streams.clear(); + m_osd_output_streams.clear(); + + // Find the index of a sound_io_device in the speaker_info vector or the microphone_info vector + + auto find_sound_io_index = [this](sound_io_device *dev) -> u32 { + for(u32 si = 0; si != m_speakers.size(); si++) + if(&m_speakers[si].m_dev == dev) + return si; + for(u32 si = 0; si != m_microphones.size(); si++) + if(&m_microphones[si].m_dev == dev) + return si; + return 0; // Can't happen + }; + + + // Find a pointer to a node_info from the node id + auto find_node_info = [this](u32 node) -> const osd::audio_info::node_info * { + for(const auto &ni : m_osd_info.m_nodes) { + if(ni.m_id == node) + return ∋ + } + // Can't happen + return nullptr; + }; + + // Two possible mapping methods depending on the osd capabilities + + for(auto &m : m_microphones) + m.m_input_mixing_steps.clear(); + m_output_mixing_steps.clear(); + + auto &osd = machine().osd(); + if(osd.sound_split_streams_per_source()) { + auto get_input_stream_for_node_and_device = [this, ¤t_input_streams] (const osd::audio_info::node_info *node, sound_io_device *dev, bool is_system_default, bool is_channel_mapping = false) -> u32 { + // Check if the osd stream already exists to pick it up in case. + // Clear the id in the current_streams structure to show it has been picked up, reset the unused mask. + // Clear the volumes + // m_dev will already be correct + + for(auto &os : current_input_streams) + if(os.m_id && os.m_node == node->m_id && os.m_dev == dev) { + u32 sid = m_osd_input_streams.size(); + m_osd_input_streams.emplace_back(std::move(os)); + os.m_id = 0; + auto &nos = m_osd_input_streams[sid]; + nos.m_is_channel_mapping = is_channel_mapping; + nos.m_unused_channels_mask = util::make_bitmask<u32>(node->m_sources); + nos.m_volumes.clear(); + nos.m_is_system_default = is_system_default; + return sid; + } - // might not have any data - if (parentnode == nullptr) - return; + // If none exists, create one + u32 sid = m_osd_input_streams.size(); + u32 rate = machine().sample_rate(); + m_osd_input_streams.emplace_back(osd_input_stream(node->m_id, is_system_default ? "" : node->m_name, node->m_sources, rate, is_system_default, dev)); + osd_input_stream &nos = m_osd_input_streams.back(); + nos.m_id = machine().osd().sound_stream_source_open(node->m_id, dev->tag(), rate); + nos.m_is_channel_mapping = is_channel_mapping; + nos.m_buffer.set_sync_sample(rate_and_last_sync_to_index(rate)); + return sid; + }; + + auto get_output_stream_for_node_and_device = [this, ¤t_output_streams] (const osd::audio_info::node_info *node, sound_io_device *dev, bool is_system_default, bool is_channel_mapping = false) -> u32 { + // Check if the osd stream already exists to pick it up in case. + // Clear the id in the current_streams structure to show it has been picked up, reset the unused mask. + // Clear the volumes + // m_dev will already be correct + + for(auto &os : current_output_streams) + if(os.m_id && os.m_node == node->m_id && os.m_dev == dev) { + u32 sid = m_osd_output_streams.size(); + m_osd_output_streams.emplace_back(std::move(os)); + os.m_id = 0; + auto &nos = m_osd_output_streams[sid]; + nos.m_is_channel_mapping = is_channel_mapping; + nos.m_volumes.clear(); + nos.m_unused_channels_mask = util::make_bitmask<u32>(node->m_sinks); + nos.m_is_system_default = is_system_default; + return sid; + } - // iterate over channel nodes - for (util::xml::data_node const *channelnode = parentnode->get_child("channel"); channelnode != nullptr; channelnode = channelnode->get_next_sibling("channel")) - { - mixer_input info; - if (indexed_mixer_input(channelnode->get_attribute_int("index", -1), info)) - { - float defvol = channelnode->get_attribute_float("defvol", 1.0f); - float newvol = channelnode->get_attribute_float("newvol", -1000.0f); - if (newvol != -1000.0f) - info.stream->set_user_gain(info.inputnum, newvol / defvol); + // If none exists, create one + u32 sid = m_osd_output_streams.size(); + u32 rate = machine().sample_rate(); + m_osd_output_streams.emplace_back(osd_output_stream(node->m_id, is_system_default ? "" : node->m_name, node->m_sinks, rate, is_system_default, dev)); + osd_output_stream &nos = m_osd_output_streams.back(); + nos.m_id = machine().osd().sound_stream_sink_open(node->m_id, dev->tag(), rate); + nos.m_is_channel_mapping = is_channel_mapping; + nos.m_last_sync = rate_and_last_sync_to_index(rate); + return sid; + }; + + auto get_input_stream_for_node_and_channel = [this, &get_input_stream_for_node_and_device] (const osd::audio_info::node_info *node, u32 node_channel, sound_io_device *dev, bool is_system_default) -> u32 { + // First check if there's an active stream + for(u32 sid = 0; sid != m_osd_input_streams.size(); sid++) { + auto &os = m_osd_input_streams[sid]; + if(os.m_node == node->m_id && os.m_dev == dev && os.m_unused_channels_mask & (1 << node_channel) && os.m_is_channel_mapping) + return sid; + } + + // Otherwise use the default method + return get_input_stream_for_node_and_device(node, dev, is_system_default, true); + }; + + + auto get_output_stream_for_node_and_channel = [this, &get_output_stream_for_node_and_device] (const osd::audio_info::node_info *node, u32 node_channel, sound_io_device *dev, bool is_system_default) -> u32 { + // First check if there's an active stream with the correct channel not used yet + for(u32 sid = 0; sid != m_osd_output_streams.size(); sid++) { + auto &os = m_osd_output_streams[sid]; + if(os.m_node == node->m_id && os.m_dev == dev && os.m_unused_channels_mask & (1 << node_channel) && os.m_is_channel_mapping) + return sid; + } + + // Otherwise use the default method + return get_output_stream_for_node_and_device(node, dev, is_system_default, true); + }; + + // Create/retrieve streams to apply the decided mapping + for(const auto &omap : m_mappings) { + u32 dev_index = find_sound_io_index(omap.m_dev); + bool is_output = omap.m_dev->is_output(); + if(is_output) { + std::vector<mixing_step> &mixing_steps = m_output_mixing_steps; + u32 dchannels = omap.m_dev->inputs(); + for(const auto &nm : omap.m_node_mappings) { + const auto *node = find_node_info(nm.m_node); + u32 osd_index = get_output_stream_for_node_and_device(node, omap.m_dev, nm.m_is_system_default); + auto &stream = m_osd_output_streams[osd_index]; + u32 umask = stream.m_unused_channels_mask; + float linear_volume = 1.0; + + if(osd.sound_external_per_channel_volume()) { + stream.m_volumes.clear(); + stream.m_volumes.resize(stream.m_channels, nm.m_db); + + } else + linear_volume = osd::db_to_linear(nm.m_db); + + for(u32 channel = 0; channel != dchannels; channel++) { + std::vector<u32> targets = find_channel_mapping(omap.m_dev->get_position(channel), node); + for(u32 tchannel : targets) { + // If the channel is output and in the to + // clear mask, use load, otherwise use add. + // Apply the volume too if needed + mixing_steps.emplace_back(mixing_step { + (umask & (1 << tchannel)) ? mixing_step::COPY : mixing_step::ADD, + osd_index, + tchannel, + dev_index, + channel, + linear_volume + }); + umask &= ~(1 << tchannel); + } + } + stream.m_unused_channels_mask = umask; + } + + for(const auto &cm : omap.m_channel_mappings) { + const auto *node = find_node_info(cm.m_node); + u32 osd_index = get_output_stream_for_node_and_channel(node, cm.m_node_channel, omap.m_dev, cm.m_is_system_default); + auto &stream = m_osd_output_streams[osd_index]; + float linear_volume = 1.0; + + if(osd.sound_external_per_channel_volume()) { + if(stream.m_volumes.empty()) + stream.m_volumes.resize(stream.m_channels, -96); + stream.m_volumes[cm.m_node_channel] = cm.m_db; + + } else + linear_volume = osd::db_to_linear(cm.m_db); + + mixing_steps.emplace_back(mixing_step { + (stream.m_unused_channels_mask & (1 << cm.m_node_channel)) ? + mixing_step::COPY : mixing_step::ADD, + osd_index, + cm.m_node_channel, + dev_index, + cm.m_guest_channel, + linear_volume + }); + stream.m_unused_channels_mask &= ~(1 << cm.m_node_channel); + } + + + } else { + std::vector<mixing_step> &mixing_steps = m_microphones[dev_index].m_input_mixing_steps; + u32 dchannels = omap.m_dev->outputs(); + for(const auto &nm : omap.m_node_mappings) { + const auto *node = find_node_info(nm.m_node); + u32 osd_index = get_input_stream_for_node_and_device(node, omap.m_dev, nm.m_is_system_default); + auto &stream = m_osd_input_streams[osd_index]; + u32 umask = stream.m_unused_channels_mask; + float linear_volume = 1.0; + + if(osd.sound_external_per_channel_volume()) { + stream.m_volumes.clear(); + stream.m_volumes.resize(stream.m_channels, nm.m_db); + + } else + linear_volume = osd::db_to_linear(nm.m_db); + + for(u32 channel = 0; channel != dchannels; channel++) { + std::vector<u32> targets = find_channel_mapping(omap.m_dev->get_position(channel), node); + for(u32 tchannel : targets) { + // If the channel is output and in the to + // clear mask, use load, otherwise use add. + // Apply the volume too if needed + mixing_steps.emplace_back(mixing_step { + mixing_step::ADD, + osd_index, + tchannel, + dev_index, + channel, + linear_volume + }); + umask &= ~(1 << tchannel); + } + } + stream.m_unused_channels_mask = umask; + } + + for(const auto &cm : omap.m_channel_mappings) { + const auto *node = find_node_info(cm.m_node); + u32 osd_index = get_input_stream_for_node_and_channel(node, cm.m_node_channel, omap.m_dev, cm.m_is_system_default); + auto &stream = m_osd_input_streams[osd_index]; + float linear_volume = 1.0; + + if(osd.sound_external_per_channel_volume()) { + if(stream.m_volumes.empty()) + stream.m_volumes.resize(stream.m_channels, -96); + stream.m_volumes[cm.m_node_channel] = cm.m_db; + + } else + linear_volume = osd::db_to_linear(cm.m_db); + + mixing_steps.emplace_back(mixing_step { + mixing_step::ADD, + osd_index, + cm.m_node_channel, + dev_index, + cm.m_guest_channel, + linear_volume + }); + stream.m_unused_channels_mask &= ~(1 << cm.m_node_channel); + } + } + } + + } else { + // All sources need to be merged per-destination, max one stream per destination + + std::map<u32, u32> stream_per_node; + + // Retrieve or create the one osd stream for a given + // destination. First check if we already have it, then + // whether it was previously created, then otherwise create + // it. + + auto get_input_stream_for_node = [this, ¤t_input_streams, &stream_per_node] (const osd::audio_info::node_info *node, bool is_system_default) -> u32 { + // Pick up the existing stream if there's one + auto si = stream_per_node.find(node->m_id); + if(si != stream_per_node.end()) + return si->second; + + // Create the default unused mask + u32 channels = node->m_sources; + u32 umask = util::make_bitmask<u32>(channels); + + // Check if the osd stream already exists to pick it up in case. + // Clear the id in the current_streams structure to show it has been picked up, reset the unused mask. + // m_speaker will already be nullptr, m_source_channels and m_volumes empty. + + for(auto &os : current_input_streams) + if(os.m_id && os.m_node == node->m_id) { + u32 sid = m_osd_input_streams.size(); + m_osd_input_streams.emplace_back(std::move(os)); + os.m_id = 0; + m_osd_input_streams.back().m_unused_channels_mask = umask; + m_osd_input_streams.back().m_is_system_default = is_system_default; + stream_per_node[node->m_id] = sid; + return sid; + } + + // If none exists, create one + u32 sid = m_osd_input_streams.size(); + u32 rate = machine().sample_rate(); + m_osd_input_streams.emplace_back(osd_input_stream(node->m_id, is_system_default ? "" : node->m_name, channels, rate, is_system_default, nullptr)); + osd_input_stream &stream = m_osd_input_streams.back(); + stream.m_id = machine().osd().sound_stream_source_open(node->m_id, machine().system().name, rate); + stream.m_buffer.set_sync_sample(rate_and_last_sync_to_index(rate)); + stream_per_node[node->m_id] = sid; + return sid; + }; + + auto get_output_stream_for_node = [this, ¤t_output_streams, &stream_per_node] (const osd::audio_info::node_info *node, bool is_system_default) -> u32 { + // Pick up the existing stream if there's one + auto si = stream_per_node.find(node->m_id); + if(si != stream_per_node.end()) + return si->second; + + // Create the default unused mask + u32 channels = node->m_sinks; + u32 umask = util::make_bitmask<u32>(channels); + + // Check if the osd stream already exists to pick it up in case. + // Clear the id in the current_streams structure to show it has been picked up, reset the unused mask. + // m_speaker will already be nullptr, m_source_channels and m_volumes empty. + + for(auto &os : current_output_streams) + if(os.m_id && os.m_node == node->m_id) { + u32 sid = m_osd_output_streams.size(); + m_osd_output_streams.emplace_back(std::move(os)); + os.m_id = 0; + m_osd_output_streams.back().m_unused_channels_mask = umask; + m_osd_output_streams.back().m_is_system_default = is_system_default; + stream_per_node[node->m_id] = sid; + return sid; + } + + // If none exists, create one + u32 sid = m_osd_output_streams.size(); + u32 rate = machine().sample_rate(); + m_osd_output_streams.emplace_back(osd_output_stream(node->m_id, is_system_default ? "" : node->m_name, channels, rate, is_system_default, nullptr)); + osd_output_stream &stream = m_osd_output_streams.back(); + stream.m_id = machine().osd().sound_stream_sink_open(node->m_id, machine().system().name, rate); + stream.m_last_sync = rate_and_last_sync_to_index(rate); + stream_per_node[node->m_id] = sid; + return sid; + }; + + + // Create/retrieve streams to apply the decided mapping + + for(const auto &omap : m_mappings) { + u32 dev_index = find_sound_io_index(omap.m_dev); + bool is_output = omap.m_dev->is_output(); + if(is_output) { + u32 channels = m_speakers[dev_index].m_channels; + std::vector<mixing_step> &mixing_steps = m_output_mixing_steps; + for(const auto &nm : omap.m_node_mappings) { + const auto *node = find_node_info(nm.m_node); + u32 osd_index = get_output_stream_for_node(node, nm.m_is_system_default); + u32 umask = m_osd_output_streams[osd_index].m_unused_channels_mask; + float linear_volume = osd::db_to_linear(nm.m_db); + + for(u32 channel = 0; channel != channels; channel++) { + std::vector<u32> targets = find_channel_mapping(omap.m_dev->get_position(channel), node); + for(u32 tchannel : targets) { + // If the channel is in the to clear mask, use load, otherwise use add + // Apply the volume too + mixing_steps.emplace_back(mixing_step { + (umask & (1 << tchannel)) ? mixing_step::COPY : mixing_step::ADD, + osd_index, + tchannel, + dev_index, + channel, + linear_volume + }); + umask &= ~(1 << tchannel); + } + } + m_osd_output_streams[osd_index].m_unused_channels_mask = umask; + } + + for(const auto &cm : omap.m_channel_mappings) { + const auto *node = find_node_info(cm.m_node); + u32 osd_index = get_output_stream_for_node(node, false); + u32 umask = m_osd_output_streams[osd_index].m_unused_channels_mask; + + // If the channel is in the to clear mask, use load, otherwise use add + // Apply the volume too + mixing_steps.emplace_back(mixing_step { + (umask & (1 << cm.m_node_channel)) ? mixing_step::COPY : mixing_step::ADD, + osd_index, + cm.m_node_channel, + dev_index, + cm.m_guest_channel, + osd::db_to_linear(cm.m_db) + }); + m_osd_output_streams[osd_index].m_unused_channels_mask = umask & ~(1 << cm.m_node_channel); + } + + } else { + u32 channels = m_microphones[dev_index].m_channels; + std::vector<mixing_step> &mixing_steps = m_microphones[dev_index].m_input_mixing_steps; + for(const auto &nm : omap.m_node_mappings) { + const auto *node = find_node_info(nm.m_node); + u32 osd_index = get_input_stream_for_node(node, nm.m_is_system_default); + float linear_volume = osd::db_to_linear(nm.m_db); + + for(u32 channel = 0; channel != channels; channel++) { + std::vector<u32> targets = find_channel_mapping(omap.m_dev->get_position(channel), node); + for(u32 tchannel : targets) { + // If the channel is in the to clear mask, use load, otherwise use add + // Apply the volume too + mixing_steps.emplace_back(mixing_step { + mixing_step::ADD, + osd_index, + tchannel, + dev_index, + channel, + linear_volume + }); + m_osd_input_streams[osd_index].m_unused_channels_mask &= ~(1 << tchannel); + } + } + } + + for(const auto &cm : omap.m_channel_mappings) { + const auto *node = find_node_info(cm.m_node); + u32 osd_index = get_input_stream_for_node(node, false); + + // If the channel is in the to clear mask, use load, otherwise use add + // Apply the volume too + mixing_steps.emplace_back(mixing_step { + mixing_step::ADD, + osd_index, + cm.m_node_channel, + dev_index, + cm.m_guest_channel, + osd::db_to_linear(cm.m_db) + }); + m_osd_input_streams[osd_index].m_unused_channels_mask &= ~(1 << cm.m_node_channel); + } + } } } -} + // Add a clear step for all output streams that need it + // Also set the volumes if supported + for(u32 stream_index = 0; stream_index != m_osd_output_streams.size(); stream_index++) { + auto &stream = m_osd_output_streams[stream_index]; + if(stream.m_unused_channels_mask) { + for(u32 channel = 0; channel != stream.m_channels; channel ++) + if(stream.m_unused_channels_mask & (1 << channel)) + m_output_mixing_steps.emplace_back(mixing_step { mixing_step::CLEAR, 0, 0, stream_index, channel, 0.0 }); + } + if(!stream.m_volumes.empty()) + osd.sound_stream_set_volumes(stream.m_id, stream.m_volumes); + } -//------------------------------------------------- -// config_save - save data to the configuration -// file -//------------------------------------------------- + // If supported, set the volumes for the input streams + for(u32 stream_index = 0; stream_index != m_osd_input_streams.size(); stream_index++) { + auto &stream = m_osd_input_streams[stream_index]; + if(!stream.m_volumes.empty()) + osd.sound_stream_set_volumes(stream.m_id, stream.m_volumes); + } -void sound_manager::config_save(config_type cfg_type, util::xml::data_node *parentnode) + // Close all previous streams that haven't been picked up + for(const auto &stream : current_input_streams) + if(stream.m_id) + machine().osd().sound_stream_close(stream.m_id); + for(const auto &stream : current_output_streams) + if(stream.m_id) + machine().osd().sound_stream_close(stream.m_id); +} + +void sound_manager::mapping_update() { - // we only care about game files - if (cfg_type != config_type::GAME) - return; + auto &osd = machine().osd(); + while(m_osd_info.m_generation != osd.sound_get_generation()) { + osd_information_update(); + + if(VERBOSE & LOG_OSD_INFO) { + LOG_OUTPUT_FUNC("OSD information:\n"); + LOG_OUTPUT_FUNC("- generation %u\n", m_osd_info.m_generation); + LOG_OUTPUT_FUNC("- default sink %u\n", m_osd_info.m_default_sink); + LOG_OUTPUT_FUNC("- default source %u\n", m_osd_info.m_default_source); + LOG_OUTPUT_FUNC("- nodes:\n"); + for(const auto &node : m_osd_info.m_nodes) { + LOG_OUTPUT_FUNC(" * %3u %s [%d %d-%d]\n", node.m_id, node.name().c_str(), node.m_rate.m_default_rate, node.m_rate.m_min_rate, node.m_rate.m_max_rate); + uint32_t port_count = node.m_sinks; + if(port_count < node.m_sources) + port_count = node.m_sources; + for(uint32_t port = 0; port != port_count; port++) + LOG_OUTPUT_FUNC(" %s %s [%g %g %g]\n", + port < node.m_sinks ? port < node.m_sources ? "<>" : ">" : "<", + node.m_port_names[port].c_str(), + node.m_port_positions[port][0], + node.m_port_positions[port][1], + node.m_port_positions[port][2]); + } + LOG_OUTPUT_FUNC("- streams:\n"); + for(const auto &stream : m_osd_info.m_streams) { + LOG_OUTPUT_FUNC(" * %3u node %u", stream.m_id, stream.m_node); + if(!stream.m_volumes.empty()) { + LOG_OUTPUT_FUNC(" volumes"); + for(float v : stream.m_volumes) + LOG_OUTPUT_FUNC(" %g", v); + } + LOG_OUTPUT_FUNC("\n"); + } + } - // iterate over mixer channels - if (parentnode != nullptr) - for (int mixernum = 0; ; mixernum++) - { - mixer_input info; - if (!indexed_mixer_input(mixernum, info)) - break; - float newvol = info.stream->user_gain(info.inputnum); - - if (newvol != 1.0f) - { - util::xml::data_node *const channelnode = parentnode->add_child("channel", nullptr); - if (channelnode != nullptr) - { - channelnode->set_attribute_int("index", mixernum); - channelnode->set_attribute_float("newvol", newvol); + generate_mapping(); + + if(VERBOSE & LOG_MAPPING) { + LOG_OUTPUT_FUNC("MAPPING:\n"); + for(const auto &omap : m_mappings) { + LOG_OUTPUT_FUNC("- sound_io %s\n", omap.m_dev->tag()); + for(const auto &nm : omap.m_node_mappings) + LOG_OUTPUT_FUNC(" * node %u volume %g%s\n", nm.m_node, nm.m_db, nm.m_is_system_default ? " (default)" : ""); + for(const auto &cm : omap.m_channel_mappings) + LOG_OUTPUT_FUNC(" * channel %u <-> node %u:%i volume %g\n", cm.m_guest_channel, cm.m_node, cm.m_node_channel, cm.m_db); + } + } + + update_osd_streams(); + + if(VERBOSE & LOG_OSD_STREAMS) { + LOG_OUTPUT_FUNC("OSD input streams:\n"); + for(const auto &os : m_osd_input_streams) { + if(machine().osd().sound_split_streams_per_source()) { + LOG_OUTPUT_FUNC("- %3u %s node %u", os.m_id, os.m_dev ? os.m_dev->tag() : "-", os.m_node); + if(!os.m_is_channel_mapping) + LOG_OUTPUT_FUNC(" channels"); + if(machine().osd().sound_external_per_channel_volume()) { + LOG_OUTPUT_FUNC(" dB"); + for(u32 i = 0; i != os.m_channels; i++) + LOG_OUTPUT_FUNC(" %g", os.m_volumes[i]); + } + LOG_OUTPUT_FUNC("\n"); + } else + LOG_OUTPUT_FUNC("- %3u node %u\n", os.m_id, os.m_node); + } + LOG_OUTPUT_FUNC("Input mixing steps:\n"); + for(const auto &m : m_microphones) { + LOG_OUTPUT_FUNC(" %s:\n", m.m_dev.tag()); + for(const auto &ms : m.m_input_mixing_steps) { + static const char *const modes[5] = { "clear", "copy", "copy+vol", "add", "add+vol" }; + LOG_OUTPUT_FUNC(" - %s osd %u:%u -> device %u:%u level %g\n", modes[ms.m_mode], ms.m_osd_index, ms.m_osd_channel, ms.m_device_index, ms.m_device_channel, ms.m_linear_volume); } } + LOG_OUTPUT_FUNC("OSD output streams:\n"); + for(const auto &os : m_osd_output_streams) { + if(machine().osd().sound_split_streams_per_source()) { + LOG_OUTPUT_FUNC("- %3u %s node %u", os.m_id, os.m_dev ? os.m_dev->tag() : "-", os.m_node); + if(!os.m_is_channel_mapping) + LOG_OUTPUT_FUNC(" channels"); + if(machine().osd().sound_external_per_channel_volume()) { + LOG_OUTPUT_FUNC(" dB"); + for(u32 i = 0; i != os.m_channels; i++) + LOG_OUTPUT_FUNC(" %g", os.m_volumes[i]); + } + LOG_OUTPUT_FUNC("\n"); + } else + LOG_OUTPUT_FUNC("- %3u node %u\n", os.m_id, os.m_node); + } + LOG_OUTPUT_FUNC("Output mixing steps:\n"); + for(const auto &ms : m_output_mixing_steps) { + static const char *const modes[5] = { "clear", "copy", "copy+vol", "add", "add+vol" }; + LOG_OUTPUT_FUNC("- %s device %u:%u -> osd %u:%u level %g\n", modes[ms.m_mode], ms.m_device_index, ms.m_device_channel, ms.m_osd_index, ms.m_osd_channel, ms.m_linear_volume); + } } + } } -//------------------------------------------------- -// update - mix everything down to its final form -// and send it to the OSD layer -//------------------------------------------------- -void sound_manager::update(void *ptr, int param) + +//**// Global sound system update + +u64 sound_manager::rate_and_time_to_index(attotime time, u32 sample_rate) const { - VPRINTF(("sound_update\n")); + return time.m_seconds * sample_rate + ((time.m_attoseconds / 100'000'000) * sample_rate) / 10'000'000'000LL; +} - g_profiler.start(PROFILER_SOUND); +void sound_manager::update(s32) +{ + auto profile = g_profiler.start(PROFILER_SOUND); - // force all the speaker streams to generate the proper number of samples - m_samples_this_update = 0; - for (speaker_device &speaker : speaker_device_iterator(machine().root_device())) - speaker.mix(&m_leftmix[0], &m_rightmix[0], m_samples_this_update, (m_muted & MUTE_REASON_SYSTEM)); + if(m_osd_info.m_generation == 0xffffffff) + startup_cleanups(); - // now downmix the final result - u32 finalmix_step = machine().video().speed_factor(); - u32 finalmix_offset = 0; - s16 *finalmix = &m_finalmix[0]; - int sample; - for (sample = m_finalmix_leftover; sample < m_samples_this_update * 1000; sample += finalmix_step) - { - int sampindex = sample / 1000; - - // clamp the left side - s32 samp = m_leftmix[sampindex]; - if (samp < -32768) - samp = -32768; - else if (samp > 32767) - samp = 32767; - finalmix[finalmix_offset++] = samp; - - // clamp the right side - samp = m_rightmix[sampindex]; - if (samp < -32768) - samp = -32768; - else if (samp > 32767) - samp = 32767; - finalmix[finalmix_offset++] = samp; - } - m_finalmix_leftover = sample - m_samples_this_update * 1000; + mapping_update(); + streams_update(); + + m_last_sync_time = machine().time(); +} - // play the result - if (finalmix_offset > 0) +void sound_manager::streams_update() +{ + attotime now = machine().time(); { - if (!m_nosound_mode) - machine().osd().update_audio_stream(finalmix, finalmix_offset / 2); - machine().osd().add_audio_to_recording(finalmix, finalmix_offset / 2); - machine().video().add_sound_to_recording(finalmix, finalmix_offset / 2); - if (m_wavfile != nullptr) - wav_add_data_16(m_wavfile, finalmix, finalmix_offset); + std::unique_lock<std::mutex> lock(m_effects_mutex); + for(osd_output_stream &stream : m_osd_output_streams) { + u64 next_sync = rate_and_time_to_index(now, stream.m_rate); + stream.m_samples = next_sync - stream.m_last_sync; + stream.m_last_sync = next_sync; + } + + for(sound_stream *stream : m_ordered_streams) + stream->update_nodeps(); } - // see if we ticked over to the next second - attotime curtime = machine().time(); - bool second_tick = false; - if (curtime.seconds() != m_last_update.seconds()) + // Send the hooked samples to lua { - assert(curtime.seconds() == m_last_update.seconds() + 1); - second_tick = true; + std::map<std::string, std::vector<std::pair<const float *, int>>> sound_data; + for(device_sound_interface &sound : sound_interface_enumerator(machine().root_device())) + if(sound.get_sound_hook()) { + std::vector<std::pair<const float *, int>> buffers; + if(sound.device().type() == SPEAKER) { + const emu::detail::output_buffer_flat<sample_t> &buffer = m_speakers[static_cast<speaker_device &>(sound.device()).get_id()].m_buffer; + int samples = buffer.available_samples(); + for(int channel = 0; channel != sound.inputs(); channel++) + buffers.emplace_back(std::make_pair(buffer.ptrs(channel, 0), samples)); + + } else { + for(int channel = 0; channel != sound.outputs(); channel++) { + std::pair<sound_stream *, int> info = sound.output_to_stream_output(channel); + const emu::detail::output_buffer_flat<sample_t> &buffer = info.first->m_output_buffer; + buffers.emplace_back(std::make_pair(buffer.ptrs(info.second, 0), buffer.available_samples())); + } + } + sound_data.emplace(sound.device().tag(), std::move(buffers)); + } + + emulator_info::sound_hook(sound_data); } - // iterate over all the streams and update them - for (auto &stream : m_stream_list) - stream->update_with_accounting(second_tick); + for(sound_stream *stream : m_ordered_streams) + if(stream->device().type() != SPEAKER) + stream->sync(now); - // remember the update time - m_last_update = curtime; + for(osd_input_stream &stream : m_osd_input_streams) + stream.m_buffer.sync(); - // update sample rates if they have changed - for (auto &stream : m_stream_list) - stream->apply_sample_rate_changes(); + machine().osd().add_audio_to_recording(m_record_buffer.data(), m_record_samples); + machine().video().add_sound_to_recording(m_record_buffer.data(), m_record_samples); + if(m_wavfile) + util::wav_add_data_16(*m_wavfile, m_record_buffer.data(), m_record_samples); - // notify that new samples have been generated - emulator_info::sound_hook(); + m_effects_condition.notify_all(); +} - g_profiler.stop(); +//**// Resampler management +const audio_resampler *sound_manager::get_resampler(u32 fs, u32 ft) +{ + auto key = std::make_pair(fs, ft); + auto i = m_resamplers.find(key); + if(i != m_resamplers.end()) + return i->second.get(); + audio_resampler *res; + if(m_resampler_type == RESAMPLER_HQ) + res = new audio_resampler_hq(fs, ft, m_resampler_hq_latency, m_resampler_hq_length, m_resampler_hq_phases); + else + res = new audio_resampler_lofi(fs, ft); + m_resamplers[key].reset(res); + return res; } +void sound_manager::rebuild_all_resamplers() +{ + m_resamplers.clear(); -//------------------------------------------------- -// samples - fills the specified buffer with -// 16-bit stereo audio samples generated during -// the current frame -//------------------------------------------------- + for(auto &stream : m_stream_list) + stream->create_resamplers(); + + for(auto &stream : m_stream_list) + stream->lookup_history_sizes(); +} -void sound_manager::samples(s16 *buffer) +void sound_manager::set_resampler_type(u32 type) { - for (int sample = 0; sample < m_samples_this_update * 2; sample++) - { - *buffer++ = m_finalmix[sample]; - } + m_resampler_type = type; + rebuild_all_resamplers(); +} + +void sound_manager::set_resampler_hq_latency(double latency) +{ + m_resampler_hq_latency = latency; + rebuild_all_resamplers(); +} + +void sound_manager::set_resampler_hq_length(u32 length) +{ + m_resampler_hq_length = length; + rebuild_all_resamplers(); +} + +void sound_manager::set_resampler_hq_phases(u32 phases) +{ + m_resampler_hq_phases = phases; + rebuild_all_resamplers(); +} + +const char *sound_manager::resampler_type_names(u32 type) const +{ + using util::lang_translate; + + if(type == RESAMPLER_HQ) + return _("HQ"); + else + return _("LoFi"); } |